diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..70c11f580 Binary files /dev/null and b/.DS_Store differ diff --git a/404.html b/404.html index 255f764c4..6cf1f8b76 100644 --- a/404.html +++ b/404.html @@ -1 +1 @@ - ColorAide Documentation
\ No newline at end of file + ColorAide Documentation
\ No newline at end of file diff --git a/about/acknowledgments/index.html b/about/acknowledgments/index.html index d3646ded0..b4406a43c 100644 --- a/about/acknowledgments/index.html +++ b/about/acknowledgments/index.html @@ -1,4 +1,4 @@ Acknowledgments - ColorAide Documentation
Skip to content

Acknowledgments

All projects gain help and inspiration from somewhere, and we wanted to document the places in which we we gathered knowledge, ideas, and help.

Projects

Color.js

When we began writing ColorAide, we wanted a simple interface to deal with different colors. We also wanted to support CSS colors which a lot of people are familiar with. We had a number of questions about the CSS spec and stumbled on Color.js, a JavaScript library developed and maintained by the co-authors of the CSS spec. We found Color.js and its authors helped clarify a number of confusing points. Additionally, their library did end up heavily inspiring many aspects of our own API as its approach very much aligned with the direction we had already started down.

Culori

The Culori library helped inspire the use of cubic splines as interpolation methods.

References

When researching color vision deficiencies, aside from the usual scientific papers, a couple of sites were found to be quite helpful.

  • daltonlens.org was particularly helpful. As it comes from the perspective of an actual Protan, it provided reviews on various algorithm's and explained in great depth some approaches and why they were preferred over others.
  • ixora.io was another useful site that went into great details specifically about the Viénot approach.

Last update: June 13, 2023
\ No newline at end of file +
Last update: June 13, 2023
\ No newline at end of file diff --git a/about/changelog/index.html b/about/changelog/index.html index 3f7bdcc9d..737da9470 100644 --- a/about/changelog/index.html +++ b/about/changelog/index.html @@ -27,4 +27,4 @@

0.7.0

0.6.0

0.5.0

0.4.0

0.3.0

Breaking Changes

XYZ changes below will cause breakage as xyz now refers to XYZ with D65 instead of D50. Also, CSS identifiers changed per the recent specification change.

0.2.0

0.1.0

First non-alpha prerelease. Notable changes from the last alpha listed below.

Breaking Changes

There are some breaking changes if coming from the previous alpha releases. All sRGB cylindrical spaces' non-hue data ranges are no longer scaled to 0 - 100, but use 0 - 1. Hue ranges have not changed.


Last update: September 11, 2023
\ No newline at end of file +
Last update: September 11, 2023
\ No newline at end of file diff --git a/about/contributing/index.html b/about/contributing/index.html index 190acb0a5..e846e279e 100644 --- a/about/contributing/index.html +++ b/about/contributing/index.html @@ -1,4 +1,4 @@ Contributing & Support - ColorAide Documentation
Skip to content

Contributing & Support

There are many ways to help support this project, regardless of skills and abilities. If you enjoy this project and want to get involved, consider checking out one of the various ways below. Feel free to get creative, there may be other ways to contribute in which we have not thought of!

Become a Sponsor

Open source projects take time and money. Help support the project by becoming a sponsor. You can add your support at any tier you feel comfortable with. No amount is too little. We also accept one time contributions via PayPal.

GitHub Sponsors PayPal

Bug Reports

  1. Please read the documentation and search the issue tracker to try and find the answer to your question before posting an issue.

  2. When creating an issue on the repository, please provide as much info as possible:

    • Version being used.
    • Operating system.
    • Version of Python.
    • Errors in console.
    • Detailed description of the problem.
    • Examples for reproducing the error. You can post pictures, but if specific text or code is required to reproduce the issue, please provide the text in a plain text format for easy copy/paste.

    The more info provided, the greater the chance someone will take the time to answer, implement, or fix the issue.

  3. Be prepared to answer questions and provide additional information if required. Issues in which the creator refuses to respond to follow up questions will be marked as stale and closed.

Reviewing Code

Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss solutions to overcome weakness in the algorithm.

Answer Questions in Issues

Take time and answer questions and offer suggestions to people who've created issues in the issue tracker. Often people will have questions that you might have an answer for. Or maybe you know how to help them accomplish a specific task they are asking about. Feel free to share your experience to help others out.

Pull Requests

Pull requests are welcome, and a great way to help fix bugs and add new features.

Documentation Improvements

A ton of time has been spent not only creating and supporting this tool and related extensions, but also spent making this documentation. If you feel it is still lacking, show your appreciation for the tool and/or extensions by helping to improve the documentation.


Last update: June 13, 2023
\ No newline at end of file +
Last update: June 13, 2023
\ No newline at end of file diff --git a/about/license/index.html b/about/license/index.html index 52a625d3a..d1221784a 100644 --- a/about/license/index.html +++ b/about/license/index.html @@ -1,4 +1,4 @@ License - ColorAide Documentation
Skip to content

License

MIT License

Copyright © 2020 - 2023 Isaac Muse

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Last update: March 10, 2022
\ No newline at end of file +
Last update: March 10, 2022
\ No newline at end of file diff --git a/about/releases/1.0/index.html b/about/releases/1.0/index.html index c1df9351e..b2c843c84 100644 --- a/about/releases/1.0/index.html +++ b/about/releases/1.0/index.html @@ -93,29 +93,29 @@ Color(d)

Interpolation

Interpolation was an area we were generally unhappy with, so it was majorly overhauled.

Prior to 1.0, interpolation could be a bit awkward. Interpolation used to require the first color in the interpolation to be the calling object, and all the rest had to be fed in.

Color('red').interpolate(['blue', 'green', 'orange'])
 

When performing a simple mix, this felt natural and made sense:

Color('red').mix('blue', 0.25)
 

But with long chains of colors, this just felt cumbersome. To remedy this, we changed the interpolate and steps methods to @classmethods. We left mix as is since with two colors it feels natural.

So moving forward, interpolate and steps will execute interpolations from class methods.

>>> Color.interpolate(['red', 'blue', 'green', 'orange'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c39c50>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d301d0>
 >>> Color.steps(['red', 'blue', 'green', 'orange'], steps=10)
 [color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.56931 0.13909 -0.01995 / 1), color(--oklab 0.51066 0.05332 -0.16574 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.47459 -0.06841 -0.17179 / 1), color(--oklab 0.49717 -0.10435 -0.03206 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1), color(--oklab 0.61073 -0.07466 0.12558 / 1), color(--oklab 0.70171 -0.00903 0.14348 / 1), color(--oklab 0.79269 0.05661 0.16138 / 1)]
 

This means that you do not have to call the function from an instantiated object, and if you do, the instantiated color that is making the call will not be included in the interpolation. Only the colors in the list are considered during the interpolation.

>>> Color('white').interpolate(['red', 'blue', 'green', 'orange'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5cbba90>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c4a3d0>
 

This will make even more sense as we highlight the other changes.

Another problem we faced was the awkwardness of color stops and easing functions. Before we used to have a Piecewise object that you'd wrap a channel in to create color stops or inject easing functions and other various behaviors between colors, but it had to be applied on the second color in the chain, and this didn't quite work for the first color. If you wanted to add a stop to the first color, you then had to use a special stop parameter…it was unintuitive.

from coloraide import Piecewise
 Color('red').interpolate(['blue', Piecewise('orange', 0.75, progress=lambda t: t * 3), 'purple'], stop=0.25)
 

In 1.0, we simplified things greatly. Since interpolate and steps now require that all colors must be in the input list if they are to be considered for interpolation, we can process them all in a consistent and more intuitive manner.

As before, steps and interpolate allow you to set function parameters to generally control the behavior for the entire interpolation across all colors. You can also still add easing functions via progress which will also affect the entire interpolation by default, but now you can inject easing functions directly between colors which will only be applied between those two colors.

>>> Color.interpolate(['red', lambda t: t * 3, 'orange', 'purple'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab6312cd0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d89550>
 

You can also directly wrap any color in the list with stop to change the color stop position. Since the first color is now treated like all the other colors, there is no need for the stop function parameter either.

>>> from coloraide import stop
 >>> Color.interpolate([stop('red', 0.25), stop('orange', 0.75), 'purple'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5da52d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112db3bd0>
 

And if you are familiar with CSS color hinting, which essentially alters the midpoint between two color stops, we've added a hint function which takes a new relative midpoint and returns a midpoint easing function which essentially acts the same as CSS interpolation hints.

>>> from coloraide import hint
 >>> Color.interpolate(['yellow', 'pink'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d17290>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d89bd0>
 >>> Color.interpolate(['yellow', hint(0.25), 'pink'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d14510>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d65e10>
 

All of this makes for a less confusing experience when using interpolation. Additionally, all of the changes simplified the logic allowing us to even add a new interpolation method!

>>> Color.interpolate(['red', 'blue', 'green', 'orange'], method='bspline')
-<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x7fbab6d22e10>
+<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x112d82ed0>
 

Color Space Filters

In the beginning, the Color space object was created with a naive filtering system. It added a little overhead, but the real issue was the fact that it only filtered inputs through new, match, and through normal instantiation. It did not filter through almost any other method that accepted inputs. It was decided to leave color filtering up to the user.

>>> c = Color('display-p3', [1, 1, 0])
 >>> try:
 ...     if c.space() not in ['srgb', 'hsl', 'hwb']:
@@ -132,4 +132,4 @@
     print(e)  

Last update: March 7, 2023
\ No newline at end of file +
Last update: March 7, 2023
\ No newline at end of file diff --git a/advanced/index.html b/advanced/index.html index f674c03b8..e7bfa5eb9 100644 --- a/advanced/index.html +++ b/advanced/index.html @@ -18,9 +18,9 @@

Special Handling: Cylindrical Spaces

Sometimes, round trip accuracy can be compromised further for practical reasons. A common case where we make compromises is with cylindrical color models.

ColorAide aims to make colors easy to use, but the one case that can frustrate users is interpolating with an achromatic color using a cylindrical color space.

Achromatic colors do not have a hue, but all conversions end up yielding something for hue, even it it has no practical meaning. This can cause odd color shifts when interpolating with an achromatic color. In order to get logical results when doing interpolation, we detect when a color is achromatic (or very close to achromatic) and set the hues to undefined. This helps us to identify achromatic cases and helps us to prevent weird color shifts when interpolating between achromatic colors. Only if a user manually defines a hue do we respect it.

>>> Color.interpolate(['lch(75 100 180)', 'lch(75 0 0)'], space='lch')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d54a50>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128f78d0>
 >>> Color.interpolate(['lch(75 100 180)', 'lch(75 0 none)'], space='lch')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5d850>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112908610>
 

Because of floating point issues, conversions to cylindrical color spaces do not always satisfy the requirements to be recognized as achromatic colors.

As an example, HSL colors are achromatic when the sRGB color it is derived from has all color channels equal to each other. Let's say we convert the color darkgray to the XYZ D65 color space and then back again. We can see that what was once a color with all color channels equal to each other is now a color that has color channels very nearly equal to each other.

>>> c1 = Color('darkgray')
 >>> c1[:-1]
@@ -66,10 +66,10 @@
 >>> gray.normalize()
 color(--cam16-jmh 50 1.5823 none / 1)
 >>> Color.interpolate([gray, 'green'], space='cam16-jmh')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5f1d890>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128756d0>
 

Last update: June 11, 2023
\ No newline at end of file +
Last update: June 11, 2023
\ No newline at end of file diff --git a/api/index.html b/api/index.html index 40f1b6fb6..0f7c5eecd 100644 --- a/api/index.html +++ b/api/index.html @@ -402,4 +402,4 @@

Description

Parameters

Return


Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/assets/coloraide-extras/extra-notebook-61fde54c.js b/assets/coloraide-extras/extra-notebook-61fde54c.js new file mode 100644 index 000000000..75de5f6cd --- /dev/null +++ b/assets/coloraide-extras/extra-notebook-61fde54c.js @@ -0,0 +1,2 @@ +function _typeof(t){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof(t)}var runtime=function(t){"use strict";var e,n=Object.prototype,o=n.hasOwnProperty,r=Object.defineProperty||function(t,e,n){t[e]=n.value},i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",s=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag";function c(t,e,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{c({},"")}catch(t){c=function(t,e,n){return t[e]=n}}function u(t,e,n,o){var i=e&&e.prototype instanceof h?e:h,a=Object.create(i.prototype),s=new I(o||[]);return r(a,"_invoke",{value:S(t,n,s)}),a}function d(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}t.wrap=u;var p="suspendedStart",f="suspendedYield",m="executing",_="completed",g={};function h(){}function y(){}function v(){}var w={};c(w,a,(function(){return this}));var x=Object.getPrototypeOf,b=x&&x(x(T([])));b&&b!==n&&o.call(b,a)&&(w=b);var E=v.prototype=h.prototype=Object.create(w);function k(t){["next","throw","return"].forEach((function(e){c(t,e,(function(t){return this._invoke(e,t)}))}))}function L(t,e){function n(r,i,a,s){var l=d(t[r],t,i);if("throw"!==l.type){var c=l.arg,u=c.value;return u&&"object"===_typeof(u)&&o.call(u,"__await")?e.resolve(u.__await).then((function(t){n("next",t,a,s)}),(function(t){n("throw",t,a,s)})):e.resolve(u).then((function(t){c.value=t,a(c)}),(function(t){return n("throw",t,a,s)}))}s(l.arg)}var i;r(this,"_invoke",{value:function(t,o){function r(){return new e((function(e,r){n(t,o,e,r)}))}return i=i?i.then(r,r):r()}})}function S(t,n,o){var r=p;return function(i,a){if(r===m)throw new Error("Generator is already running");if(r===_){if("throw"===i)throw a;return{value:e,done:!0}}for(o.method=i,o.arg=a;;){var s=o.delegate;if(s){var l=A(s,o);if(l){if(l===g)continue;return l}}if("next"===o.method)o.sent=o._sent=o.arg;else if("throw"===o.method){if(r===p)throw r=_,o.arg;o.dispatchException(o.arg)}else"return"===o.method&&o.abrupt("return",o.arg);r=m;var c=d(t,n,o);if("normal"===c.type){if(r=o.done?_:f,c.arg===g)continue;return{value:c.arg,done:o.done}}"throw"===c.type&&(r=_,o.method="throw",o.arg=c.arg)}}}function A(t,n){var o=n.method,r=t.iterator[o];if(r===e)return n.delegate=null,"throw"===o&&t.iterator.return&&(n.method="return",n.arg=e,A(t,n),"throw"===n.method)||"return"!==o&&(n.method="throw",n.arg=new TypeError("The iterator does not provide a '"+o+"' method")),g;var i=d(r,t.iterator,n.arg);if("throw"===i.type)return n.method="throw",n.arg=i.arg,n.delegate=null,g;var a=i.arg;return a?a.done?(n[t.resultName]=a.value,n.next=t.nextLoc,"return"!==n.method&&(n.method="next",n.arg=e),n.delegate=null,g):a:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,g)}function C(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function B(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(C,this),this.reset(!0)}function T(t){if(t||""===t){var n=t[a];if(n)return n.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var r=-1,i=function n(){for(;++r=0;--i){var a=this.tryEntries[i],s=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var l=o.call(a,"catchLoc"),c=o.call(a,"finallyLoc");if(l&&c){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),B(n),g}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var o=n.completion;if("throw"===o.type){var r=o.arg;B(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,o){return this.delegate={iterator:T(t),resultName:n,nextLoc:o},"next"===this.method&&(this.arg=e),g}},t}("object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports:{});try{regeneratorRuntime=runtime}catch(t){"object"===("undefined"==typeof globalThis?"undefined":_typeof(globalThis))?globalThis.regeneratorRuntime=runtime:Function("r","regeneratorRuntime = r")(runtime)}!function(){"use strict";function t(){t=function(){return e};var e={},n=Object.prototype,o=n.hasOwnProperty,r=Object.defineProperty||function(t,e,n){t[e]=n.value},i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",s=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag";function c(t,e,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{c({},"")}catch(t){c=function(t,e,n){return t[e]=n}}function u(t,e,n,o){var i=e&&e.prototype instanceof f?e:f,a=Object.create(i.prototype),s=new S(o||[]);return r(a,"_invoke",{value:b(t,n,s)}),a}function d(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}e.wrap=u;var p={};function f(){}function m(){}function _(){}var g={};c(g,a,(function(){return this}));var h=Object.getPrototypeOf,y=h&&h(h(A([])));y&&y!==n&&o.call(y,a)&&(g=y);var v=_.prototype=f.prototype=Object.create(g);function w(t){["next","throw","return"].forEach((function(e){c(t,e,(function(t){return this._invoke(e,t)}))}))}function x(t,e){function n(r,i,a,s){var l=d(t[r],t,i);if("throw"!==l.type){var c=l.arg,u=c.value;return u&&"object"==_typeof(u)&&o.call(u,"__await")?e.resolve(u.__await).then((function(t){n("next",t,a,s)}),(function(t){n("throw",t,a,s)})):e.resolve(u).then((function(t){c.value=t,a(c)}),(function(t){return n("throw",t,a,s)}))}s(l.arg)}var i;r(this,"_invoke",{value:function(t,o){function r(){return new e((function(e,r){n(t,o,e,r)}))}return i=i?i.then(r,r):r()}})}function b(t,e,n){var o="suspendedStart";return function(r,i){if("executing"===o)throw new Error("Generator is already running");if("completed"===o){if("throw"===r)throw i;return{value:void 0,done:!0}}for(n.method=r,n.arg=i;;){var a=n.delegate;if(a){var s=E(a,n);if(s){if(s===p)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 l=d(t,e,n);if("normal"===l.type){if(o=n.done?"completed":"suspendedYield",l.arg===p)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(o="completed",n.method="throw",n.arg=l.arg)}}}function E(t,e){var n=e.method,o=t.iterator[n];if(void 0===o)return e.delegate=null,"throw"===n&&t.iterator.return&&(e.method="return",e.arg=void 0,E(t,e),"throw"===e.method)||"return"!==n&&(e.method="throw",e.arg=new TypeError("The iterator does not provide a '"+n+"' method")),p;var r=d(o,t.iterator,e.arg);if("throw"===r.type)return e.method="throw",e.arg=r.arg,e.delegate=null,p;var i=r.arg;return i?i.done?(e[t.resultName]=i.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=void 0),e.delegate=null,p):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,p)}function k(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function L(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function S(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(k,this),this.reset(!0)}function A(t){if(t||""===t){var e=t[a];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,r=function e(){for(;++n=0;--r){var i=this.tryEntries[r],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=o.call(i,"catchLoc"),l=o.call(i,"finallyLoc");if(s&&l){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),L(n),p}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var o=n.completion;if("throw"===o.type){var r=o.arg;L(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:A(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=void 0),p}},e}function e(t,e,n,o,r,i,a){try{var s=t[i](a),l=s.value}catch(t){return void n(t)}s.done?e(l):Promise.resolve(l).then(o,r)}function n(t){return function(){var n=this,o=arguments;return new Promise((function(r,i){var a=t.apply(n,o);function s(t){e(a,r,i,s,l,"next",t)}function l(t){e(a,r,i,s,l,"throw",t)}s(void 0)}))}}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:i}}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 a,s=!0,l=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){l=!0,a=t},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}}!function(){var e="";try{var o="srgb";window.matchMedia("(color-gamut: rec2020)").matches?o="rec2020":window.matchMedia("(color-gamut: p3)").matches&&(o="display-p3"),e=CSS.supports("color: color(display-p3 1 0 0)")?o:"srgb"}catch(t){e="srgb"}var i=null,a=!1,s="",l="",c={},u=/.*?_(\d+)$/,d=!1,p="",f=!1,m=/^( {1,4}|\t)/,_="\n_P='
{}
'\n_O='swatch'\n_N='transparent'\n_M='pycon'\n_L='playground'\n_K='gamut'\n_J='color'\n_I='{} {}%'\n_H='exceptions'\n_G='highlight'\n_F='class'\n_E='eval'\n_D=''\n_C=None\n_B=True\n_A=False\nimport xml.etree.ElementTree as Etree\nfrom collections.abc import Sequence,Mapping\nfrom collections import namedtuple\nimport ast\nfrom io import StringIO\nimport sys,re\nfrom functools import partial\nfrom pygments import highlight\nfrom pygments.lexers import get_lexer_by_name\nfrom pygments.formatters import find_formatter_class\nfrom coloraide import Color\nfrom coloraide.interpolate import Interpolator,normalize_domain\ntry:from coloraide_extras.everything import ColorAll\nexcept ImportError:from coloraide.everything import ColorAll\nPY310=(3,10)<=sys.version_info\nPY311=(3,11)<=sys.version_info\nWEBSPACE='srgb'\nAST_BLOCKS=ast.If,ast.For,ast.While,ast.Try,ast.With,ast.FunctionDef,ast.ClassDef,ast.AsyncFor,ast.AsyncWith,ast.AsyncFunctionDef\nif PY310:AST_BLOCKS=AST_BLOCKS+(ast.Match,)\nif PY311:AST_BLOCKS=AST_BLOCKS+(ast.TryStar,)\nRE_INIT=re.compile('^\\\\s*#\\\\s*pragma:\\\\s*init\\\\n(.*?)#\\\\s*pragma:\\\\s*init\\\\n',re.DOTALL|re.I)\nRE_COLOR_START=re.compile('(?i)(?:\\\\b(?\\n
\\n{results}\\n
\\n
\\n
\\n\\n
\\n
\\n
\\n
\\n\\n\\n\\n\\nGamut: {gamut}\\n
\\n'\ncode_id=0\nclass Ramp(list):0\nclass Steps(list):0\nclass Row(list):0\nclass ColorTuple(namedtuple('ColorTuple',['string',_J])):0\nclass AtomicString(str):0\nclass Break(Exception):0\nclass Continue(Exception):0\nHtmlRow=Row\nHtmlSteps=Steps\nHtmlGradient=Ramp\ndef _escape(txt):txt=txt.replace('&','&');txt=txt.replace('<','<');txt=txt.replace('>','>');return txt\nclass StreamOut:\n\tdef __init__(self):self.old=sys.stdout;self.stdout=StringIO();sys.stdout=self.stdout\n\tdef read(self):\n\t\tvalue=''\n\t\tif self.stdout is not _C:self.stdout.flush();value=self.stdout.getvalue();self.stdout=StringIO();sys.stdout=self.stdout\n\t\treturn value\n\tdef __enter__(self):return self\n\tdef __exit__(self,type,value,traceback):sys.stdout=self.old;self.old=_C;self.stdout=_C\ndef get_colors(result):\n\tdomain=[]\n\tif isinstance(result,AtomicString):yield find_colors(result)\n\tif isinstance(result,Row):yield Row([ColorTuple(c.to_string(fit=_A),c.clone())if isinstance(c,Color)else ColorTuple(c,ColorAll(c))for c in result])\n\telif isinstance(result,(Steps,Ramp)):t=type(result);yield t([c.clone()if isinstance(c,Color)else ColorAll(c)for c in result])\n\telif isinstance(result,Color):yield[ColorTuple(result.to_string(fit=_A),result.clone())]\n\telif isinstance(result,Interpolator):\n\t\tif result._domain:domain=result._domain;result.domain(normalize_domain(result._domain))\n\t\tgrad=Ramp(result.steps(steps=5,max_delta_e=2.3))\n\t\tif domain:result._domain=domain;domain=[]\n\t\tyield grad\n\telif isinstance(result,str):\n\t\ttry:yield[ColorTuple(result,ColorAll(result))]\n\t\texcept Exception:pass\n\telif isinstance(result,(list,tuple)):\n\t\tfor r in result:\n\t\t\tfor x in get_colors(r):\n\t\t\t\tif x:yield x\ndef find_colors(text):\n\tcolors=[]\n\tfor m in RE_COLOR_START.finditer(text):\n\t\tstart=m.start();mcolor=ColorAll.match(text,start=start)\n\t\tif mcolor is not _C:colors.append(ColorTuple(text[mcolor.start:mcolor.end],mcolor.color))\n\treturn colors\ndef evaluate_with(node,g,loop,index=0):\n\tl=len(node.items)-1;withitem=node.items[index]\n\tif withitem.context_expr:\n\t\twith eval(compile(ast.Expression(withitem.context_expr),_D,_E),g)as w:\n\t\t\tg[withitem.optional_vars.id]=w\n\t\t\tif index=l2-1 or l1==l2:\n\t\t\t\tfor (e,p) in enumerate(node.patterns[:-1]if star else node.patterns):\n\t\t\t\t\tif not compare_match(s[e],g,p):return _A\n\t\t\t\tif star and node.patterns[-1].name:g[node.patterns[-1].name]=s[l2-1:]\n\t\t\t\treturn _B\n\t\treturn _A\n\telif isinstance(node,ast.MatchMapping):\n\t\tif isinstance(s,Mapping):\n\t\t\tstar=node.rest;l1,l2=len(s),len(node.patterns)\n\t\t\tif star and l1>=l2 or l1==l2:\n\t\t\t\tkeys=set()\n\t\t\t\tfor (kp,vp) in zip(node.keys,node.patterns):\n\t\t\t\t\tkey=eval(compile(ast.Expression(kp),_D,_E),g);keys.add(key)\n\t\t\t\t\tif key not in s:return _A\n\t\t\t\t\tif not compare_match(s[key],g,vp):return _A\n\t\t\t\tif star:g[star]={k:v for(k,v)in s.items()if k not in keys}\n\t\t\t\treturn _B\n\t\treturn _A\n\telif isinstance(node,ast.MatchClass):\n\t\tname=g.get(node.cls.id,_C)\n\t\tif name is _C:raise NameError(\"name '{}' is not defined\".format(node.cls.id))\n\t\tif not isinstance(s,name):return _A\n\t\tma=getattr(s,'__match_args__',());l1=len(ma);l2=len(node.patterns)\n\t\tif l1>> '+line\n\t\t\telse:stmt[i]='... '+line\n\t\tcommand+=A.join(stmt)\n\t\tif isinstance(node,AST_BLOCKS):command+='\\n... '\n\t\ttry:\n\t\t\twith StreamOut()as s:\n\t\t\t\tfor x in evaluate(node,g):\n\t\t\t\t\tresult.append(x);text=s.read()\n\t\t\t\t\tif text:result.append(AtomicString(text))\n\t\t\t\tconsole+=command\n\t\texcept Exception as e:\n\t\t\tif no_except:\n\t\t\t\tif not inline:from pymdownx.superfences import SuperFencesException;raise SuperFencesException from e\n\t\t\t\telse:from pymdownx.inlinehilite import InlineHiliteException;raise InlineHiliteException from e\n\t\t\timport traceback;console+='{}\\n{}'.format(command,traceback.format_exc());break\n\t\tresult_text=A\n\t\tfor r in result:\n\t\t\tif r is _C:continue\n\t\t\tfor clist in get_colors(r):\n\t\t\t\tif clist:colors.append(clist)\n\t\t\tresult_text+='{}{}'.format(repr(r)if isinstance(r,str)and not isinstance(r,AtomicString)else str(r),A if not isinstance(r,AtomicString)else'')\n\t\tconsole+=result_text\n\treturn console,colors\ndef colorize(src,lang,**options):HtmlFormatter=find_formatter_class('html');lexer=get_lexer_by_name(lang,**options);formatter=HtmlFormatter(cssclass=_G,wrapcode=_B);return highlight(src,lexer,formatter).strip()\ndef color_command_validator(language,inputs,options,attrs,md):\n\tvalid_inputs={_H,'play','wheel'}\n\tfor (k,v) in inputs.items():\n\t\tif k in valid_inputs:options[k]=_B;continue\n\t\tattrs[k]=v\n\treturn _B\ndef _color_command_console(colors,gamut=WEBSPACE):\n\tB=' ';A='
{}
';el='';bar=_A;values=[]\n\tfor item in colors:\n\t\tis_grad=isinstance(item,HtmlGradient);is_steps=isinstance(item,Steps)\n\t\tif is_grad or is_steps:\n\t\t\tcurrent=total=percent=last=0\n\t\t\tif isinstance(item,Steps):total=len(item);percent=100/total;current=percent\n\t\t\tif bar:el+=A.format(B.join(values));values=[]\n\t\t\tsub_el1='
{}
';style='--swatch-stops: ';stops=[]\n\t\t\tfor (e,color) in enumerate(item):\n\t\t\t\tcolor.fit(gamut);color_str=color.convert(gamut).to_string()\n\t\t\t\tif current:\n\t\t\t\t\tif is_steps:stops.append(_I.format(color_str,str(last)));stops.append(_I.format(color_str,str(current)))\n\t\t\t\t\telse:stops.append(color_str)\n\t\t\t\t\tlast=current\n\t\t\t\t\tif e'.format(style);el+=sub_el1.format(sub_el2);bar=_A\n\t\telse:\n\t\t\tis_row=_A\n\t\t\tif isinstance(item,Row):\n\t\t\t\tis_row=_B\n\t\t\t\tif bar and values:el+=A.format(B.join(values));values=[]\n\t\t\t\tbar=_A\n\t\t\tbar=_B\n\t\t\tfor color in item:\n\t\t\t\tbase_classes=_O\n\t\t\t\tif not color.color.in_gamut(gamut):base_classes+=' out-of-gamut'\n\t\t\t\tcolor.color.fit(gamut);srgb=color.color.convert(gamut);value1=srgb.to_string(alpha=_A);value2=srgb.to_string();style='--swatch-stops: {} 50%, {} 50%'.format(value1,value2);title=color.string;classes=base_classes;c=''.format(style=style);c='{color}'.format(classes=classes,color=c,title=title);values.append(c)\n\t\t\tif is_row and values:el+=A.format(B.join(values));values=[];bar=_A\n\tif bar:el+=A.format(B.join(values));values=[]\n\treturn el\ndef _color_command_formatter(src='',language='',class_name=_C,options=_C,md='',init='',**kwargs):\n\tC='';B='formatter';A='fenced_code_block';global code_id;from pymdownx.superfences import SuperFencesException;gamut=kwargs.get(_K,WEBSPACE);wheel=options.get('wheel',_A);play=options.get('play',_A)if options is not _C else _A\n\tif not play and language==_L:play=_B\n\tif not play:return md.preprocessors[A].extension.superfences[0][B](src=src,class_name=class_name,language='py',md=md,options=options,**kwargs)\n\ttry:\n\t\tif wheel:\n\t\t\tgamut='srgb';exceptions=options.get(_H,_A)if options is not _C else _A;_,colors=execute(src.strip(),not exceptions,init=init);l=len(colors)\n\t\t\tif l not in(12,24,48):raise SuperFencesException('Color wheel requires either 12, 24, or 48 colors')\n\t\t\tcolors=[c[0].color for c in colors]\n\t\t\tif l==12:freq=4;offset=6\n\t\t\telif l==24:freq=8;offset=12\n\t\t\telse:freq=16;offset=24\n\t\t\tprimary=list(reversed(colors[::freq]));secondary=list(reversed(colors[offset::freq]+[colors[offset//3]]));tertiary=list(reversed(colors[::offset//6]));color_rings=[primary,secondary,tertiary];extra_rings_start='';extra_rings_end=''\n\t\t\tif l>12:extra_rings_start='
';extra_rings_end+=C;color_rings.append(list(reversed(colors[::offset//12])))\n\t\t\tif l>24:extra_rings_start='
'+extra_rings_start;extra_rings_end+=C;color_rings.append(list(reversed(colors)))\n\t\t\tcolor_stops=''\n\t\t\tfor (i,colors) in enumerate(color_rings,1):\n\t\t\t\ttotal=len(colors);percent=100/total;current=percent;last=-1;stops=[]\n\t\t\t\tfor (e,color) in enumerate(colors):\n\t\t\t\t\tcolor.fit(gamut);color_str=color.convert(gamut).to_string()\n\t\t\t\t\tif current:\n\t\t\t\t\t\tstops.append(_I.format(color_str,str(last)));stops.append(_I.format(color_str,str(current)));last=current\n\t\t\t\t\t\tif e
{}
'.format(colorize(traceback.format_exc(),_M))\n\treturn el\ndef live_color_command_formatter(init='',gamut=WEBSPACE):return partial(_live_color_command_formatter,init=init,gamut=gamut)\ndef live_color_command_validator(language,inputs,options,attrs,md):value=color_command_validator(language,inputs,options,attrs,md);options[_H]=_B;return value\ndef render_console(*args,**kwargs):\n\tC='.swatch-bar';B='code';A='id_num';from js import document;gamut=kwargs.get(_K,WEBSPACE)\n\ttry:\n\t\tinputs=document.getElementById('__playground-inputs_{}'.format(globals()[A]));results=document.getElementById('__playground-results_{}'.format(globals()[A]));footer=document.querySelector('#__playground_{} .gamut'.format(globals()[A]));result=live_color_command_formatter(LIVE_INIT,gamut)(inputs.value);temp=document.createElement('div');temp.innerHTML=result;cmd=results.querySelector('.color-command')\n\t\tfor el in cmd.querySelectorAll(C):el.remove()\n\t\tfor el in temp.querySelectorAll(C):cmd.insertBefore(el,cmd.lastChild)\n\t\tfooter.innerHTML='Gamut: {}'.format(gamut);pre=cmd.querySelector('pre');pre.replaceChild(temp.querySelector(B),pre.querySelector(B));temp.remove();scrollingElement=results.querySelector(B);scrollingElement.scrollTop=scrollingElement.scrollHeight\n\texcept Exception as e:print(e)\ndef render_notebook(*args,**kwargs):\n\tM='diagram';L='pymdownx.blocks.tab';K='pymdownx.blocks.admonition';J='pymdownx.arithmatex';I='pymdownx.keys';H='pymdownx.magiclink';G='pymdownx.inlinehilite';F='pymdownx.superfences';E='markdown.extensions.smarty';D='markdown.extensions.toc';C='validator';B='format';A='name';import markdown;from pymdownx import slugs,superfences;from js import document;gamut=kwargs.get(_K,WEBSPACE);text=globals().get('content','');extensions=[D,E,'pymdownx.betterem','markdown.extensions.attr_list','markdown.extensions.tables','markdown.extensions.abbr','markdown.extensions.footnotes',F,'pymdownx.highlight',G,H,'pymdownx.tilde','pymdownx.caret','pymdownx.smartsymbols','pymdownx.emoji','pymdownx.escapeall','pymdownx.tasklist','pymdownx.striphtml','pymdownx.snippets',I,'pymdownx.saneheaders',J,K,'pymdownx.blocks.details','pymdownx.blocks.html','pymdownx.blocks.definition',L];extension_configs={D:{'slugify':slugs.slugify(case='lower'),'permalink':''},E:{'smart_quotes':_A},J:{'generic':_B,'block_tag':'pre'},F:{'preserve_tabs':_B,'custom_fences':[{A:M,_F:M,B:superfences.fence_code_format},{A:_L,_F:_L,B:color_command_formatter(LIVE_INIT,gamut),C:live_color_command_validator},{A:'python',_F:_G,B:color_command_formatter(LIVE_INIT,gamut),C:live_color_command_validator},{A:'py',_F:_G,B:color_command_formatter(LIVE_INIT,gamut),C:live_color_command_validator}]},G:{'custom_inline':[{A:_J,_F:_J,B:color_formatter(LIVE_INIT,gamut)}]},H:{'repo_url_shortener':_B,'repo_url_shorthand':_B,'social_url_shorthand':_B,'user':'facelessuser','repo':'coloraide'},I:{'separator':'+'},L:{'alternate_style':_B},K:{'types':['new','settings','note','abstract','info','tip','success','question','warning','failure','danger','bug','example','quote']}}\n\ttry:html=markdown.markdown(text,extensions=extensions,extension_configs=extension_configs)\n\texcept Exception:html=''\n\tcontent=document.getElementById('__notebook-render');content.innerHTML=html\n\naction = globals().get('action')\nif action == 'notebook':\n callback = render_notebook\nelse:\n callback = render_console\n\ncallback(gamut='".concat(e,"')\n"),g=window.colorNotebook.defaultPlayground,h=function(t){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\n````````py play\n".concat(t,"\n````````\n")},y=!1,v=!1,w=function(){f=!0,window.document.dispatchEvent(new Event("DOMContentLoaded",{bubbles:!0,cancelable:!0})),window.document$.next()},x=function(t){var e=window.pageXOffset||(document.documentElement||document.body.parentNode||document.body).scrollLeft,n=window.pageYOffset||(document.documentElement||document.body.parentNode||document.body).scrollTop;t.style.height="5px",t.style.height="".concat(t.scrollHeight,"px"),window.scrollTo(e,n)},b=function(t){return encodeURIComponent(t).replace(/[.!'()*]/g,(function(t){return"%".concat(t.charCodeAt(0).toString(16))}))},E=function(){var e=n(t().mark((function e(n){var o;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return(o=document.getElementById("__playground-inputs_".concat(n))).setAttribute("readonly",""),i.globals.set("id_num",n),i.globals.set("action","playground"),t.next=6,i.runPythonAsync(_);case 6:o.removeAttribute("readonly");case 7:case"end":return t.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),k=function(){var e=n(t().mark((function e(n){var o;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return i.globals.set("content",n),i.globals.set("action","notebook"),t.next=4,i.runPythonAsync(_);case 4:(o=document.getElementById("__notebook-input"))&&(s=n,o.value=n),window.location.hash&&(window.location.href=window.location.href);case 7:case"end":return t.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),L=function(){var e=n(t().mark((function e(n){var o,a,s,l,c,u;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(d){t.next=5;break}return d=!0,t.next=4,loadPyodide({indexURL:"https://cdn.jsdelivr.net/pyodide/v0.23.4/full/",fullStdLib:!1});case 4:i=t.sent;case 5:if((y||!n)&&(v||n)){t.next=14;break}o="".concat(window.location.origin,"/").concat(window.location.pathname.split("/")[1],"/playground/"),a=n?window.colorNotebook.notebookWheels:window.colorNotebook.playgroundWheels,s=[],n?y=!0:v=!0,l=r(a);try{for(l.s();!(c=l.n()).done;)(u=c.value).endsWith(".whl")?s.push(o+u):s.push(u)}catch(t){l.e(t)}finally{l.f()}return t.next=14,i.loadPackage(s);case 14:case"end":return t.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),S=function(t,e,n){var o=null==e?"Loading...":e,r=n?"loading relative":"loading",i=document.createElement("template");i.innerHTML='
').concat(o,"
"),t.appendChild(i.content.firstChild)},A=function(t){t.querySelector(".loading")&&t.removeChild(t.querySelector(".loading"))},C=function(t){if("Tab"===t.key){var e=t.target;if(e.selectionStart!==e.selectionEnd){t.preventDefault();for(var n=e.selectionStart,o=e.selectionEnd,r=e.value;n>0&&"\n"!==r[n-1];)n--;for(;o>0&&"\n"!==r[o-1]&&o2e3?alert("Code must be small enough to generate a shareable URL under 2000 characters!"):navigator.clipboard.writeText(l).then(n(t().mark((function e(){return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:alert("Link copied to clipboard :)");case 1:case"end":return t.stop()}}),e)}))),n(t().mark((function e(){return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:alert("Failed to copy link clipboard!");case 1:case"end":return t.stop()}}),e)}))));case 7:case"end":return e.stop()}}),e)})))),p.addEventListener("click",n(t().mark((function e(){var n,r;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(!a){t.next=2;break}return t.abrupt("return");case 2:return a=!0,n=s.querySelector("form"),S(n,null,!0),(r=document.querySelectorAll(".playground .playground-run"))&&r.forEach((function(t){t.setAttribute("disabled","")})),t.next=9,L(!1);case 9:return i.querySelector("code").innerHTML="",t.next=12,E(o);case 12:r&&r.forEach((function(t){t.removeAttribute("disabled")})),A(n),s.classList.toggle("hidden"),i.classList.toggle("hidden"),l.classList.toggle("hidden"),d.classList.toggle("hidden"),p.classList.toggle("hidden"),f.classList.toggle("hidden"),delete c[o],a=!1;case 22:case"end":return t.stop()}}),e)})))),f.addEventListener("click",(function(){r.value=c[o],delete c[o],s.classList.toggle("hidden"),i.classList.toggle("hidden"),l.classList.toggle("hidden"),d.classList.toggle("hidden"),p.classList.toggle("hidden"),f.classList.toggle("hidden")}))}));case 4:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),I=function(){var e=n(t().mark((function e(o,r){var i,a,s,u,d,f,m,_,y;return t().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(c={},!window.location.pathname.endsWith("/playground/")){e.next=32;break}if(i=r||new URLSearchParams(window.location.search),a="Loading Pyodide...",s="Loading Notebook...",u=i.has("source")?i.get("source"):i.get("notebook"),d=document.querySelector("article"),null===u||!u.trim()){e.next=16;break}return S(d,a),e.next=11,L(!0);case 11:A(d),S(d,s);try{f=i.has("source")?"source":"notebook",p=decodeURIComponent(i.toString()),m="",_=new XMLHttpRequest,l=u,_.open("GET",u,!0),_.onload=n(t().mark((function e(){return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return 4===_.readyState&&200===_.status&&(m=_.responseText),"source"===f&&(m=h(m)),t.next=4,k(m);case 4:return t.next=6,B(o);case 6:A(d),w();case 8:case"end":return t.stop()}}),e)}))),_.send()}catch(t){}e.next=30;break;case 16:return l="",y=h(i.has("code")?i.get("code"):g),p=decodeURIComponent(i.toString()),S(d,a),e.next=22,L(!0);case 22:return A(d),S(d,s),e.next=26,k(y);case 26:return e.next=28,B(o);case 28:A(d),w();case 30:e.next=35;break;case 32:l="",p="",B(o);case 35:case"end":return e.stop()}}),e)})));return function(t,n){return e.apply(this,arguments)}}();document.addEventListener("click",(function(t){var e=window.location.pathname.split("/")[1],n=t.target||t.srcElement;if("A"===n.tagName&&I&&n.getAttribute("href")&&n.host===window.location.host&&window.location.pathname==="/".concat(e,"/playground/")&&window.location.pathname===n.pathname&&window.location.search!==n.search){t.preventDefault();var o=new URLSearchParams(n.search);I(!1,o)}}),!0),window.addEventListener("popstate",(function(){var t=window.location.pathname.split("/")[1];window.location.pathname==="/".concat(t,"/playground/")&&(decodeURIComponent(new URLSearchParams(window.location.search).toString())!==p&&I(!1))})),window.addEventListener("unload",(function(){f=!1})),window.document$.subscribe((function(){f?f=!1:I(!0)}))}()}(); +//# sourceMappingURL=extra-notebook-61fde54c.js.map diff --git a/assets/coloraide-extras/extra-notebook-b4a7bff2.js.map b/assets/coloraide-extras/extra-notebook-61fde54c.js.map similarity index 99% rename from assets/coloraide-extras/extra-notebook-b4a7bff2.js.map rename to assets/coloraide-extras/extra-notebook-61fde54c.js.map index a0bf1bcfa..59e9aea4d 100644 --- a/assets/coloraide-extras/extra-notebook-b4a7bff2.js.map +++ b/assets/coloraide-extras/extra-notebook-61fde54c.js.map @@ -1 +1 @@ -{"version":3,"file":"extra-notebook-b4a7bff2.js","sources":["extra-notebook.js"],"sourcesContent":["(() => {\n let webspace = ''\n try {\n let gamut = 'srgb'\n if (window.matchMedia(\"(color-gamut: rec2020)\").matches) {\n gamut = 'rec2020'\n } else if (window.matchMedia(\"(color-gamut: p3)\").matches) {\n gamut = 'display-p3'\n }\n webspace = (CSS.supports('color: color(display-p3 1 0 0)')) ? gamut : 'srgb'\n } catch {\n webspace = 'srgb'\n }\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\naction = globals().get('action')\nif action == 'notebook':\n callback = render_notebook\nelse:\n callback = render_console\n\ncallback(gamut='${webspace}')\n`\n\n const defContent = window.colorNotebook.defaultPlayground\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\n\\`\\`\\`\\`\\`\\`\\`\\`py play\n${content}\n\\`\\`\\`\\`\\`\\`\\`\\`\n`\n }\n\n let notebookInstalled = false\n let playgroundInstalled = false\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 window.document$.next()\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 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 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 full => {\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.23.4/full/\",\n fullStdLib: false\n })\n }\n\n if ((!notebookInstalled && full) || (!playgroundInstalled && !full)) {\n const base = `${window.location.origin}/${window.location.pathname.split('/')[1]}/playground/`\n const packages = (full) ? window.colorNotebook.notebookWheels : window.colorNotebook.playgroundWheels\n const installs = []\n if (full) {\n notebookInstalled = true\n } else {\n playgroundInstalled = true\n }\n for (const s of packages) {\n if (s.endsWith('.whl')) {\n installs.push(base + s)\n } else {\n installs.push(s)\n }\n }\n await pyodide.loadPackage(installs)\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 = `
${loaderLabel}
`\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\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 main(false, search) // 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\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(true)\n await pyrender(raw)\n await init()\n hideBusy(article)\n fakeDOMContentLoaded()\n })\n }\n\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 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 (path.length > 2000) {\n alert( // eslint-disable-line no-alert\n \"Code must be small enough to generate a shareable URL under 2000 characters!\"\n )\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(false)\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, search) => {\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 = search || 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(true)\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(true)\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, true)\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 = false\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 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":["webspace","gamut","window","matchMedia","matches","CSS","supports","_unused","pyodide","busy","raw","gist","editTemp","reIdNum","initialized","lastSearch","fake","tabStart","pycode","concat","defContent","colorNotebook","defaultPlayground","getContent","content","notebookInstalled","playgroundInstalled","fakeDOMContentLoaded","document","dispatchEvent","Event","bubbles","cancelable","document$","next","textResize","inpt","scrollLeft","pageXOffset","documentElement","body","parentNode","scrollTop","pageYOffset","style","height","scrollHeight","scrollTo","encodeuri","uri","encodeURIComponent","replace","c","charCodeAt","toString","pyexecute","_ref","_asyncToGenerator","_regeneratorRuntime","mark","_callee","currentID","currentInputs","wrap","_context","prev","getElementById","setAttribute","globals","set","runPythonAsync","removeAttribute","stop","_x","apply","this","arguments","pyrender","_ref2","_callee2","text","src","_context2","value","location","hash","href","_x2","setupPyodide","_ref3","_callee3","full","base","packages","installs","_iterator","_step","s","_context3","loadPyodide","indexURL","fullStdLib","sent","origin","pathname","split","notebookWheels","playgroundWheels","_createForOfIteratorHelper","n","done","endsWith","push","err","e","f","loadPackage","_x3","showBusy","target","label","relative","loaderLabel","classes","template","createElement","innerHTML","appendChild","firstChild","hideBusy","querySelector","removeChild","handleTab","key","selectionStart","selectionEnd","preventDefault","start","end","length","lines","substr","i","shiftKey","join","init","_ref4","_callee12","first","notebook","playgrounds","notebookInput","_context12","querySelectorAll","addEventListener","id","classList","toggle","_ref5","_callee4","_context4","prompt","history","pushState","URLSearchParams","main","_x5","_ref6","_callee5","_context5","source","_x6","_callee6","render","article","_context6","forEach","pg","inputs","results","pgcode","buttonEdit","buttonShare","buttonRun","buttonCancel","stopPropagation","el","parent","getAttribute","clipboardData","setData","queryCommandSupported","textarea","textContent","position","select","execCommand","ex","_callee7","_context7","focus","_callee10","loc","path","_context10","startsWith","protocol","host","alert","navigator","clipboard","writeText","then","_callee8","_context8","_callee9","_context9","_callee11","form","buttons","_context11","abrupt","b","_x4","_ref13","_callee14","search","params","loadMsg","pageMsg","gistType","xhr","_context14","has","get","trim","decodeURIComponent","XMLHttpRequest","open","onload","_callee13","_context13","readyState","status","responseText","send","_x7","_x8","srcElement","tagName","subscribe"],"mappings":"urdAAA,WACE,IAAIA,EAAW,GACf,IACE,IAAIC,EAAQ,OACRC,OAAOC,WAAW,0BAA0BC,QAC9CH,EAAQ,UACCC,OAAOC,WAAW,qBAAqBC,UAChDH,EAAQ,cAEVD,EAAYK,IAAIC,SAAS,kCAAqCL,EAAQ,MACxE,CAAE,MAAAM,GACAP,EAAW,MACb,CACA,IAAIQ,EAAU,KACVC,GAAO,EACPC,EAAM,GACNC,EAAO,GACPC,EAAW,CAAA,EACTC,EAAU,aACZC,GAAc,EACdC,EAAa,GACbC,GAAO,EACLC,EAAW,eAKXC,EAAM,86tBAAAC,OASInB,EACjB,QAEOoB,EAAalB,OAAOmB,cAAcC,kBAElCC,EAAa,SAAAC,GACjB,MAAAL,gSAAAA,OAQFK,EAAO,iBAKHC,GAAoB,EACpBC,GAAsB,EAEpBC,EAAuB,WAE3BX,GAAO,EACPd,OAAO0B,SAASC,cAAc,IAAIC,MAAM,mBAAoB,CAC1DC,SAAS,EACTC,YAAY,KAEd9B,OAAO+B,UAAUC,QAGbC,EAAa,SAAAC,GAGjB,IAAMC,EAAanC,OAAOoC,cACvBV,SAASW,iBAAmBX,SAASY,KAAKC,YAAcb,SAASY,MAAMH,WAEpEK,EAAaxC,OAAOyC,cACvBf,SAASW,iBAAmBX,SAASY,KAAKC,YAAcb,SAASY,MAAME,UAE1EN,EAAKQ,MAAMC,OAAS,MACpBT,EAAKQ,MAAMC,OAAM,GAAA1B,OAAMiB,EAAKU,aAAgB,MAE5C5C,OAAO6C,SAASV,EAAYK,IAGxBM,EAAY,SAAAC,GAGhB,OAAOC,mBAAmBD,GAAKE,QAAQ,aAAa,SAAAC,GAClD,MAAA,IAAAjC,OAAWiC,EAAEC,WAAW,GAAGC,SAAS,IACtC,KAGIC,EAAS,WAAA,IAAAC,EAAAC,EAAAC,IAAAC,MAAG,SAAAC,EAAMC,GAAS,IAAAC,SAAAJ,IAAAK,MAAA,SAAAC,GAAA,OAAA,OAAAA,EAAAC,KAAAD,EAAA9B,MAAA,KAAA,EAMY,OAHrC4B,EAAgBlC,SAASsC,sCAAc/C,OAAwB0C,KACvDM,aAAa,WAAY,IACvC3D,EAAQ4D,QAAQC,IAAI,SAAUR,GAC9BrD,EAAQ4D,QAAQC,IAAI,SAAU,cAAaL,EAAA9B,KAAA,EACrC1B,EAAQ8D,eAAepD,GAAO,KAAA,EACpC4C,EAAcS,gBAAgB,YAAW,KAAA,EAAA,IAAA,MAAA,OAAAP,EAAAQ,OAAA,GAAAZ,EAC1C,KAAA,OATKL,SAASkB,GAAA,OAAAjB,EAAAkB,MAAAC,KAAAC,WAAA,CAAA,GAWTC,EAAQ,WAAA,IAAAC,EAAArB,EAAAC,IAAAC,MAAG,SAAAoB,EAAMC,GAAI,IAAAC,SAAAvB,IAAAK,MAAA,SAAAmB,GAAA,OAAA,OAAAA,EAAAjB,KAAAiB,EAAAhD,MAAA,KAAA,EAIgB,OADzC1B,EAAQ4D,QAAQC,IAAI,UAAWW,GAC/BxE,EAAQ4D,QAAQC,IAAI,SAAU,YAAWa,EAAAhD,KAAA,EACnC1B,EAAQ8D,eAAepD,GAAO,KAAA,GAC9B+D,EAAMrD,SAASsC,eAAe,uBAElCxD,EAAMsE,EACNC,EAAIE,MAAQH,GAEV9E,OAAOkF,SAASC,OAElBnF,OAAOkF,SAASE,KAAOpF,OAAOkF,SAASE,MACxC,KAAA,EAAA,IAAA,MAAA,OAAAJ,EAAAV,OAAA,GAAAO,EACF,KAAA,OAfKF,SAAQU,GAAA,OAAAT,EAAAJ,MAAAC,KAAAC,WAAA,CAAA,GAiBRY,EAAY,WAAA,IAAAC,EAAAhC,EAAAC,IAAAC,MAAG,SAAA+B,EAAMC,GAAI,IAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,SAAAvC,IAAAK,MAAA,SAAAmC,GAAA,OAAA,OAAAA,EAAAjC,KAAAiC,EAAAhE,MAAA,KAAA,EAAA,GAGxBpB,EAAW,CAAAoF,EAAAhE,KAAA,EAAA,KAAA,CACI,OAAlBpB,GAAc,EAAIoF,EAAAhE,KAAA,EACFiE,YAAY,CAC1BC,SAAU,iDACVC,YAAY,IACZ,KAAA,EAHF7F,EAAO0F,EAAAI,KAAA,KAAA,EAAA,IAMH7E,IAAqBkE,KAAWjE,GAAwBiE,GAAK,CAAAO,EAAAhE,KAAA,GAAA,KAAA,CAC3D0D,EAAI,GAAAzE,OAAMjB,OAAOkF,SAASmB,OAAM,KAAApF,OAAIjB,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAAE,gBAC1EZ,EAAYF,EAAQzF,OAAOmB,cAAcqF,eAAiBxG,OAAOmB,cAAcsF,iBAC/Eb,EAAW,GACbH,EACFlE,GAAoB,EAEpBC,GAAsB,EACvBqE,EAAAa,EACef,GAAQ,IAAxB,IAAAE,EAAAE,MAAAD,EAAAD,EAAAc,KAAAC,OAAWb,EAACD,EAAAb,OACJ4B,SAAS,QACbjB,EAASkB,KAAKpB,EAAOK,GAErBH,EAASkB,KAAKf,EAEjB,CAAA,MAAAgB,GAAAlB,EAAAmB,EAAAD,EAAA,CAAA,QAAAlB,EAAAoB,GAAA,CAAA,OAAAjB,EAAAhE,KAAA,GACK1B,EAAQ4G,YAAYtB,GAAS,KAAA,GAAA,IAAA,MAAA,OAAAI,EAAA1B,OAAA,GAAAkB,EAEtC,KAAA,OA7BKF,SAAY6B,GAAA,OAAA5B,EAAAf,MAAAC,KAAAC,WAAA,CAAA,GA+BZ0C,EAAW,SAACC,EAAQC,EAAOC,GAG/B,IAAMC,EAAe,MAAOF,EAA2C,aAAeA,EAChFG,EAAUF,EAAW,mBAAqB,UAC1CG,EAAWhG,SAASiG,cAAc,YACxCD,EAASE,UAAS,eAAA3G,OAAkBwG,EAAOxG,qCAAAA,OAAoCuG,EAAyB,gBACxGH,EAAOQ,YAAYH,EAASpG,QAAQwG,aAGhCC,EAAW,SAAAV,GAGCA,EAAOW,cAAc,aAEnCX,EAAOY,YAAYZ,EAAOW,cAAc,cAuCtCE,EAAY,SAAAlB,GAGhB,GAAc,QAAVA,EAAEmB,IAAe,CACnB,IAAMd,EAASL,EAAEK,OAEjB,GAAIA,EAAOe,iBAAmBf,EAAOgB,aAAc,CACjDrB,EAAEsB,iBAOF,IALA,IAAIC,EAAQlB,EAAOe,eACfI,EAAMnB,EAAOgB,aAEXvD,EAAOuC,EAAOpC,MAEbsD,EAAQ,GAAyB,OAApBzD,EAAKyD,EAAQ,IAC/BA,IAEF,KAAOC,EAAM,GAAuB,OAAlB1D,EAAK0D,EAAM,IAAeA,EAAM1D,EAAK2D,QACrDD,IAKF,IAFA,IAAIE,EAAQ5D,EAAK6D,OAAOJ,EAAOC,EAAMD,GAAOhC,MAAM,MAEzCqC,EAAI,EAAGA,EAAIF,EAAMD,OAAQG,IAG5BA,IAAMF,EAAMD,OAAS,GAAyB,IAApBC,EAAME,GAAGH,SAKnCzB,EAAE6B,SACJH,EAAME,GAAKF,EAAME,GAAG3F,QAAQlC,EAAU,IAEtC2H,EAAME,GAAE3H,OAAAA,OAAUyH,EAAME,KAG5BF,EAAQA,EAAMI,KAAK,MAGnBzB,EAAOpC,MAAQH,EAAK6D,OAAO,EAAGJ,GAASG,EAAQ5D,EAAK6D,OAAOH,GAC3DnB,EAAOe,eAAiBG,EACxBlB,EAAOgB,aAAeE,EAAQG,EAAMD,MACtC,CACF,GAGIM,EAAI,WAAA,IAAAC,EAAAzF,EAAAC,IAAAC,MAAG,SAAAwF,EAAMC,GAAK,IAAAC,EAAAC,EAAAC,SAAA7F,IAAAK,MAAA,SAAAyF,GAAA,OAAA,OAAAA,EAAAvF,KAAAuF,EAAAtH,MAAA,KAAA,EAGhBmH,EAAWzH,SAASsC,eAAe,qBACnCoF,EAAc1H,SAAS6H,iBAAiB,eAE1CJ,GAAYD,KACRG,EAAgB3H,SAASsC,eAAe,qBAEhCwF,iBAAiB,SAAS,SAAAxC,GAGtC/E,EAAW+E,EAAEK,OACf,IAEAgC,EAAcG,iBAAiB,UAAWtB,GAEzBxG,SAASsC,eAAe,mBAChCwF,iBAAiB,SAAS,WACjC9I,EAAS2I,EAAcI,IAAMJ,EAAcpE,MAC3CvD,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UAC9DjI,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UAC9D1H,EAAWP,SAASsC,eAAe,oBACrC,IAEAtC,SAASsC,eAAe,sBAAsBwF,iBAAiB,QAAO,WAAA,IAAAI,EAAArG,EAAAC,IAAAC,MAAE,SAAAoG,EAAM7C,GAAC,IAAAjE,SAAAS,IAAAK,MAAA,SAAAiG,GAAA,OAAA,OAAAA,EAAA/F,KAAA+F,EAAA9H,MAAA,KAAA,EAEjE,QADRe,EAAMgH,OAAO,iDAAkDtJ,MAEjEsC,EAAMD,EAAUC,GAChBiE,EAAEsB,iBACF0B,QAAQC,UAAU,CAACd,SAAUpG,GAAM,GAAE,IAAA9B,OAAM,IAAIiJ,gBAAejJ,YAAAA,OAAa8B,IAAOK,aAClF+G,GAAK,IACN,KAAA,EAAA,IAAA,MAAA,OAAAL,EAAAxF,OAAA,GAAAuF,EACF,KAAA,OAAA,SAAAO,GAAA,OAAAR,EAAApF,MAAAC,KAAAC,WAAC,CARoE,IAUtEhD,SAASsC,eAAe,sBAAsBwF,iBAAiB,QAAO,WAAA,IAAAa,EAAA9G,EAAAC,IAAAC,MAAE,SAAA6G,EAAMtD,GAAC,IAAAjE,SAAAS,IAAAK,MAAA,SAAA0G,GAAA,OAAA,OAAAA,EAAAxG,KAAAwG,EAAAvI,MAAA,KAAA,EAEjE,QADRe,EAAMgH,OAAO,mDAAoDtJ,MAEnEsC,EAAMD,EAAUC,GAChBiE,EAAEsB,iBACF0B,QAAQC,UAAU,CAACO,OAAQzH,GAAM,GAAE,IAAA9B,OAAM,IAAIiJ,gBAAejJ,UAAAA,OAAW8B,IAAOK,aAC9E+G,GAAK,IACN,KAAA,EAAA,IAAA,MAAA,OAAAI,EAAAjG,OAAA,GAAAgG,EACF,KAAA,OAAA,SAAAG,GAAA,OAAAJ,EAAA7F,MAAAC,KAAAC,WAAC,CARoE,IAUtEhD,SAASsC,eAAe,oBAAoBiB,MAAQzE,EACpDkB,SAASsC,eAAe,qBAAqBwF,iBAAiB,SAAS,WACrEH,EAAcpE,MAAQvE,EAAS2I,EAAcI,WACtC/I,EAAS2I,EAAcI,IAC9B/H,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UAC9DjI,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,SAChE,IAEAjI,SAASsC,eAAe,qBAAqBwF,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAAiH,IAAA,IAAAC,EAAAC,SAAApH,IAAAK,MAAA,SAAAgH,GAAA,OAAA,OAAAA,EAAA9G,KAAA8G,EAAA7I,MAAA,KAAA,EAQxD,OAPP2I,EAASjJ,SAASsC,eAAe,qBACvCxD,EAAMkB,SAASsC,eAAe,oBAAoBiB,MAClD0F,EAAOjB,UAAUC,OAAO,UACxBjI,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UACxDiB,EAAUlJ,SAASsG,cAAc,WACvCZ,EAASwD,EAAS,uBAClBD,EAAO/C,UAAY,GACnBlH,EAAW,CAAA,EAAEmK,EAAA7I,KAAA,GACPsD,GAAa,GAAK,KAAA,GAAA,OAAAuF,EAAA7I,KAAA,GAClB2C,EAASnE,GAAI,KAAA,GAAA,OAAAqK,EAAA7I,KAAA,GACb+G,IAAM,KAAA,GACZhB,EAAS6C,GACTnJ,IAAsB,KAAA,GAAA,IAAA,MAAA,OAAAoJ,EAAAvG,OAAA,GAAAoG,EACvB,OAGHtB,EAAY0B,SAAQ,SAAAC,GAElB,IAAMpH,EAAYoH,EAAGtB,GAAGxG,QAAQtC,EAAS,MACnCqK,EAAStJ,SAASsC,sCAAc/C,OAAwB0C,IACxDsH,EAAUvJ,SAASsC,uCAAc/C,OAAyB0C,IAC1DuH,EAASxJ,SAASsC,oCAAc/C,OAAsB0C,IACtDwH,EAAazJ,SAASsG,0CAAa/G,OAA6B0C,IAChEyH,EAAc1J,SAASsG,2CAAa/G,OAA8B0C,IAClE0H,EAAY3J,SAASsG,yCAAa/G,OAA4B0C,IAC9D2H,EAAe5J,SAASsG,4CAAa/G,OAA+B0C,IAE1EqH,EAAOxB,iBAAiB,SAAS,WAG/BvH,EAAW+I,EACb,IAEAA,EAAOxB,iBAAiB,UAAWtB,GAEnC8C,EAAOxB,iBAAiB,aAAa,SAAAxC,GAGnCA,EAAEuE,iBACJ,IAEAN,EAAQzB,iBAAiB,SAAS,SAAAxC,GAGhC,IAAMwE,EAAKxE,EAAEK,OACb,GAAImE,EAAGtL,QAAQ,qBAAsB,CACnC,IAAIoB,EAAU,GACRmK,EAASD,EAAGjJ,WAClB,IAAKkJ,EAAOvL,QAAQ,wBAAyB,CAG3C,GADAoB,GADAA,EAAUmK,EAAOC,aAAa,SAASzI,QAAQ,oBAAqB,KAClDA,QAAQ,KAAM,IAC5BjD,OAAO2L,eAAiB3L,OAAO2L,cAAcC,QAE/C,OAAO5L,OAAO2L,cAAcC,QAAQ,OAAQtK,GACvC,GAAII,SAASmK,uBAAyBnK,SAASmK,sBAAsB,QAAS,CACnF,IAAMC,EAAWpK,SAASiG,cAAc,YACxCmE,EAASC,YAAczK,EACvBwK,EAASpJ,MAAMsJ,SAAW,QAC1BtK,SAASY,KAAKuF,YAAYiE,GAC1BA,EAASG,SACT,IACE,OAAOvK,SAASwK,YAAY,OAC5B,CAAA,MAAOC,GACP,OAAOpC,OAAO,mCAAoCzI,EACpD,CAAU,QACRI,SAASY,KAAK2F,YAAY6D,EAC5B,CACF,CACF,CACF,CACF,IAEAX,EAAW3B,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAA2I,WAAA5I,IAAAK,MAAA,SAAAwI,GAAA,OAAA,OAAAA,EAAAtI,KAAAsI,EAAArK,MAAA,KAAA,EAGnCtB,EAASiD,GAAaqH,EAAO/F,MAC7BiG,EAAOxB,UAAUC,OAAO,UACxBsB,EAAQvB,UAAUC,OAAO,UACzB0B,EAAU3B,UAAUC,OAAO,UAC3B2B,EAAa5B,UAAUC,OAAO,UAC9BwB,EAAWzB,UAAUC,OAAO,UAC5ByB,EAAY1B,UAAUC,OAAO,UAC7B1H,EAAW+I,GACXA,EAAOsB,QAAO,KAAA,EAAA,IAAA,MAAA,OAAAD,EAAA/H,OAAA,GAAA8H,EACf,MAEDhB,EAAY5B,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAA8I,IAAA,IAAA7G,EAAA3C,EAAAyJ,EAAAlG,EAAAmG,SAAAjJ,IAAAK,MAAA,SAAA6I,GAAA,OAAA,OAAAA,EAAA3I,KAAA2I,EAAA1K,MAAA,KAAA,EAG9B0D,EAAO1F,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAC3CxD,EAAMD,EAAUkI,EAAO/F,OACvBuH,EAAMxM,OAAOkF,SACfoB,EAAW,eACXkG,EAAIlG,SAASqG,WAAU1L,IAAAA,OAAKyE,EAAO,QACrCY,EAAQrF,IAAAA,OAAOyE,EAAkB,kBAE7B+G,KAAIxL,OAAMuL,EAAII,eAAQ3L,OAAKuL,EAAIK,MAAI5L,OAAGqF,EAAQrF,UAAAA,OAAS8B,IACpD0F,OAAS,IAChBqE,MACE,gFAGFC,UAAUC,UAAUC,UAAUR,GAAMS,KAAI3J,EAAAC,IAAAC,MAAC,SAAA0J,WAAA3J,IAAAK,MAAA,SAAAuJ,GAAA,OAAA,OAAAA,EAAArJ,KAAAqJ,EAAApL,MAAA,KAAA,EACvC8K,MAAM,+BAA+B,KAAA,EAAA,IAAA,MAAA,OAAAM,EAAA9I,OAAA,GAAA6I,EAAA,KACtC5J,EAAAC,IAAAC,MAAE,SAAA4J,WAAA7J,IAAAK,MAAA,SAAAyJ,GAAA,OAAA,OAAAA,EAAAvJ,KAAAuJ,EAAAtL,MAAA,KAAA,EACD8K,MAAM,kCAAkC,KAAA,EAAA,IAAA,MAAA,OAAAQ,EAAAhJ,OAAA,GAAA+I,EACzC,MACF,KAAA,EAAA,IAAA,MAAA,OAAAX,EAAApI,OAAA,GAAAiI,EACF,MAEDlB,EAAU7B,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAA8J,IAAA,IAAAC,EAAAC,SAAAjK,IAAAK,MAAA,SAAA6J,GAAA,OAAA,OAAAA,EAAA3J,KAAA2J,EAAA1L,MAAA,KAAA,EAAA,IAG9BzB,EAAI,CAAAmN,EAAA1L,KAAA,EAAA,KAAA,CAAA,OAAA0L,EAAAC,OAAA,UAAA,KAAA,EAaP,OATDpN,GAAO,EAEDiN,EAAOtC,EAAOlD,cAAc,QAClCZ,EAASoG,EAAM,MAAM,IACfC,EAAU/L,SAAS6H,iBAAiB,iCAExCkE,EAAQ3C,SAAQ,SAAA8C,GACdA,EAAE3J,aAAa,WAAY,GAC7B,IACDyJ,EAAA1L,KAAA,EACKsD,GAAa,GAAM,KAAA,EACmB,OAA5C2F,EAAQjD,cAAc,QAAQJ,UAAY,GAAE8F,EAAA1L,KAAA,GACtCqB,EAAUM,GAAU,KAAA,GACtB8J,GACFA,EAAQ3C,SAAQ,SAAA8C,GACdA,EAAEvJ,gBAAgB,WACpB,IAEF0D,EAASyF,GACTtC,EAAOxB,UAAUC,OAAO,UACxBsB,EAAQvB,UAAUC,OAAO,UACzBwB,EAAWzB,UAAUC,OAAO,UAC5ByB,EAAY1B,UAAUC,OAAO,UAC7B0B,EAAU3B,UAAUC,OAAO,UAC3B2B,EAAa5B,UAAUC,OAAO,iBAEvBjJ,EAASiD,GAChBpD,GAAO,EAAK,KAAA,GAAA,IAAA,MAAA,OAAAmN,EAAApJ,OAAA,GAAAiJ,EACb,MAEDjC,EAAa9B,iBAAiB,SAAS,WAGrCwB,EAAO/F,MAAQvE,EAASiD,UACjBjD,EAASiD,GAChBuH,EAAOxB,UAAUC,OAAO,UACxBsB,EAAQvB,UAAUC,OAAO,UACzBwB,EAAWzB,UAAUC,OAAO,UAC5ByB,EAAY1B,UAAUC,OAAO,UAC7B0B,EAAU3B,UAAUC,OAAO,UAC3B2B,EAAa5B,UAAUC,OAAO,SAChC,GACF,IAAE,KAAA,EAAA,IAAA,MAAA,OAAAL,EAAAhF,OAAA,GAAA2E,EACH,KAAA,OAtNKF,SAAI8E,GAAA,OAAA7E,EAAAxE,MAAAC,KAAAC,WAAA,CAAA,GAwNJyF,EAAI,WAAA,IAAA2D,EAAAvK,EAAAC,IAAAC,MAAG,SAAAsK,EAAM7E,EAAO8E,GAAM,IAAAC,EAAAC,EAAAC,EAAApL,EAAA6H,EAAAwD,EAAAnJ,EAAAoJ,EAAA/M,SAAAkC,IAAAK,MAAA,SAAAyK,GAAA,OAAA,OAAAA,EAAAvK,KAAAuK,EAAAtM,MAAA,KAAA,EAKjB,GAAbtB,EAAW,CAAA,GAEPV,OAAOkF,SAASoB,SAASO,SAAS,gBAAe,CAAAyH,EAAAtM,KAAA,GAAA,KAAA,CAKF,GAJ3CiM,EAASD,GAAU,IAAI9D,gBAAgBlK,OAAOkF,SAAS8I,QACvDE,EAAU,qBACVC,EAAU,sBACVpL,EAAMkL,EAAOM,IAAI,UAAYN,EAAOO,IAAI,UAAYP,EAAOO,IAAI,YAC/D5D,EAAUlJ,SAASsG,cAAc,WAC3B,OAARjF,IAAgBA,EAAI0L,OAAM,CAAAH,EAAAtM,KAAA,GAAA,KAAA,CAEF,OAA1BoF,EAASwD,EAASsD,GAAQI,EAAAtM,KAAA,GACpBsD,GAAa,GAAK,KAAA,GACxByC,EAAS6C,GACTxD,EAASwD,EAASuD,GAClB,IACQC,EAAWH,EAAOM,IAAI,UAAY,SAAW,WACnD1N,EAAa6N,mBAAmBT,EAAO7K,YACnC6B,EAAQ,GACNoJ,EAAM,IAAIM,eAChBlO,EAAOsC,EACPsL,EAAIO,KAAK,MAAO7L,GAAK,GACrBsL,EAAIQ,OAAMtL,EAAAC,IAAAC,MAAG,SAAAqL,WAAAtL,IAAAK,MAAA,SAAAkL,GAAA,OAAA,OAAAA,EAAAhL,KAAAgL,EAAA/M,MAAA,KAAA,EAUV,OARsB,IAAnBqM,EAAIW,YACa,MAAfX,EAAIY,SACNhK,EAAQoJ,EAAIa,cAIC,WAAbd,IACFnJ,EAAQ5D,EAAW4D,IACpB8J,EAAA/M,KAAA,EACK2C,EAASM,GAAM,KAAA,EAAA,OAAA8J,EAAA/M,KAAA,EACf+G,EAAKG,GAAM,KAAA,EACjBnB,EAAS6C,GACTnJ,IAAsB,KAAA,EAAA,IAAA,MAAA,OAAAsN,EAAAzK,OAAA,GAAAwK,EACvB,KACDT,EAAIc,MACN,CAAE,MAAOpI,GAAQ,CAAAuH,EAAAtM,KAAA,GAAA,MAAA,KAAA,GAKS,OAH1BvB,EAAO,GACDa,EAAUD,EAAW4M,EAAOM,IAAI,QAAUN,EAAOO,IAAI,QAAUtN,GACrEL,EAAa6N,mBAAmBT,EAAO7K,YACvCgE,EAASwD,EAASsD,GAAQI,EAAAtM,KAAA,GACpBsD,GAAa,GAAK,KAAA,GAEE,OAD1ByC,EAAS6C,GACTxD,EAASwD,EAASuD,GAAQG,EAAAtM,KAAA,GACpB2C,EAASrD,GAAQ,KAAA,GAAA,OAAAgN,EAAAtM,KAAA,GACjB+G,EAAKG,GAAM,KAAA,GACjBnB,EAAS6C,GACTnJ,IAAsB,KAAA,GAAA6M,EAAAtM,KAAA,GAAA,MAAA,KAAA,GAGxBvB,EAAO,GACPI,EAAa,GACbkI,EAAKG,GAAM,KAAA,GAAA,IAAA,MAAA,OAAAoF,EAAAhK,OAAA,GAAAyJ,EAEd,KAAA,OAAA,SA9DSqB,EAAAC,GAAA,OAAAvB,EAAAtJ,MAAAC,KAAAC,WAAA,CAAA,GAiEVhD,SAAS8H,iBAAiB,SA7VE,SAAAxC,GAG1B,IAAMtB,EAAO1F,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAC3Cc,EAASL,EAAEK,QAAUL,EAAEsI,WAE7B,GAAuB,MAAnBjI,EAAOkI,SAAmBpF,GAE1B9C,EAAOqE,aAAa,SACpBrE,EAAOwF,OAAS7M,OAAOkF,SAAS2H,MAChC7M,OAAOkF,SAASoB,WAAQ,IAAArF,OAASyE,mBACjC1F,OAAOkF,SAASoB,WAAae,EAAOf,UACpCtG,OAAOkF,SAAS8I,SAAW3G,EAAO2G,OAClC,CACAhH,EAAEsB,iBACF,IAAM0F,EAAS,IAAI9D,gBAAgB7C,EAAO2G,QAC1C7D,GAAK,EAAO6D,EACd,KA4UoD,GAGxDhO,OAAOwJ,iBAAiB,YA9WP,WAGf,IAAM9D,EAAO1F,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAE/CvG,OAAOkF,SAASoB,eAAQrF,OAASyE,EAAI,kBAErBgJ,mBAAmB,IAAIxE,gBAAgBlK,OAAOkF,SAAS8I,QAAQ5K,cAC/DvC,GACdsJ,GAAK,OAwWXnK,OAAOwJ,iBAAiB,UAAU,WAChC1I,GAAO,CACT,IAGAd,OAAO+B,UAAUyN,WAAU,WAGrB1O,EACFA,GAAO,EAGTqJ,GAAK,EACP,GACD,CAxiBD"} \ No newline at end of file +{"version":3,"file":"extra-notebook-61fde54c.js","sources":["extra-notebook.js"],"sourcesContent":["(() => {\n let webspace = ''\n try {\n let gamut = 'srgb'\n if (window.matchMedia(\"(color-gamut: rec2020)\").matches) {\n gamut = 'rec2020'\n } else if (window.matchMedia(\"(color-gamut: p3)\").matches) {\n gamut = 'display-p3'\n }\n webspace = (CSS.supports('color: color(display-p3 1 0 0)')) ? gamut : 'srgb'\n } catch {\n webspace = 'srgb'\n }\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\naction = globals().get('action')\nif action == 'notebook':\n callback = render_notebook\nelse:\n callback = render_console\n\ncallback(gamut='${webspace}')\n`\n\n const defContent = window.colorNotebook.defaultPlayground\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\n\\`\\`\\`\\`\\`\\`\\`\\`py play\n${content}\n\\`\\`\\`\\`\\`\\`\\`\\`\n`\n }\n\n let notebookInstalled = false\n let playgroundInstalled = false\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 window.document$.next()\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 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 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 full => {\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.23.4/full/\",\n fullStdLib: false\n })\n }\n\n if ((!notebookInstalled && full) || (!playgroundInstalled && !full)) {\n const base = `${window.location.origin}/${window.location.pathname.split('/')[1]}/playground/`\n const packages = (full) ? window.colorNotebook.notebookWheels : window.colorNotebook.playgroundWheels\n const installs = []\n if (full) {\n notebookInstalled = true\n } else {\n playgroundInstalled = true\n }\n for (const s of packages) {\n if (s.endsWith('.whl')) {\n installs.push(base + s)\n } else {\n installs.push(s)\n }\n }\n await pyodide.loadPackage(installs)\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 = `
${loaderLabel}
`\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\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 main(false, search) // 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\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(true)\n await pyrender(raw)\n await init()\n hideBusy(article)\n fakeDOMContentLoaded()\n })\n }\n\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 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 (path.length > 2000) {\n alert( // eslint-disable-line no-alert\n \"Code must be small enough to generate a shareable URL under 2000 characters!\"\n )\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(false)\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, search) => {\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 = search || 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(true)\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(true)\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, true)\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 = false\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 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":["webspace","gamut","window","matchMedia","matches","CSS","supports","_unused","pyodide","busy","raw","gist","editTemp","reIdNum","initialized","lastSearch","fake","tabStart","pycode","concat","defContent","colorNotebook","defaultPlayground","getContent","content","notebookInstalled","playgroundInstalled","fakeDOMContentLoaded","document","dispatchEvent","Event","bubbles","cancelable","document$","next","textResize","inpt","scrollLeft","pageXOffset","documentElement","body","parentNode","scrollTop","pageYOffset","style","height","scrollHeight","scrollTo","encodeuri","uri","encodeURIComponent","replace","c","charCodeAt","toString","pyexecute","_ref","_asyncToGenerator","_regeneratorRuntime","mark","_callee","currentID","currentInputs","wrap","_context","prev","getElementById","setAttribute","globals","set","runPythonAsync","removeAttribute","stop","_x","apply","this","arguments","pyrender","_ref2","_callee2","text","src","_context2","value","location","hash","href","_x2","setupPyodide","_ref3","_callee3","full","base","packages","installs","_iterator","_step","s","_context3","loadPyodide","indexURL","fullStdLib","sent","origin","pathname","split","notebookWheels","playgroundWheels","_createForOfIteratorHelper","n","done","endsWith","push","err","e","f","loadPackage","_x3","showBusy","target","label","relative","loaderLabel","classes","template","createElement","innerHTML","appendChild","firstChild","hideBusy","querySelector","removeChild","handleTab","key","selectionStart","selectionEnd","preventDefault","start","end","length","lines","substr","i","shiftKey","join","init","_ref4","_callee12","first","notebook","playgrounds","notebookInput","_context12","querySelectorAll","addEventListener","id","classList","toggle","_ref5","_callee4","_context4","prompt","history","pushState","URLSearchParams","main","_x5","_ref6","_callee5","_context5","source","_x6","_callee6","render","article","_context6","forEach","pg","inputs","results","pgcode","buttonEdit","buttonShare","buttonRun","buttonCancel","stopPropagation","el","parent","getAttribute","clipboardData","setData","queryCommandSupported","textarea","textContent","position","select","execCommand","ex","_callee7","_context7","focus","_callee10","loc","path","_context10","startsWith","protocol","host","alert","navigator","clipboard","writeText","then","_callee8","_context8","_callee9","_context9","_callee11","form","buttons","_context11","abrupt","b","_x4","_ref13","_callee14","search","params","loadMsg","pageMsg","gistType","xhr","_context14","has","get","trim","decodeURIComponent","XMLHttpRequest","open","onload","_callee13","_context13","readyState","status","responseText","send","_x7","_x8","srcElement","tagName","subscribe"],"mappings":"urdAAA,WACE,IAAIA,EAAW,GACf,IACE,IAAIC,EAAQ,OACRC,OAAOC,WAAW,0BAA0BC,QAC9CH,EAAQ,UACCC,OAAOC,WAAW,qBAAqBC,UAChDH,EAAQ,cAEVD,EAAYK,IAAIC,SAAS,kCAAqCL,EAAQ,MACxE,CAAE,MAAAM,GACAP,EAAW,MACb,CACA,IAAIQ,EAAU,KACVC,GAAO,EACPC,EAAM,GACNC,EAAO,GACPC,EAAW,CAAA,EACTC,EAAU,aACZC,GAAc,EACdC,EAAa,GACbC,GAAO,EACLC,EAAW,eAKXC,EAAM,w8tBAAAC,OASInB,EACjB,QAEOoB,EAAalB,OAAOmB,cAAcC,kBAElCC,EAAa,SAAAC,GACjB,MAAAL,gSAAAA,OAQFK,EAAO,iBAKHC,GAAoB,EACpBC,GAAsB,EAEpBC,EAAuB,WAE3BX,GAAO,EACPd,OAAO0B,SAASC,cAAc,IAAIC,MAAM,mBAAoB,CAC1DC,SAAS,EACTC,YAAY,KAEd9B,OAAO+B,UAAUC,QAGbC,EAAa,SAAAC,GAGjB,IAAMC,EAAanC,OAAOoC,cACvBV,SAASW,iBAAmBX,SAASY,KAAKC,YAAcb,SAASY,MAAMH,WAEpEK,EAAaxC,OAAOyC,cACvBf,SAASW,iBAAmBX,SAASY,KAAKC,YAAcb,SAASY,MAAME,UAE1EN,EAAKQ,MAAMC,OAAS,MACpBT,EAAKQ,MAAMC,OAAM,GAAA1B,OAAMiB,EAAKU,aAAgB,MAE5C5C,OAAO6C,SAASV,EAAYK,IAGxBM,EAAY,SAAAC,GAGhB,OAAOC,mBAAmBD,GAAKE,QAAQ,aAAa,SAAAC,GAClD,MAAA,IAAAjC,OAAWiC,EAAEC,WAAW,GAAGC,SAAS,IACtC,KAGIC,EAAS,WAAA,IAAAC,EAAAC,EAAAC,IAAAC,MAAG,SAAAC,EAAMC,GAAS,IAAAC,SAAAJ,IAAAK,MAAA,SAAAC,GAAA,OAAA,OAAAA,EAAAC,KAAAD,EAAA9B,MAAA,KAAA,EAMY,OAHrC4B,EAAgBlC,SAASsC,sCAAc/C,OAAwB0C,KACvDM,aAAa,WAAY,IACvC3D,EAAQ4D,QAAQC,IAAI,SAAUR,GAC9BrD,EAAQ4D,QAAQC,IAAI,SAAU,cAAaL,EAAA9B,KAAA,EACrC1B,EAAQ8D,eAAepD,GAAO,KAAA,EACpC4C,EAAcS,gBAAgB,YAAW,KAAA,EAAA,IAAA,MAAA,OAAAP,EAAAQ,OAAA,GAAAZ,EAC1C,KAAA,OATKL,SAASkB,GAAA,OAAAjB,EAAAkB,MAAAC,KAAAC,WAAA,CAAA,GAWTC,EAAQ,WAAA,IAAAC,EAAArB,EAAAC,IAAAC,MAAG,SAAAoB,EAAMC,GAAI,IAAAC,SAAAvB,IAAAK,MAAA,SAAAmB,GAAA,OAAA,OAAAA,EAAAjB,KAAAiB,EAAAhD,MAAA,KAAA,EAIgB,OADzC1B,EAAQ4D,QAAQC,IAAI,UAAWW,GAC/BxE,EAAQ4D,QAAQC,IAAI,SAAU,YAAWa,EAAAhD,KAAA,EACnC1B,EAAQ8D,eAAepD,GAAO,KAAA,GAC9B+D,EAAMrD,SAASsC,eAAe,uBAElCxD,EAAMsE,EACNC,EAAIE,MAAQH,GAEV9E,OAAOkF,SAASC,OAElBnF,OAAOkF,SAASE,KAAOpF,OAAOkF,SAASE,MACxC,KAAA,EAAA,IAAA,MAAA,OAAAJ,EAAAV,OAAA,GAAAO,EACF,KAAA,OAfKF,SAAQU,GAAA,OAAAT,EAAAJ,MAAAC,KAAAC,WAAA,CAAA,GAiBRY,EAAY,WAAA,IAAAC,EAAAhC,EAAAC,IAAAC,MAAG,SAAA+B,EAAMC,GAAI,IAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,SAAAvC,IAAAK,MAAA,SAAAmC,GAAA,OAAA,OAAAA,EAAAjC,KAAAiC,EAAAhE,MAAA,KAAA,EAAA,GAGxBpB,EAAW,CAAAoF,EAAAhE,KAAA,EAAA,KAAA,CACI,OAAlBpB,GAAc,EAAIoF,EAAAhE,KAAA,EACFiE,YAAY,CAC1BC,SAAU,iDACVC,YAAY,IACZ,KAAA,EAHF7F,EAAO0F,EAAAI,KAAA,KAAA,EAAA,IAMH7E,IAAqBkE,KAAWjE,GAAwBiE,GAAK,CAAAO,EAAAhE,KAAA,GAAA,KAAA,CAC3D0D,EAAI,GAAAzE,OAAMjB,OAAOkF,SAASmB,OAAM,KAAApF,OAAIjB,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAAE,gBAC1EZ,EAAYF,EAAQzF,OAAOmB,cAAcqF,eAAiBxG,OAAOmB,cAAcsF,iBAC/Eb,EAAW,GACbH,EACFlE,GAAoB,EAEpBC,GAAsB,EACvBqE,EAAAa,EACef,GAAQ,IAAxB,IAAAE,EAAAE,MAAAD,EAAAD,EAAAc,KAAAC,OAAWb,EAACD,EAAAb,OACJ4B,SAAS,QACbjB,EAASkB,KAAKpB,EAAOK,GAErBH,EAASkB,KAAKf,EAEjB,CAAA,MAAAgB,GAAAlB,EAAAmB,EAAAD,EAAA,CAAA,QAAAlB,EAAAoB,GAAA,CAAA,OAAAjB,EAAAhE,KAAA,GACK1B,EAAQ4G,YAAYtB,GAAS,KAAA,GAAA,IAAA,MAAA,OAAAI,EAAA1B,OAAA,GAAAkB,EAEtC,KAAA,OA7BKF,SAAY6B,GAAA,OAAA5B,EAAAf,MAAAC,KAAAC,WAAA,CAAA,GA+BZ0C,EAAW,SAACC,EAAQC,EAAOC,GAG/B,IAAMC,EAAe,MAAOF,EAA2C,aAAeA,EAChFG,EAAUF,EAAW,mBAAqB,UAC1CG,EAAWhG,SAASiG,cAAc,YACxCD,EAASE,UAAS,eAAA3G,OAAkBwG,EAAOxG,qCAAAA,OAAoCuG,EAAyB,gBACxGH,EAAOQ,YAAYH,EAASpG,QAAQwG,aAGhCC,EAAW,SAAAV,GAGCA,EAAOW,cAAc,aAEnCX,EAAOY,YAAYZ,EAAOW,cAAc,cAuCtCE,EAAY,SAAAlB,GAGhB,GAAc,QAAVA,EAAEmB,IAAe,CACnB,IAAMd,EAASL,EAAEK,OAEjB,GAAIA,EAAOe,iBAAmBf,EAAOgB,aAAc,CACjDrB,EAAEsB,iBAOF,IALA,IAAIC,EAAQlB,EAAOe,eACfI,EAAMnB,EAAOgB,aAEXvD,EAAOuC,EAAOpC,MAEbsD,EAAQ,GAAyB,OAApBzD,EAAKyD,EAAQ,IAC/BA,IAEF,KAAOC,EAAM,GAAuB,OAAlB1D,EAAK0D,EAAM,IAAeA,EAAM1D,EAAK2D,QACrDD,IAKF,IAFA,IAAIE,EAAQ5D,EAAK6D,OAAOJ,EAAOC,EAAMD,GAAOhC,MAAM,MAEzCqC,EAAI,EAAGA,EAAIF,EAAMD,OAAQG,IAG5BA,IAAMF,EAAMD,OAAS,GAAyB,IAApBC,EAAME,GAAGH,SAKnCzB,EAAE6B,SACJH,EAAME,GAAKF,EAAME,GAAG3F,QAAQlC,EAAU,IAEtC2H,EAAME,GAAE3H,OAAAA,OAAUyH,EAAME,KAG5BF,EAAQA,EAAMI,KAAK,MAGnBzB,EAAOpC,MAAQH,EAAK6D,OAAO,EAAGJ,GAASG,EAAQ5D,EAAK6D,OAAOH,GAC3DnB,EAAOe,eAAiBG,EACxBlB,EAAOgB,aAAeE,EAAQG,EAAMD,MACtC,CACF,GAGIM,EAAI,WAAA,IAAAC,EAAAzF,EAAAC,IAAAC,MAAG,SAAAwF,EAAMC,GAAK,IAAAC,EAAAC,EAAAC,SAAA7F,IAAAK,MAAA,SAAAyF,GAAA,OAAA,OAAAA,EAAAvF,KAAAuF,EAAAtH,MAAA,KAAA,EAGhBmH,EAAWzH,SAASsC,eAAe,qBACnCoF,EAAc1H,SAAS6H,iBAAiB,eAE1CJ,GAAYD,KACRG,EAAgB3H,SAASsC,eAAe,qBAEhCwF,iBAAiB,SAAS,SAAAxC,GAGtC/E,EAAW+E,EAAEK,OACf,IAEAgC,EAAcG,iBAAiB,UAAWtB,GAEzBxG,SAASsC,eAAe,mBAChCwF,iBAAiB,SAAS,WACjC9I,EAAS2I,EAAcI,IAAMJ,EAAcpE,MAC3CvD,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UAC9DjI,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UAC9D1H,EAAWP,SAASsC,eAAe,oBACrC,IAEAtC,SAASsC,eAAe,sBAAsBwF,iBAAiB,QAAO,WAAA,IAAAI,EAAArG,EAAAC,IAAAC,MAAE,SAAAoG,EAAM7C,GAAC,IAAAjE,SAAAS,IAAAK,MAAA,SAAAiG,GAAA,OAAA,OAAAA,EAAA/F,KAAA+F,EAAA9H,MAAA,KAAA,EAEjE,QADRe,EAAMgH,OAAO,iDAAkDtJ,MAEjEsC,EAAMD,EAAUC,GAChBiE,EAAEsB,iBACF0B,QAAQC,UAAU,CAACd,SAAUpG,GAAM,GAAE,IAAA9B,OAAM,IAAIiJ,gBAAejJ,YAAAA,OAAa8B,IAAOK,aAClF+G,GAAK,IACN,KAAA,EAAA,IAAA,MAAA,OAAAL,EAAAxF,OAAA,GAAAuF,EACF,KAAA,OAAA,SAAAO,GAAA,OAAAR,EAAApF,MAAAC,KAAAC,WAAC,CARoE,IAUtEhD,SAASsC,eAAe,sBAAsBwF,iBAAiB,QAAO,WAAA,IAAAa,EAAA9G,EAAAC,IAAAC,MAAE,SAAA6G,EAAMtD,GAAC,IAAAjE,SAAAS,IAAAK,MAAA,SAAA0G,GAAA,OAAA,OAAAA,EAAAxG,KAAAwG,EAAAvI,MAAA,KAAA,EAEjE,QADRe,EAAMgH,OAAO,mDAAoDtJ,MAEnEsC,EAAMD,EAAUC,GAChBiE,EAAEsB,iBACF0B,QAAQC,UAAU,CAACO,OAAQzH,GAAM,GAAE,IAAA9B,OAAM,IAAIiJ,gBAAejJ,UAAAA,OAAW8B,IAAOK,aAC9E+G,GAAK,IACN,KAAA,EAAA,IAAA,MAAA,OAAAI,EAAAjG,OAAA,GAAAgG,EACF,KAAA,OAAA,SAAAG,GAAA,OAAAJ,EAAA7F,MAAAC,KAAAC,WAAC,CARoE,IAUtEhD,SAASsC,eAAe,oBAAoBiB,MAAQzE,EACpDkB,SAASsC,eAAe,qBAAqBwF,iBAAiB,SAAS,WACrEH,EAAcpE,MAAQvE,EAAS2I,EAAcI,WACtC/I,EAAS2I,EAAcI,IAC9B/H,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UAC9DjI,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,SAChE,IAEAjI,SAASsC,eAAe,qBAAqBwF,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAAiH,IAAA,IAAAC,EAAAC,SAAApH,IAAAK,MAAA,SAAAgH,GAAA,OAAA,OAAAA,EAAA9G,KAAA8G,EAAA7I,MAAA,KAAA,EAQxD,OAPP2I,EAASjJ,SAASsC,eAAe,qBACvCxD,EAAMkB,SAASsC,eAAe,oBAAoBiB,MAClD0F,EAAOjB,UAAUC,OAAO,UACxBjI,SAASsC,eAAe,qBAAqB0F,UAAUC,OAAO,UACxDiB,EAAUlJ,SAASsG,cAAc,WACvCZ,EAASwD,EAAS,uBAClBD,EAAO/C,UAAY,GACnBlH,EAAW,CAAA,EAAEmK,EAAA7I,KAAA,GACPsD,GAAa,GAAK,KAAA,GAAA,OAAAuF,EAAA7I,KAAA,GAClB2C,EAASnE,GAAI,KAAA,GAAA,OAAAqK,EAAA7I,KAAA,GACb+G,IAAM,KAAA,GACZhB,EAAS6C,GACTnJ,IAAsB,KAAA,GAAA,IAAA,MAAA,OAAAoJ,EAAAvG,OAAA,GAAAoG,EACvB,OAGHtB,EAAY0B,SAAQ,SAAAC,GAElB,IAAMpH,EAAYoH,EAAGtB,GAAGxG,QAAQtC,EAAS,MACnCqK,EAAStJ,SAASsC,sCAAc/C,OAAwB0C,IACxDsH,EAAUvJ,SAASsC,uCAAc/C,OAAyB0C,IAC1DuH,EAASxJ,SAASsC,oCAAc/C,OAAsB0C,IACtDwH,EAAazJ,SAASsG,0CAAa/G,OAA6B0C,IAChEyH,EAAc1J,SAASsG,2CAAa/G,OAA8B0C,IAClE0H,EAAY3J,SAASsG,yCAAa/G,OAA4B0C,IAC9D2H,EAAe5J,SAASsG,4CAAa/G,OAA+B0C,IAE1EqH,EAAOxB,iBAAiB,SAAS,WAG/BvH,EAAW+I,EACb,IAEAA,EAAOxB,iBAAiB,UAAWtB,GAEnC8C,EAAOxB,iBAAiB,aAAa,SAAAxC,GAGnCA,EAAEuE,iBACJ,IAEAN,EAAQzB,iBAAiB,SAAS,SAAAxC,GAGhC,IAAMwE,EAAKxE,EAAEK,OACb,GAAImE,EAAGtL,QAAQ,qBAAsB,CACnC,IAAIoB,EAAU,GACRmK,EAASD,EAAGjJ,WAClB,IAAKkJ,EAAOvL,QAAQ,wBAAyB,CAG3C,GADAoB,GADAA,EAAUmK,EAAOC,aAAa,SAASzI,QAAQ,oBAAqB,KAClDA,QAAQ,KAAM,IAC5BjD,OAAO2L,eAAiB3L,OAAO2L,cAAcC,QAE/C,OAAO5L,OAAO2L,cAAcC,QAAQ,OAAQtK,GACvC,GAAII,SAASmK,uBAAyBnK,SAASmK,sBAAsB,QAAS,CACnF,IAAMC,EAAWpK,SAASiG,cAAc,YACxCmE,EAASC,YAAczK,EACvBwK,EAASpJ,MAAMsJ,SAAW,QAC1BtK,SAASY,KAAKuF,YAAYiE,GAC1BA,EAASG,SACT,IACE,OAAOvK,SAASwK,YAAY,OAC5B,CAAA,MAAOC,GACP,OAAOpC,OAAO,mCAAoCzI,EACpD,CAAU,QACRI,SAASY,KAAK2F,YAAY6D,EAC5B,CACF,CACF,CACF,CACF,IAEAX,EAAW3B,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAA2I,WAAA5I,IAAAK,MAAA,SAAAwI,GAAA,OAAA,OAAAA,EAAAtI,KAAAsI,EAAArK,MAAA,KAAA,EAGnCtB,EAASiD,GAAaqH,EAAO/F,MAC7BiG,EAAOxB,UAAUC,OAAO,UACxBsB,EAAQvB,UAAUC,OAAO,UACzB0B,EAAU3B,UAAUC,OAAO,UAC3B2B,EAAa5B,UAAUC,OAAO,UAC9BwB,EAAWzB,UAAUC,OAAO,UAC5ByB,EAAY1B,UAAUC,OAAO,UAC7B1H,EAAW+I,GACXA,EAAOsB,QAAO,KAAA,EAAA,IAAA,MAAA,OAAAD,EAAA/H,OAAA,GAAA8H,EACf,MAEDhB,EAAY5B,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAA8I,IAAA,IAAA7G,EAAA3C,EAAAyJ,EAAAlG,EAAAmG,SAAAjJ,IAAAK,MAAA,SAAA6I,GAAA,OAAA,OAAAA,EAAA3I,KAAA2I,EAAA1K,MAAA,KAAA,EAG9B0D,EAAO1F,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAC3CxD,EAAMD,EAAUkI,EAAO/F,OACvBuH,EAAMxM,OAAOkF,SACfoB,EAAW,eACXkG,EAAIlG,SAASqG,WAAU1L,IAAAA,OAAKyE,EAAO,QACrCY,EAAQrF,IAAAA,OAAOyE,EAAkB,kBAE7B+G,KAAIxL,OAAMuL,EAAII,eAAQ3L,OAAKuL,EAAIK,MAAI5L,OAAGqF,EAAQrF,UAAAA,OAAS8B,IACpD0F,OAAS,IAChBqE,MACE,gFAGFC,UAAUC,UAAUC,UAAUR,GAAMS,KAAI3J,EAAAC,IAAAC,MAAC,SAAA0J,WAAA3J,IAAAK,MAAA,SAAAuJ,GAAA,OAAA,OAAAA,EAAArJ,KAAAqJ,EAAApL,MAAA,KAAA,EACvC8K,MAAM,+BAA+B,KAAA,EAAA,IAAA,MAAA,OAAAM,EAAA9I,OAAA,GAAA6I,EAAA,KACtC5J,EAAAC,IAAAC,MAAE,SAAA4J,WAAA7J,IAAAK,MAAA,SAAAyJ,GAAA,OAAA,OAAAA,EAAAvJ,KAAAuJ,EAAAtL,MAAA,KAAA,EACD8K,MAAM,kCAAkC,KAAA,EAAA,IAAA,MAAA,OAAAQ,EAAAhJ,OAAA,GAAA+I,EACzC,MACF,KAAA,EAAA,IAAA,MAAA,OAAAX,EAAApI,OAAA,GAAAiI,EACF,MAEDlB,EAAU7B,iBAAiB,QAAOjG,EAAAC,IAAAC,MAAE,SAAA8J,IAAA,IAAAC,EAAAC,SAAAjK,IAAAK,MAAA,SAAA6J,GAAA,OAAA,OAAAA,EAAA3J,KAAA2J,EAAA1L,MAAA,KAAA,EAAA,IAG9BzB,EAAI,CAAAmN,EAAA1L,KAAA,EAAA,KAAA,CAAA,OAAA0L,EAAAC,OAAA,UAAA,KAAA,EAaP,OATDpN,GAAO,EAEDiN,EAAOtC,EAAOlD,cAAc,QAClCZ,EAASoG,EAAM,MAAM,IACfC,EAAU/L,SAAS6H,iBAAiB,iCAExCkE,EAAQ3C,SAAQ,SAAA8C,GACdA,EAAE3J,aAAa,WAAY,GAC7B,IACDyJ,EAAA1L,KAAA,EACKsD,GAAa,GAAM,KAAA,EACmB,OAA5C2F,EAAQjD,cAAc,QAAQJ,UAAY,GAAE8F,EAAA1L,KAAA,GACtCqB,EAAUM,GAAU,KAAA,GACtB8J,GACFA,EAAQ3C,SAAQ,SAAA8C,GACdA,EAAEvJ,gBAAgB,WACpB,IAEF0D,EAASyF,GACTtC,EAAOxB,UAAUC,OAAO,UACxBsB,EAAQvB,UAAUC,OAAO,UACzBwB,EAAWzB,UAAUC,OAAO,UAC5ByB,EAAY1B,UAAUC,OAAO,UAC7B0B,EAAU3B,UAAUC,OAAO,UAC3B2B,EAAa5B,UAAUC,OAAO,iBAEvBjJ,EAASiD,GAChBpD,GAAO,EAAK,KAAA,GAAA,IAAA,MAAA,OAAAmN,EAAApJ,OAAA,GAAAiJ,EACb,MAEDjC,EAAa9B,iBAAiB,SAAS,WAGrCwB,EAAO/F,MAAQvE,EAASiD,UACjBjD,EAASiD,GAChBuH,EAAOxB,UAAUC,OAAO,UACxBsB,EAAQvB,UAAUC,OAAO,UACzBwB,EAAWzB,UAAUC,OAAO,UAC5ByB,EAAY1B,UAAUC,OAAO,UAC7B0B,EAAU3B,UAAUC,OAAO,UAC3B2B,EAAa5B,UAAUC,OAAO,SAChC,GACF,IAAE,KAAA,EAAA,IAAA,MAAA,OAAAL,EAAAhF,OAAA,GAAA2E,EACH,KAAA,OAtNKF,SAAI8E,GAAA,OAAA7E,EAAAxE,MAAAC,KAAAC,WAAA,CAAA,GAwNJyF,EAAI,WAAA,IAAA2D,EAAAvK,EAAAC,IAAAC,MAAG,SAAAsK,EAAM7E,EAAO8E,GAAM,IAAAC,EAAAC,EAAAC,EAAApL,EAAA6H,EAAAwD,EAAAnJ,EAAAoJ,EAAA/M,SAAAkC,IAAAK,MAAA,SAAAyK,GAAA,OAAA,OAAAA,EAAAvK,KAAAuK,EAAAtM,MAAA,KAAA,EAKjB,GAAbtB,EAAW,CAAA,GAEPV,OAAOkF,SAASoB,SAASO,SAAS,gBAAe,CAAAyH,EAAAtM,KAAA,GAAA,KAAA,CAKF,GAJ3CiM,EAASD,GAAU,IAAI9D,gBAAgBlK,OAAOkF,SAAS8I,QACvDE,EAAU,qBACVC,EAAU,sBACVpL,EAAMkL,EAAOM,IAAI,UAAYN,EAAOO,IAAI,UAAYP,EAAOO,IAAI,YAC/D5D,EAAUlJ,SAASsG,cAAc,WAC3B,OAARjF,IAAgBA,EAAI0L,OAAM,CAAAH,EAAAtM,KAAA,GAAA,KAAA,CAEF,OAA1BoF,EAASwD,EAASsD,GAAQI,EAAAtM,KAAA,GACpBsD,GAAa,GAAK,KAAA,GACxByC,EAAS6C,GACTxD,EAASwD,EAASuD,GAClB,IACQC,EAAWH,EAAOM,IAAI,UAAY,SAAW,WACnD1N,EAAa6N,mBAAmBT,EAAO7K,YACnC6B,EAAQ,GACNoJ,EAAM,IAAIM,eAChBlO,EAAOsC,EACPsL,EAAIO,KAAK,MAAO7L,GAAK,GACrBsL,EAAIQ,OAAMtL,EAAAC,IAAAC,MAAG,SAAAqL,WAAAtL,IAAAK,MAAA,SAAAkL,GAAA,OAAA,OAAAA,EAAAhL,KAAAgL,EAAA/M,MAAA,KAAA,EAUV,OARsB,IAAnBqM,EAAIW,YACa,MAAfX,EAAIY,SACNhK,EAAQoJ,EAAIa,cAIC,WAAbd,IACFnJ,EAAQ5D,EAAW4D,IACpB8J,EAAA/M,KAAA,EACK2C,EAASM,GAAM,KAAA,EAAA,OAAA8J,EAAA/M,KAAA,EACf+G,EAAKG,GAAM,KAAA,EACjBnB,EAAS6C,GACTnJ,IAAsB,KAAA,EAAA,IAAA,MAAA,OAAAsN,EAAAzK,OAAA,GAAAwK,EACvB,KACDT,EAAIc,MACN,CAAE,MAAOpI,GAAQ,CAAAuH,EAAAtM,KAAA,GAAA,MAAA,KAAA,GAKS,OAH1BvB,EAAO,GACDa,EAAUD,EAAW4M,EAAOM,IAAI,QAAUN,EAAOO,IAAI,QAAUtN,GACrEL,EAAa6N,mBAAmBT,EAAO7K,YACvCgE,EAASwD,EAASsD,GAAQI,EAAAtM,KAAA,GACpBsD,GAAa,GAAK,KAAA,GAEE,OAD1ByC,EAAS6C,GACTxD,EAASwD,EAASuD,GAAQG,EAAAtM,KAAA,GACpB2C,EAASrD,GAAQ,KAAA,GAAA,OAAAgN,EAAAtM,KAAA,GACjB+G,EAAKG,GAAM,KAAA,GACjBnB,EAAS6C,GACTnJ,IAAsB,KAAA,GAAA6M,EAAAtM,KAAA,GAAA,MAAA,KAAA,GAGxBvB,EAAO,GACPI,EAAa,GACbkI,EAAKG,GAAM,KAAA,GAAA,IAAA,MAAA,OAAAoF,EAAAhK,OAAA,GAAAyJ,EAEd,KAAA,OAAA,SA9DSqB,EAAAC,GAAA,OAAAvB,EAAAtJ,MAAAC,KAAAC,WAAA,CAAA,GAiEVhD,SAAS8H,iBAAiB,SA7VE,SAAAxC,GAG1B,IAAMtB,EAAO1F,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAC3Cc,EAASL,EAAEK,QAAUL,EAAEsI,WAE7B,GAAuB,MAAnBjI,EAAOkI,SAAmBpF,GAE1B9C,EAAOqE,aAAa,SACpBrE,EAAOwF,OAAS7M,OAAOkF,SAAS2H,MAChC7M,OAAOkF,SAASoB,WAAQ,IAAArF,OAASyE,mBACjC1F,OAAOkF,SAASoB,WAAae,EAAOf,UACpCtG,OAAOkF,SAAS8I,SAAW3G,EAAO2G,OAClC,CACAhH,EAAEsB,iBACF,IAAM0F,EAAS,IAAI9D,gBAAgB7C,EAAO2G,QAC1C7D,GAAK,EAAO6D,EACd,KA4UoD,GAGxDhO,OAAOwJ,iBAAiB,YA9WP,WAGf,IAAM9D,EAAO1F,OAAOkF,SAASoB,SAASC,MAAM,KAAK,GAE/CvG,OAAOkF,SAASoB,eAAQrF,OAASyE,EAAI,kBAErBgJ,mBAAmB,IAAIxE,gBAAgBlK,OAAOkF,SAAS8I,QAAQ5K,cAC/DvC,GACdsJ,GAAK,OAwWXnK,OAAOwJ,iBAAiB,UAAU,WAChC1I,GAAO,CACT,IAGAd,OAAO+B,UAAUyN,WAAU,WAGrB1O,EACFA,GAAO,EAGTqJ,GAAK,EACP,GACD,CAxiBD"} \ No newline at end of file diff --git a/assets/coloraide-extras/extra-notebook-b4a7bff2.js b/assets/coloraide-extras/extra-notebook-b4a7bff2.js deleted file mode 100644 index fd857df67..000000000 --- a/assets/coloraide-extras/extra-notebook-b4a7bff2.js +++ /dev/null @@ -1,2 +0,0 @@ -function _typeof(t){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_typeof(t)}var runtime=function(t){"use strict";var e,n=Object.prototype,o=n.hasOwnProperty,r=Object.defineProperty||function(t,e,n){t[e]=n.value},i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",s=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag";function c(t,e,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{c({},"")}catch(t){c=function(t,e,n){return t[e]=n}}function u(t,e,n,o){var i=e&&e.prototype instanceof h?e:h,a=Object.create(i.prototype),s=new I(o||[]);return r(a,"_invoke",{value:S(t,n,s)}),a}function d(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}t.wrap=u;var p="suspendedStart",f="suspendedYield",m="executing",_="completed",g={};function h(){}function y(){}function v(){}var w={};c(w,a,(function(){return this}));var x=Object.getPrototypeOf,b=x&&x(x(T([])));b&&b!==n&&o.call(b,a)&&(w=b);var E=v.prototype=h.prototype=Object.create(w);function k(t){["next","throw","return"].forEach((function(e){c(t,e,(function(t){return this._invoke(e,t)}))}))}function L(t,e){function n(r,i,a,s){var l=d(t[r],t,i);if("throw"!==l.type){var c=l.arg,u=c.value;return u&&"object"===_typeof(u)&&o.call(u,"__await")?e.resolve(u.__await).then((function(t){n("next",t,a,s)}),(function(t){n("throw",t,a,s)})):e.resolve(u).then((function(t){c.value=t,a(c)}),(function(t){return n("throw",t,a,s)}))}s(l.arg)}var i;r(this,"_invoke",{value:function(t,o){function r(){return new e((function(e,r){n(t,o,e,r)}))}return i=i?i.then(r,r):r()}})}function S(t,n,o){var r=p;return function(i,a){if(r===m)throw new Error("Generator is already running");if(r===_){if("throw"===i)throw a;return{value:e,done:!0}}for(o.method=i,o.arg=a;;){var s=o.delegate;if(s){var l=A(s,o);if(l){if(l===g)continue;return l}}if("next"===o.method)o.sent=o._sent=o.arg;else if("throw"===o.method){if(r===p)throw r=_,o.arg;o.dispatchException(o.arg)}else"return"===o.method&&o.abrupt("return",o.arg);r=m;var c=d(t,n,o);if("normal"===c.type){if(r=o.done?_:f,c.arg===g)continue;return{value:c.arg,done:o.done}}"throw"===c.type&&(r=_,o.method="throw",o.arg=c.arg)}}}function A(t,n){var o=n.method,r=t.iterator[o];if(r===e)return n.delegate=null,"throw"===o&&t.iterator.return&&(n.method="return",n.arg=e,A(t,n),"throw"===n.method)||"return"!==o&&(n.method="throw",n.arg=new TypeError("The iterator does not provide a '"+o+"' method")),g;var i=d(r,t.iterator,n.arg);if("throw"===i.type)return n.method="throw",n.arg=i.arg,n.delegate=null,g;var a=i.arg;return a?a.done?(n[t.resultName]=a.value,n.next=t.nextLoc,"return"!==n.method&&(n.method="next",n.arg=e),n.delegate=null,g):a:(n.method="throw",n.arg=new TypeError("iterator result is not an object"),n.delegate=null,g)}function C(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function B(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(C,this),this.reset(!0)}function T(t){if(t||""===t){var n=t[a];if(n)return n.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var r=-1,i=function n(){for(;++r=0;--i){var a=this.tryEntries[i],s=a.completion;if("root"===a.tryLoc)return r("end");if(a.tryLoc<=this.prev){var l=o.call(a,"catchLoc"),c=o.call(a,"finallyLoc");if(l&&c){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),B(n),g}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var o=n.completion;if("throw"===o.type){var r=o.arg;B(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(t,n,o){return this.delegate={iterator:T(t),resultName:n,nextLoc:o},"next"===this.method&&(this.arg=e),g}},t}("object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports:{});try{regeneratorRuntime=runtime}catch(t){"object"===("undefined"==typeof globalThis?"undefined":_typeof(globalThis))?globalThis.regeneratorRuntime=runtime:Function("r","regeneratorRuntime = r")(runtime)}!function(){"use strict";function t(){t=function(){return e};var e={},n=Object.prototype,o=n.hasOwnProperty,r=Object.defineProperty||function(t,e,n){t[e]=n.value},i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",s=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag";function c(t,e,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{c({},"")}catch(t){c=function(t,e,n){return t[e]=n}}function u(t,e,n,o){var i=e&&e.prototype instanceof f?e:f,a=Object.create(i.prototype),s=new S(o||[]);return r(a,"_invoke",{value:b(t,n,s)}),a}function d(t,e,n){try{return{type:"normal",arg:t.call(e,n)}}catch(t){return{type:"throw",arg:t}}}e.wrap=u;var p={};function f(){}function m(){}function _(){}var g={};c(g,a,(function(){return this}));var h=Object.getPrototypeOf,y=h&&h(h(A([])));y&&y!==n&&o.call(y,a)&&(g=y);var v=_.prototype=f.prototype=Object.create(g);function w(t){["next","throw","return"].forEach((function(e){c(t,e,(function(t){return this._invoke(e,t)}))}))}function x(t,e){function n(r,i,a,s){var l=d(t[r],t,i);if("throw"!==l.type){var c=l.arg,u=c.value;return u&&"object"==_typeof(u)&&o.call(u,"__await")?e.resolve(u.__await).then((function(t){n("next",t,a,s)}),(function(t){n("throw",t,a,s)})):e.resolve(u).then((function(t){c.value=t,a(c)}),(function(t){return n("throw",t,a,s)}))}s(l.arg)}var i;r(this,"_invoke",{value:function(t,o){function r(){return new e((function(e,r){n(t,o,e,r)}))}return i=i?i.then(r,r):r()}})}function b(t,e,n){var o="suspendedStart";return function(r,i){if("executing"===o)throw new Error("Generator is already running");if("completed"===o){if("throw"===r)throw i;return{value:void 0,done:!0}}for(n.method=r,n.arg=i;;){var a=n.delegate;if(a){var s=E(a,n);if(s){if(s===p)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 l=d(t,e,n);if("normal"===l.type){if(o=n.done?"completed":"suspendedYield",l.arg===p)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(o="completed",n.method="throw",n.arg=l.arg)}}}function E(t,e){var n=e.method,o=t.iterator[n];if(void 0===o)return e.delegate=null,"throw"===n&&t.iterator.return&&(e.method="return",e.arg=void 0,E(t,e),"throw"===e.method)||"return"!==n&&(e.method="throw",e.arg=new TypeError("The iterator does not provide a '"+n+"' method")),p;var r=d(o,t.iterator,e.arg);if("throw"===r.type)return e.method="throw",e.arg=r.arg,e.delegate=null,p;var i=r.arg;return i?i.done?(e[t.resultName]=i.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=void 0),e.delegate=null,p):i:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,p)}function k(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function L(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function S(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(k,this),this.reset(!0)}function A(t){if(t||""===t){var e=t[a];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var n=-1,r=function e(){for(;++n=0;--r){var i=this.tryEntries[r],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var s=o.call(i,"catchLoc"),l=o.call(i,"finallyLoc");if(s&&l){if(this.prev=0;--n){var r=this.tryEntries[n];if(r.tryLoc<=this.prev&&o.call(r,"finallyLoc")&&this.prev=0;--e){var n=this.tryEntries[e];if(n.finallyLoc===t)return this.complete(n.completion,n.afterLoc),L(n),p}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n.tryLoc===t){var o=n.completion;if("throw"===o.type){var r=o.arg;L(n)}return r}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,n){return this.delegate={iterator:A(t),resultName:e,nextLoc:n},"next"===this.method&&(this.arg=void 0),p}},e}function e(t,e,n,o,r,i,a){try{var s=t[i](a),l=s.value}catch(t){return void n(t)}s.done?e(l):Promise.resolve(l).then(o,r)}function n(t){return function(){var n=this,o=arguments;return new Promise((function(r,i){var a=t.apply(n,o);function s(t){e(a,r,i,s,l,"next",t)}function l(t){e(a,r,i,s,l,"throw",t)}s(void 0)}))}}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:i}}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 a,s=!0,l=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){l=!0,a=t},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}}!function(){var e="";try{var o="srgb";window.matchMedia("(color-gamut: rec2020)").matches?o="rec2020":window.matchMedia("(color-gamut: p3)").matches&&(o="display-p3"),e=CSS.supports("color: color(display-p3 1 0 0)")?o:"srgb"}catch(t){e="srgb"}var i=null,a=!1,s="",l="",c={},u=/.*?_(\d+)$/,d=!1,p="",f=!1,m=/^( {1,4}|\t)/,_="\n_P='
{}
'\n_O='swatch'\n_N='transparent'\n_M='pycon'\n_L='playground'\n_K='gamut'\n_J='color'\n_I='{} {}%'\n_H='exceptions'\n_G='highlight'\n_F='class'\n_E='eval'\n_D=''\n_C=None\n_B=True\n_A=False\nimport xml.etree.ElementTree as Etree\nfrom collections.abc import Sequence,Mapping\nfrom collections import namedtuple\nimport ast\nfrom io import StringIO\nimport sys,re\nfrom functools import partial\nfrom pygments import highlight\nfrom pygments.lexers import get_lexer_by_name\nfrom pygments.formatters import find_formatter_class\nfrom coloraide import Color\nfrom coloraide.interpolate import Interpolator,normalize_domain\ntry:from coloraide_extras.everything import ColorAll\nexcept ImportError:from coloraide.everything import ColorAll\nPY310=(3,10)<=sys.version_info\nPY311=(3,11)<=sys.version_info\nWEBSPACE='srgb'\nAST_BLOCKS=ast.If,ast.For,ast.While,ast.Try,ast.With,ast.FunctionDef,ast.ClassDef,ast.AsyncFor,ast.AsyncWith,ast.AsyncFunctionDef\nif PY310:AST_BLOCKS=AST_BLOCKS+(ast.Match,)\nif PY311:AST_BLOCKS=AST_BLOCKS+(ast.TryStar,)\nRE_INIT=re.compile('^\\\\s*#\\\\s*pragma:\\\\s*init\\\\n(.*?)#\\\\s*pragma:\\\\s*init\\\\n',re.DOTALL|re.I)\nRE_COLOR_START=re.compile('(?i)(?:\\\\b(?\\n
\\n{results}\\n
\\n
\\n
\\n\\n
\\n
\\n
\\n
\\n\\n\\n\\n\\nGamut: {gamut}\\n
\\n
'\ncode_id=0\nclass Ramp(list):0\nclass Steps(list):0\nclass Row(list):0\nclass ColorTuple(namedtuple('ColorTuple',['string',_J])):0\nclass AtomicString(str):0\nclass Break(Exception):0\nclass Continue(Exception):0\nHtmlRow=Row\nHtmlSteps=Steps\nHtmlGradient=Ramp\ndef _escape(txt):txt=txt.replace('&','&');txt=txt.replace('<','<');txt=txt.replace('>','>');return txt\nclass StreamOut:\n\tdef __init__(self):self.old=sys.stdout;self.stdout=StringIO();sys.stdout=self.stdout\n\tdef read(self):\n\t\tvalue=''\n\t\tif self.stdout is not _C:self.stdout.flush();value=self.stdout.getvalue();self.stdout=StringIO();sys.stdout=self.stdout\n\t\treturn value\n\tdef __enter__(self):return self\n\tdef __exit__(self,type,value,traceback):sys.stdout=self.old;self.old=_C;self.stdout=_C\ndef get_colors(result):\n\tdomain=[]\n\tif isinstance(result,AtomicString):yield find_colors(result)\n\tif isinstance(result,Row):yield Row([ColorTuple(c.to_string(fit=_A),c.clone())if isinstance(c,Color)else ColorTuple(c,ColorAll(c))for c in result])\n\telif isinstance(result,(Steps,Ramp)):t=type(result);yield t([c.clone()if isinstance(c,Color)else ColorAll(c)for c in result])\n\telif isinstance(result,Color):yield[ColorTuple(result.to_string(fit=_A),result.clone())]\n\telif isinstance(result,Interpolator):\n\t\tif result._domain:domain=result._domain;result.domain(normalize_domain(result._domain))\n\t\tgrad=Ramp(result.steps(steps=5,max_delta_e=2.3))\n\t\tif domain:result._domain=domain;domain=[]\n\t\tyield grad\n\telif isinstance(result,str):\n\t\ttry:yield[ColorTuple(result,ColorAll(result))]\n\t\texcept Exception:pass\n\telif isinstance(result,(list,tuple)):\n\t\tfor r in result:\n\t\t\tfor x in get_colors(r):\n\t\t\t\tif x:yield x\ndef find_colors(text):\n\tcolors=[]\n\tfor m in RE_COLOR_START.finditer(text):\n\t\tstart=m.start();mcolor=ColorAll.match(text,start=start)\n\t\tif mcolor is not _C:colors.append(ColorTuple(text[mcolor.start:mcolor.end],mcolor.color))\n\treturn colors\ndef evaluate_with(node,g,loop,index=0):\n\tl=len(node.items)-1;withitem=node.items[index]\n\tif withitem.context_expr:\n\t\twith eval(compile(ast.Expression(withitem.context_expr),_D,_E),g)as w:\n\t\t\tg[withitem.optional_vars.id]=w\n\t\t\tif index=l2-1 or l1==l2:\n\t\t\t\tfor(e,p)in enumerate(node.patterns[:-1]if star else node.patterns):\n\t\t\t\t\tif not compare_match(s[e],g,p):return _A\n\t\t\t\tif star and node.patterns[-1].name:g[node.patterns[-1].name]=s[l2-1:]\n\t\t\t\treturn _B\n\t\treturn _A\n\telif isinstance(node,ast.MatchMapping):\n\t\tif isinstance(s,Mapping):\n\t\t\tstar=node.rest;l1,l2=len(s),len(node.patterns)\n\t\t\tif star and l1>=l2 or l1==l2:\n\t\t\t\tkeys=set()\n\t\t\t\tfor(kp,vp)in zip(node.keys,node.patterns):\n\t\t\t\t\tkey=eval(compile(ast.Expression(kp),_D,_E),g);keys.add(key)\n\t\t\t\t\tif key not in s:return _A\n\t\t\t\t\tif not compare_match(s[key],g,vp):return _A\n\t\t\t\tif star:g[star]={k:v for(k,v)in s.items()if k not in keys}\n\t\t\t\treturn _B\n\t\treturn _A\n\telif isinstance(node,ast.MatchClass):\n\t\tname=g.get(node.cls.id,_C)\n\t\tif name is _C:raise NameError(\"name '{}' is not defined\".format(node.cls.id))\n\t\tif not isinstance(s,name):return _A\n\t\tma=getattr(s,'__match_args__',());l1=len(ma);l2=len(node.patterns)\n\t\tif l1>> '+line\n\t\t\telse:stmt[i]='... '+line\n\t\tcommand+=A.join(stmt)\n\t\tif isinstance(node,AST_BLOCKS):command+='\\n... '\n\t\ttry:\n\t\t\twith StreamOut()as s:\n\t\t\t\tfor x in evaluate(node,g):\n\t\t\t\t\tresult.append(x);text=s.read()\n\t\t\t\t\tif text:result.append(AtomicString(text))\n\t\t\t\tconsole+=command\n\t\texcept Exception as e:\n\t\t\tif no_except:\n\t\t\t\tif not inline:from pymdownx.superfences import SuperFencesException;raise SuperFencesException from e\n\t\t\t\telse:from pymdownx.inlinehilite import InlineHiliteException;raise InlineHiliteException from e\n\t\t\timport traceback;console+='{}\\n{}'.format(command,traceback.format_exc());break\n\t\tresult_text=A\n\t\tfor r in result:\n\t\t\tif r is _C:continue\n\t\t\tfor clist in get_colors(r):\n\t\t\t\tif clist:colors.append(clist)\n\t\t\tresult_text+='{}{}'.format(repr(r)if isinstance(r,str)and not isinstance(r,AtomicString)else str(r),A if not isinstance(r,AtomicString)else'')\n\t\tconsole+=result_text\n\treturn console,colors\ndef colorize(src,lang,**options):HtmlFormatter=find_formatter_class('html');lexer=get_lexer_by_name(lang,**options);formatter=HtmlFormatter(cssclass=_G,wrapcode=_B);return highlight(src,lexer,formatter).strip()\ndef color_command_validator(language,inputs,options,attrs,md):\n\tvalid_inputs={_H,'play','wheel'}\n\tfor(k,v)in inputs.items():\n\t\tif k in valid_inputs:options[k]=_B;continue\n\t\tattrs[k]=v\n\treturn _B\ndef _color_command_console(colors,gamut=WEBSPACE):\n\tB=' ';A='
{}
';el='';bar=_A;values=[]\n\tfor item in colors:\n\t\tis_grad=isinstance(item,HtmlGradient);is_steps=isinstance(item,Steps)\n\t\tif is_grad or is_steps:\n\t\t\tcurrent=total=percent=last=0\n\t\t\tif isinstance(item,Steps):total=len(item);percent=100/total;current=percent\n\t\t\tif bar:el+=A.format(B.join(values));values=[]\n\t\t\tsub_el1='
{}
';style='--swatch-stops: ';stops=[]\n\t\t\tfor(e,color)in enumerate(item):\n\t\t\t\tcolor.fit(gamut);color_str=color.convert(gamut).to_string()\n\t\t\t\tif current:\n\t\t\t\t\tif is_steps:stops.append(_I.format(color_str,str(last)));stops.append(_I.format(color_str,str(current)))\n\t\t\t\t\telse:stops.append(color_str)\n\t\t\t\t\tlast=current\n\t\t\t\t\tif e'.format(style);el+=sub_el1.format(sub_el2);bar=_A\n\t\telse:\n\t\t\tis_row=_A\n\t\t\tif isinstance(item,Row):\n\t\t\t\tis_row=_B\n\t\t\t\tif bar and values:el+=A.format(B.join(values));values=[]\n\t\t\t\tbar=_A\n\t\t\tbar=_B\n\t\t\tfor color in item:\n\t\t\t\tbase_classes=_O\n\t\t\t\tif not color.color.in_gamut(gamut):base_classes+=' out-of-gamut'\n\t\t\t\tcolor.color.fit(gamut);srgb=color.color.convert(gamut);value1=srgb.to_string(alpha=_A);value2=srgb.to_string();style='--swatch-stops: {} 50%, {} 50%'.format(value1,value2);title=color.string;classes=base_classes;c=''.format(style=style);c='{color}'.format(classes=classes,color=c,title=title);values.append(c)\n\t\t\tif is_row and values:el+=A.format(B.join(values));values=[];bar=_A\n\tif bar:el+=A.format(B.join(values));values=[]\n\treturn el\ndef _color_command_formatter(src='',language='',class_name=_C,options=_C,md='',init='',**kwargs):\n\tC='';B='formatter';A='fenced_code_block';global code_id;from pymdownx.superfences import SuperFencesException;gamut=kwargs.get(_K,WEBSPACE);wheel=options.get('wheel',_A);play=options.get('play',_A)if options is not _C else _A\n\tif not play and language==_L:play=_B\n\tif not play:return md.preprocessors[A].extension.superfences[0][B](src=src,class_name=class_name,language='py',md=md,options=options,**kwargs)\n\ttry:\n\t\tif wheel:\n\t\t\tgamut='srgb';exceptions=options.get(_H,_A)if options is not _C else _A;_,colors=execute(src.strip(),not exceptions,init=init);l=len(colors)\n\t\t\tif l not in(12,24,48):raise SuperFencesException('Color wheel requires either 12, 24, or 48 colors')\n\t\t\tcolors=[c[0].color for c in colors]\n\t\t\tif l==12:freq=4;offset=6\n\t\t\telif l==24:freq=8;offset=12\n\t\t\telse:freq=16;offset=24\n\t\t\tprimary=list(reversed(colors[::freq]));secondary=list(reversed(colors[offset::freq]+[colors[offset//3]]));tertiary=list(reversed(colors[::offset//6]));color_rings=[primary,secondary,tertiary];extra_rings_start='';extra_rings_end=''\n\t\t\tif l>12:extra_rings_start='
';extra_rings_end+=C;color_rings.append(list(reversed(colors[::offset//12])))\n\t\t\tif l>24:extra_rings_start='
'+extra_rings_start;extra_rings_end+=C;color_rings.append(list(reversed(colors)))\n\t\t\tcolor_stops=''\n\t\t\tfor(i,colors)in enumerate(color_rings,1):\n\t\t\t\ttotal=len(colors);percent=100/total;current=percent;last=-1;stops=[]\n\t\t\t\tfor(e,color)in enumerate(colors):\n\t\t\t\t\tcolor.fit(gamut);color_str=color.convert(gamut).to_string()\n\t\t\t\t\tif current:\n\t\t\t\t\t\tstops.append(_I.format(color_str,str(last)));stops.append(_I.format(color_str,str(current)));last=current\n\t\t\t\t\t\tif e
{}
'.format(colorize(traceback.format_exc(),_M))\n\treturn el\ndef live_color_command_formatter(init='',gamut=WEBSPACE):return partial(_live_color_command_formatter,init=init,gamut=gamut)\ndef live_color_command_validator(language,inputs,options,attrs,md):value=color_command_validator(language,inputs,options,attrs,md);options[_H]=_B;return value\ndef render_console(*args,**kwargs):\n\tC='.swatch-bar';B='code';A='id_num';from js import document;gamut=kwargs.get(_K,WEBSPACE)\n\ttry:\n\t\tinputs=document.getElementById('__playground-inputs_{}'.format(globals()[A]));results=document.getElementById('__playground-results_{}'.format(globals()[A]));footer=document.querySelector('#__playground_{} .gamut'.format(globals()[A]));result=live_color_command_formatter(LIVE_INIT,gamut)(inputs.value);temp=document.createElement('div');temp.innerHTML=result;cmd=results.querySelector('.color-command')\n\t\tfor el in cmd.querySelectorAll(C):el.remove()\n\t\tfor el in temp.querySelectorAll(C):cmd.insertBefore(el,cmd.lastChild)\n\t\tfooter.innerHTML='Gamut: {}'.format(gamut);pre=cmd.querySelector('pre');pre.replaceChild(temp.querySelector(B),pre.querySelector(B));temp.remove();scrollingElement=results.querySelector(B);scrollingElement.scrollTop=scrollingElement.scrollHeight\n\texcept Exception as e:print(e)\ndef render_notebook(*args,**kwargs):\n\tM='diagram';L='pymdownx.blocks.tab';K='pymdownx.blocks.admonition';J='pymdownx.arithmatex';I='pymdownx.keys';H='pymdownx.magiclink';G='pymdownx.inlinehilite';F='pymdownx.superfences';E='markdown.extensions.smarty';D='markdown.extensions.toc';C='validator';B='format';A='name';import markdown;from pymdownx import slugs,superfences;from js import document;gamut=kwargs.get(_K,WEBSPACE);text=globals().get('content','');extensions=[D,E,'pymdownx.betterem','markdown.extensions.attr_list','markdown.extensions.tables','markdown.extensions.abbr','markdown.extensions.footnotes',F,'pymdownx.highlight',G,H,'pymdownx.tilde','pymdownx.caret','pymdownx.smartsymbols','pymdownx.emoji','pymdownx.escapeall','pymdownx.tasklist','pymdownx.striphtml','pymdownx.snippets',I,'pymdownx.saneheaders',J,K,'pymdownx.blocks.details','pymdownx.blocks.html','pymdownx.blocks.definition',L];extension_configs={D:{'slugify':slugs.slugify(case='lower'),'permalink':''},E:{'smart_quotes':_A},J:{'generic':_B,'block_tag':'pre'},F:{'preserve_tabs':_B,'custom_fences':[{A:M,_F:M,B:superfences.fence_code_format},{A:_L,_F:_L,B:color_command_formatter(LIVE_INIT,gamut),C:live_color_command_validator},{A:'python',_F:_G,B:color_command_formatter(LIVE_INIT,gamut),C:live_color_command_validator},{A:'py',_F:_G,B:color_command_formatter(LIVE_INIT,gamut),C:live_color_command_validator}]},G:{'custom_inline':[{A:_J,_F:_J,B:color_formatter(LIVE_INIT,gamut)}]},H:{'repo_url_shortener':_B,'repo_url_shorthand':_B,'social_url_shorthand':_B,'user':'facelessuser','repo':'coloraide'},I:{'separator':'+'},L:{'alternate_style':_B},K:{'types':['new','settings','note','abstract','info','tip','success','question','warning','failure','danger','bug','example','quote']}}\n\ttry:html=markdown.markdown(text,extensions=extensions,extension_configs=extension_configs)\n\texcept Exception:html=''\n\tcontent=document.getElementById('__notebook-render');content.innerHTML=html\n\naction = globals().get('action')\nif action == 'notebook':\n callback = render_notebook\nelse:\n callback = render_console\n\ncallback(gamut='".concat(e,"')\n"),g=window.colorNotebook.defaultPlayground,h=function(t){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\n````````py play\n".concat(t,"\n````````\n")},y=!1,v=!1,w=function(){f=!0,window.document.dispatchEvent(new Event("DOMContentLoaded",{bubbles:!0,cancelable:!0})),window.document$.next()},x=function(t){var e=window.pageXOffset||(document.documentElement||document.body.parentNode||document.body).scrollLeft,n=window.pageYOffset||(document.documentElement||document.body.parentNode||document.body).scrollTop;t.style.height="5px",t.style.height="".concat(t.scrollHeight,"px"),window.scrollTo(e,n)},b=function(t){return encodeURIComponent(t).replace(/[.!'()*]/g,(function(t){return"%".concat(t.charCodeAt(0).toString(16))}))},E=function(){var e=n(t().mark((function e(n){var o;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return(o=document.getElementById("__playground-inputs_".concat(n))).setAttribute("readonly",""),i.globals.set("id_num",n),i.globals.set("action","playground"),t.next=6,i.runPythonAsync(_);case 6:o.removeAttribute("readonly");case 7:case"end":return t.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),k=function(){var e=n(t().mark((function e(n){var o;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return i.globals.set("content",n),i.globals.set("action","notebook"),t.next=4,i.runPythonAsync(_);case 4:(o=document.getElementById("__notebook-input"))&&(s=n,o.value=n),window.location.hash&&(window.location.href=window.location.href);case 7:case"end":return t.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),L=function(){var e=n(t().mark((function e(n){var o,a,s,l,c,u;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(d){t.next=5;break}return d=!0,t.next=4,loadPyodide({indexURL:"https://cdn.jsdelivr.net/pyodide/v0.23.4/full/",fullStdLib:!1});case 4:i=t.sent;case 5:if((y||!n)&&(v||n)){t.next=14;break}o="".concat(window.location.origin,"/").concat(window.location.pathname.split("/")[1],"/playground/"),a=n?window.colorNotebook.notebookWheels:window.colorNotebook.playgroundWheels,s=[],n?y=!0:v=!0,l=r(a);try{for(l.s();!(c=l.n()).done;)(u=c.value).endsWith(".whl")?s.push(o+u):s.push(u)}catch(t){l.e(t)}finally{l.f()}return t.next=14,i.loadPackage(s);case 14:case"end":return t.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),S=function(t,e,n){var o=null==e?"Loading...":e,r=n?"loading relative":"loading",i=document.createElement("template");i.innerHTML='
').concat(o,"
"),t.appendChild(i.content.firstChild)},A=function(t){t.querySelector(".loading")&&t.removeChild(t.querySelector(".loading"))},C=function(t){if("Tab"===t.key){var e=t.target;if(e.selectionStart!==e.selectionEnd){t.preventDefault();for(var n=e.selectionStart,o=e.selectionEnd,r=e.value;n>0&&"\n"!==r[n-1];)n--;for(;o>0&&"\n"!==r[o-1]&&o2e3?alert("Code must be small enough to generate a shareable URL under 2000 characters!"):navigator.clipboard.writeText(l).then(n(t().mark((function e(){return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:alert("Link copied to clipboard :)");case 1:case"end":return t.stop()}}),e)}))),n(t().mark((function e(){return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:alert("Failed to copy link clipboard!");case 1:case"end":return t.stop()}}),e)}))));case 7:case"end":return e.stop()}}),e)})))),p.addEventListener("click",n(t().mark((function e(){var n,r;return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(!a){t.next=2;break}return t.abrupt("return");case 2:return a=!0,n=s.querySelector("form"),S(n,null,!0),(r=document.querySelectorAll(".playground .playground-run"))&&r.forEach((function(t){t.setAttribute("disabled","")})),t.next=9,L(!1);case 9:return i.querySelector("code").innerHTML="",t.next=12,E(o);case 12:r&&r.forEach((function(t){t.removeAttribute("disabled")})),A(n),s.classList.toggle("hidden"),i.classList.toggle("hidden"),l.classList.toggle("hidden"),d.classList.toggle("hidden"),p.classList.toggle("hidden"),f.classList.toggle("hidden"),delete c[o],a=!1;case 22:case"end":return t.stop()}}),e)})))),f.addEventListener("click",(function(){r.value=c[o],delete c[o],s.classList.toggle("hidden"),i.classList.toggle("hidden"),l.classList.toggle("hidden"),d.classList.toggle("hidden"),p.classList.toggle("hidden"),f.classList.toggle("hidden")}))}));case 4:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),I=function(){var e=n(t().mark((function e(o,r){var i,a,s,u,d,f,m,_,y;return t().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(c={},!window.location.pathname.endsWith("/playground/")){e.next=32;break}if(i=r||new URLSearchParams(window.location.search),a="Loading Pyodide...",s="Loading Notebook...",u=i.has("source")?i.get("source"):i.get("notebook"),d=document.querySelector("article"),null===u||!u.trim()){e.next=16;break}return S(d,a),e.next=11,L(!0);case 11:A(d),S(d,s);try{f=i.has("source")?"source":"notebook",p=decodeURIComponent(i.toString()),m="",_=new XMLHttpRequest,l=u,_.open("GET",u,!0),_.onload=n(t().mark((function e(){return t().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return 4===_.readyState&&200===_.status&&(m=_.responseText),"source"===f&&(m=h(m)),t.next=4,k(m);case 4:return t.next=6,B(o);case 6:A(d),w();case 8:case"end":return t.stop()}}),e)}))),_.send()}catch(t){}e.next=30;break;case 16:return l="",y=h(i.has("code")?i.get("code"):g),p=decodeURIComponent(i.toString()),S(d,a),e.next=22,L(!0);case 22:return A(d),S(d,s),e.next=26,k(y);case 26:return e.next=28,B(o);case 28:A(d),w();case 30:e.next=35;break;case 32:l="",p="",B(o);case 35:case"end":return e.stop()}}),e)})));return function(t,n){return e.apply(this,arguments)}}();document.addEventListener("click",(function(t){var e=window.location.pathname.split("/")[1],n=t.target||t.srcElement;if("A"===n.tagName&&I&&n.getAttribute("href")&&n.host===window.location.host&&window.location.pathname==="/".concat(e,"/playground/")&&window.location.pathname===n.pathname&&window.location.search!==n.search){t.preventDefault();var o=new URLSearchParams(n.search);I(!1,o)}}),!0),window.addEventListener("popstate",(function(){var t=window.location.pathname.split("/")[1];window.location.pathname==="/".concat(t,"/playground/")&&(decodeURIComponent(new URLSearchParams(window.location.search).toString())!==p&&I(!1))})),window.addEventListener("unload",(function(){f=!1})),window.document$.subscribe((function(){f?f=!1:I(!0)}))}()}(); -//# sourceMappingURL=extra-notebook-b4a7bff2.js.map diff --git a/assets/coloraide-extras/manifest-js.json b/assets/coloraide-extras/manifest-js.json index 1a3cac206..f4a09fa73 100644 --- a/assets/coloraide-extras/manifest-js.json +++ b/assets/coloraide-extras/manifest-js.json @@ -1,3 +1,3 @@ { - "extra-notebook.js": "extra-notebook-b4a7bff2.js" + "extra-notebook.js": "extra-notebook-61fde54c.js" } \ No newline at end of file diff --git a/average/index.html b/average/index.html index 7685bb5ec..ff0b56976 100644 --- a/average/index.html +++ b/average/index.html @@ -84,4 +84,4 @@

Last update: August 26, 2023
\ No newline at end of file +
Last update: August 26, 2023
\ No newline at end of file diff --git a/cat/index.html b/cat/index.html index 39f8ef099..faa7a7248 100644 --- a/cat/index.html +++ b/cat/index.html @@ -61,4 +61,4 @@ d50, d65
Last update: June 25, 2023
\ No newline at end of file +
Last update: June 25, 2023
\ No newline at end of file diff --git a/chromaticity/index.html b/chromaticity/index.html index 184dab1ee..417889b16 100644 --- a/chromaticity/index.html +++ b/chromaticity/index.html @@ -72,4 +72,4 @@ Color.convert_chromaticity('xyz', 'xy-1931', black.convert('xyz-d65').coords(), white=black.white('xy-1931'))
Last update: June 25, 2023
\ No newline at end of file +
Last update: June 25, 2023
\ No newline at end of file diff --git a/color/index.html b/color/index.html index b2251ac52..6d8d81969 100644 --- a/color/index.html +++ b/color/index.html @@ -36,12 +36,12 @@ color(srgb 0 0 1 / 1)

Tip

If the Color class has be subclassed, this is an easy way to convert between the different subclasses, assuming the registered color spaces are compatible between the two different Color classes.

Random

If you'd like to generate a random color, simply call Color.random with a given color space and one will be generated.

>>> [Color.random('srgb') for _ in range(10)]
-[color(srgb 0.36945 0.94249 0.8054 / 1), color(srgb 0.62098 0.56303 0.51077 / 1), color(srgb 0.56704 0.55184 0.27834 / 1), color(srgb 0.88936 0.56696 0.69625 / 1), color(srgb 0.258 0.6967 0.09435 / 1), color(srgb 0.26608 0.0159 0.92105 / 1), color(srgb 0.73206 0.23182 0.01587 / 1), color(srgb 0.49447 0.23812 0.13037 / 1), color(srgb 0.95768 0.22528 0.84887 / 1), color(srgb 0.81995 0.7889 0.3919 / 1)]
-

Ranges are based on the color space's defined channel range. For color spaces with defined gamuts, the values will be confined to appropriate ranges. For color space's without defined gamuts, the ranges may be quite arbitrary in some cases. For color spaces with no hard, defined gamut, or gamuts that that far exceed practical usage it is recommend to fit the colors to whatever gamut you'd like, or simply use a target space with a clear defined gamut.

>>> Color.random('lab').fit('srgb')
-color(--lab 59.478 66.62 71.212 / 1)
-

Lastly, if you'd like to further constrain the limits, you can provide a list of constraints. A constraint should be a sequence of two values specifying the minimum and maximum for the channel. If None is provided, that constraint will be ignored. If the list doesn't have enough values, those missing indexes will be ignored. If the list has too many values, those extra values will be ignored.

>>> Color.random('srgb', limits=[(0.25, 0.75)] * 3)
-color(srgb 0.47308 0.61544 0.6777 / 1)
+color1.new("blue")  

Tip

If the Color class has be subclassed, this is an easy way to convert between the different subclasses, assuming the registered color spaces are compatible between the two different Color classes.

Random

If you'd like to generate a random color, simply call Color.random with a given color space and one will be generated.

>>> [Color.random('srgb') for _ in range(10)]
+[color(srgb 0.02438 0.67719 0.81316 / 1), color(srgb 0.35667 0.04083 0.32678 / 1), color(srgb 0.71755 0.91093 0.74213 / 1), color(srgb 0.60069 0.88372 0.07998 / 1), color(srgb 0.95268 0.8185 0.2335 / 1), color(srgb 0.401 0.16545 0.66271 / 1), color(srgb 0.27553 0.38181 0.14499 / 1), color(srgb 0.98796 0.78747 0.21055 / 1), color(srgb 0.84131 0.563 0.16015 / 1), color(srgb 0.56572 0.39736 0.85305 / 1)]
+

Ranges are based on the color space's defined channel range. For color spaces with defined gamuts, the values will be confined to appropriate ranges. For color space's without defined gamuts, the ranges may be quite arbitrary in some cases. For color spaces with no hard, defined gamut, or gamuts that that far exceed practical usage it is recommend to fit the colors to whatever gamut you'd like, or simply use a target space with a clear defined gamut.

>>> Color.random('lab').fit('srgb')
+color(--lab 8.3727 17.942 -41.971 / 1)
+

Lastly, if you'd like to further constrain the limits, you can provide a list of constraints. A constraint should be a sequence of two values specifying the minimum and maximum for the channel. If None is provided, that constraint will be ignored. If the list doesn't have enough values, those missing indexes will be ignored. If the list has too many values, those extra values will be ignored.

>>> Color.random('srgb', limits=[(0.25, 0.75)] * 3)
+color(srgb 0.60899 0.5199 0.33739 / 1)
 

Cloning

The clone method is an easy way to duplicate the current color object.

Here we clone the green color object, giving us two.

>>> c1 = Color("green")
 >>> c1
 color(srgb 0 0.50196 0 / 1)
@@ -210,4 +210,4 @@
     print('Could not convert to Lab D65 as it is no longer registered')  

Use of * with deregister will remove all plugins. Use of category:* will remove all plugins of that category.


Last update: August 23, 2023
\ No newline at end of file +
Last update: August 23, 2023
\ No newline at end of file diff --git a/colors/a98_rgb/index.html b/colors/a98_rgb/index.html index 6dae6222c..b37c6ac4c 100644 --- a/colors/a98_rgb/index.html +++ b/colors/a98_rgb/index.html @@ -14,4 +14,4 @@

Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/a98_rgb_linear/index.html b/colors/a98_rgb_linear/index.html index f17530cf5..a9697f221 100644 --- a/colors/a98_rgb_linear/index.html +++ b/colors/a98_rgb_linear/index.html @@ -14,4 +14,4 @@

Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/aces2065_1/index.html b/colors/aces2065_1/index.html index 4e1e28a7e..df04ca9a8 100644 --- a/colors/aces2065_1/index.html +++ b/colors/aces2065_1/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/acescc/index.html b/colors/acescc/index.html index 7c97a778a..207a43ab6 100644 --- a/colors/acescc/index.html +++ b/colors/acescc/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/acescct/index.html b/colors/acescct/index.html index a26c7fae9..7694c701e 100644 --- a/colors/acescct/index.html +++ b/colors/acescct/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/acescg/index.html b/colors/acescg/index.html index eb72e4d3a..069492e68 100644 --- a/colors/acescg/index.html +++ b/colors/acescg/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cam16/index.html b/colors/cam16/index.html index 45ddfeb25..7189ad302 100644 --- a/colors/cam16/index.html +++ b/colors/cam16/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cam16_jmh/index.html b/colors/cam16_jmh/index.html index 6876f8bf8..9dfa92288 100644 --- a/colors/cam16_jmh/index.html +++ b/colors/cam16_jmh/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cam16_lcd/index.html b/colors/cam16_lcd/index.html index 2084fbbf2..7e5cbf9fe 100644 --- a/colors/cam16_lcd/index.html +++ b/colors/cam16_lcd/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cam16_scd/index.html b/colors/cam16_scd/index.html index d3196a545..112052d9a 100644 --- a/colors/cam16_scd/index.html +++ b/colors/cam16_scd/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cam16_ucs/index.html b/colors/cam16_ucs/index.html index 37b17eac9..2aa64c4be 100644 --- a/colors/cam16_ucs/index.html +++ b/colors/cam16_ucs/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cmy/index.html b/colors/cmy/index.html index e97c55024..1683083d8 100644 --- a/colors/cmy/index.html +++ b/colors/cmy/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cmyk/index.html b/colors/cmyk/index.html index fc338b168..065716251 100644 --- a/colors/cmyk/index.html +++ b/colors/cmyk/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/cubehelix/index.html b/colors/cubehelix/index.html index 7cb613958..cdaa2326a 100644 --- a/colors/cubehelix/index.html +++ b/colors/cubehelix/index.html @@ -1,36 +1,36 @@ Cubehelix - ColorAide Documentation

Cubehelix

The Cubehelix color space is not registered in Color by default

Properties

Name: cubehelix

White Point: D65 / 2˚

Coordinates:

Name Range*
h [0, 360)
s [0, 4.614]
l [0, 1]

* The maximum saturation represents how high saturation can go, not that all colors with that saturation will be valid. As seen in the 3D rendering, while the coordinates are cylindrical, the shape of the space is not a cylinder.

Cubehelix

The sRGB gamut represented within the Cubehelix color space.

Cubehelix is a color scheme created by Dave Green. It was originally created for the display of astronomical intensity images. It is not really one color scheme, but a method to generate various "cubehelix" color schemes. The name comes from the way the colors spiral through the sRGB color space.

Mike Bostock of Observable and D3 fame along with Jason Davies took the color scheme and created a cylindrical color space with it. This is the color space that is implemented in ColorAide.

Cubehelix color schemes can be easily generated by interpolating in the color space.

>>> c1 = Color('cubehelix', [0, 1, 0])
 >>> c2 = Color('cubehelix', [360, 1, 1])
 >>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='longer')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ba1150>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112e4e290>
 >>> Color.interpolate([c1, c2], space='cubehelix', hue='longer')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5a91490>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112dadc50>
 

You can change the scheme by changing the start and end angle.

>>> c1 = Color('cubehelix', [0 + 180, 1, 0])
 >>> c2 = Color('cubehelix', [360 + 180, 1, 1])
 >>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='longer')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5b9d6d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1124d1410>
 >>> Color.interpolate([c1, c2], space='cubehelix', hue='longer')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab59fa210>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112ed8f50>
 

You can increase the rotations by setting hue interpolation to specified and extending the angle difference to a distance greater than 360.

>>> c1 = Color('cubehelix', [0, 1, 0])
 >>> c2 = Color('cubehelix', [360 * 3, 1, 1])
 >>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='specified')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ebac90>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1117e74d0>
 >>> Color.interpolate([c1, c2], space='cubehelix', hue='specified')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5b821d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112be8b50>
 

You can even reverse the rotation by utilizing a negative difference in hue.

>>> c1 = Color('cubehelix', [0, 1, 0])
 >>> c2 = Color('cubehelix', [-360, 1, 1])
 >>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='specified')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d31310>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b6f890>
 >>> Color.interpolate([c1, c2], steps=16, space='cubehelix', hue='specified')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ba17d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112de27d0>
 

Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/hpluv/index.html b/colors/hpluv/index.html index 592e90948..31e26bb14 100644 --- a/colors/hpluv/index.html +++ b/colors/hpluv/index.html @@ -1,4 +1,4 @@ - HPLuv - ColorAide Documentation

HPLuv

The HPLuv color space is not registered in Color by default

Properties

Name: hpluv

Color CSS ID: --hpluv

White Point: D65 / 2˚

Coordinates:

Name Range
h [0, 360)
p [0, 100]
l [0, 100]

HPLuv 3D

HSLuv color space in 3D

HPLuv is similar to HSLuv but takes as many colors as it can from CIELChuv without distorting the chroma. This ends up reducing the gamut to a subset of the sRGB gamut. In the end, only more pastel colors remain.

Learn about HSLuv

Channel Aliases

Channels Aliases
h hue
s perpendiculars
l lightness

Inputs/Output

HPLuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hpluv:

color(--hpluv h p l / a)  // Color function
+ HPLuv - ColorAide Documentation      

HPLuv

The HPLuv color space is not registered in Color by default

Properties

Name: hpluv

Color CSS ID: --hpluv

White Point: D65 / 2˚

Coordinates:

Name Range
h [0, 360)
p [0, 100]
l [0, 100]

HPLuv 3D

HPLuv color space in 3D

HPLuv is similar to HSLuv but takes as many colors as it can from CIELChuv without distorting the chroma. This ends up reducing the gamut to a subset of the sRGB gamut. In the end, only more pastel colors remain.

Learn about HPLuv

Channel Aliases

Channels Aliases
h hue
s perpendiculars
l lightness

Inputs/Output

HPLuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hpluv:

color(--hpluv h p l / a)  // Color function
 

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color("hpluv", [0, 0, 0], 1)
 

The string representation of the color object and the default string output use the color(--hpluv h p l / a) form.

>>> Color("hpluv", [23.881, 100, 53.237])
 color(--hpluv 23.881 100 53.237 / 1)
@@ -14,4 +14,4 @@
 

Last update: September 10, 2023
\ No newline at end of file +
Last update: September 17, 2023
\ No newline at end of file diff --git a/colors/hsi/index.html b/colors/hsi/index.html index 8a4c92b5d..34d08363e 100644 --- a/colors/hsi/index.html +++ b/colors/hsi/index.html @@ -13,4 +13,4 @@

Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/hsl/index.html b/colors/hsl/index.html index b169da47f..239dfa32b 100644 --- a/colors/hsl/index.html +++ b/colors/hsl/index.html @@ -26,4 +26,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/hsluv/index.html b/colors/hsluv/index.html index 0291f3e09..a343aabda 100644 --- a/colors/hsluv/index.html +++ b/colors/hsluv/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/hsv/index.html b/colors/hsv/index.html index 9519721f8..653ee56fa 100644 --- a/colors/hsv/index.html +++ b/colors/hsv/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/hunter_lab/index.html b/colors/hunter_lab/index.html index 03b0cf3ca..58c8fec20 100644 --- a/colors/hunter_lab/index.html +++ b/colors/hunter_lab/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/hwb/index.html b/colors/hwb/index.html index 4c51c5433..a8e794e74 100644 --- a/colors/hwb/index.html +++ b/colors/hwb/index.html @@ -21,4 +21,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/ictcp/index.html b/colors/ictcp/index.html index 622ff26b4..1382ec473 100644 --- a/colors/ictcp/index.html +++ b/colors/ictcp/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/igpgtg/index.html b/colors/igpgtg/index.html index a50403383..83daa4a46 100644 --- a/colors/igpgtg/index.html +++ b/colors/igpgtg/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/index.html b/colors/index.html index 060160b5a..cc694c980 100644 --- a/colors/index.html +++ b/colors/index.html @@ -189,4 +189,4 @@ click cubehelix "./cubehelix/" _self
Last update: August 28, 2023
\ No newline at end of file +
Last update: August 28, 2023
\ No newline at end of file diff --git a/colors/ipt/index.html b/colors/ipt/index.html index 7e6a8a5c3..9085e865e 100644 --- a/colors/ipt/index.html +++ b/colors/ipt/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/jzazbz/index.html b/colors/jzazbz/index.html index d30897395..44c3e1a83 100644 --- a/colors/jzazbz/index.html +++ b/colors/jzazbz/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/jzczhz/index.html b/colors/jzczhz/index.html index 03fa97848..c2a566570 100644 --- a/colors/jzczhz/index.html +++ b/colors/jzczhz/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/lab/index.html b/colors/lab/index.html index 2cfa22044..67870d662 100644 --- a/colors/lab/index.html +++ b/colors/lab/index.html @@ -21,4 +21,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/lab_d65/index.html b/colors/lab_d65/index.html index 502a5212d..a27288fc6 100644 --- a/colors/lab_d65/index.html +++ b/colors/lab_d65/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/lch/index.html b/colors/lch/index.html index 46611c8e9..e357f6444 100644 --- a/colors/lch/index.html +++ b/colors/lch/index.html @@ -21,4 +21,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/lch99o/index.html b/colors/lch99o/index.html index f13267d22..2490272e0 100644 --- a/colors/lch99o/index.html +++ b/colors/lch99o/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/lch_d65/index.html b/colors/lch_d65/index.html index 96c1e5d10..13b78c408 100644 --- a/colors/lch_d65/index.html +++ b/colors/lch_d65/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/lchuv/index.html b/colors/lchuv/index.html index 97a281cce..d786ec128 100644 --- a/colors/lchuv/index.html +++ b/colors/lchuv/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/luv/index.html b/colors/luv/index.html index 37c820458..95ca19af3 100644 --- a/colors/luv/index.html +++ b/colors/luv/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/okhsl/index.html b/colors/okhsl/index.html index 8186a503e..f8f888b2c 100644 --- a/colors/okhsl/index.html +++ b/colors/okhsl/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/okhsv/index.html b/colors/okhsv/index.html index 4f4d3c07f..93219107b 100644 --- a/colors/okhsv/index.html +++ b/colors/okhsv/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/oklab/index.html b/colors/oklab/index.html index 9ea101dff..6ee360aba 100644 --- a/colors/oklab/index.html +++ b/colors/oklab/index.html @@ -21,4 +21,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/oklch/index.html b/colors/oklch/index.html index f08aac5e8..5e16da018 100644 --- a/colors/oklch/index.html +++ b/colors/oklch/index.html @@ -20,4 +20,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/orgb/index.html b/colors/orgb/index.html index 7d381d7c4..041610b8b 100644 --- a/colors/orgb/index.html +++ b/colors/orgb/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/prismatic/index.html b/colors/prismatic/index.html index dcbbb7db4..503f9ada8 100644 --- a/colors/prismatic/index.html +++ b/colors/prismatic/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/prophoto_rgb/index.html b/colors/prophoto_rgb/index.html index 51a716dcf..e80c4444f 100644 --- a/colors/prophoto_rgb/index.html +++ b/colors/prophoto_rgb/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/prophoto_rgb_linear/index.html b/colors/prophoto_rgb_linear/index.html index 02087e69d..ea0ab0389 100644 --- a/colors/prophoto_rgb_linear/index.html +++ b/colors/prophoto_rgb_linear/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/rec2020/index.html b/colors/rec2020/index.html index 096773fa4..cc7493378 100644 --- a/colors/rec2020/index.html +++ b/colors/rec2020/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/rec2020_linear/index.html b/colors/rec2020_linear/index.html index c36ae6a2b..e12fa8fe7 100644 --- a/colors/rec2020_linear/index.html +++ b/colors/rec2020_linear/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/rec2100_hlg/index.html b/colors/rec2100_hlg/index.html index 6f7aa2c75..d93d66343 100644 --- a/colors/rec2100_hlg/index.html +++ b/colors/rec2100_hlg/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/rec2100_pq/index.html b/colors/rec2100_pq/index.html index 1aeca3d10..a2db231f7 100644 --- a/colors/rec2100_pq/index.html +++ b/colors/rec2100_pq/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/rec709/index.html b/colors/rec709/index.html index 60257b33d..be9fb1e41 100644 --- a/colors/rec709/index.html +++ b/colors/rec709/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/rlab/index.html b/colors/rlab/index.html index f43c18287..58906a34f 100644 --- a/colors/rlab/index.html +++ b/colors/rlab/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/ryb/index.html b/colors/ryb/index.html index 1d6be065c..e22f13c81 100644 --- a/colors/ryb/index.html +++ b/colors/ryb/index.html @@ -1,15 +1,15 @@ RYB - ColorAide Documentation

RYB

The RYB color space is not registered in Color by default

Properties

Name: ryb

White Point: D65 / 2˚

Coordinates:

Name Range
r [0, 1]
y [0, 1]
b [0, 1]

The RYB color wheel.

The RYB color model is a subtractive color model in which red, yellow and blue pigments or dyes are added together in various ways to reproduce different colors. RYB is the classical way of thinking about colors. While in schools it is often still taught that red, yellow, and blue are the primary colors (for pigments), modern wisdom shows that these "primaries" are not sufficient. Spaces like CMYK which use cyan, magenta and yellow can create a much wider array of colors, even red, yellow, and blue. We've also since learned that the primary colors of light (which operates in an additive color space) are red, green, and blue.

Even though in modern day electronic screens and printing we rely on color models such as RGB and CMYK, RYB still holds a special place with artists. Color harmony is often used as the model when talking about color harmonies.

There is no standard RYB color model. No standard primaries. The RYB model that ColorAide implements is based on the work of Nathan Gossett and Baoquan Chen at the University of Minnesota at Twin Cities. Their paper devised an approach of using trilinear interpolation to create a transform from RYB to sRGB. The bases of the model uses colors similar to Johannes Itten's color wheel (which we showed an example of above), but in all the literature, there are variants of the same color wheel and no precise color codes.

ColorAide implements Gossett and Chen's algorithm to convert from RYB to sRGB, but also uses an algorithm that employs Newton's method to approximates the reverse transform. Additionally, their paper implements an easing function that biases the color transforms to corners of the cube. While we've also implemented this behavior to be true to the paper, the color spaces does not utilize this biasing by default, but we provide an alternative version does. The biasing does not improve the color space, but limits the intermediate colors, essentially clumping them near the corners of the RYB color cube which was a requirement for the types of data representations that they were performing.

Notice how the colors in the "biased" RYB are more concentrated at the corners while in the normal RYB they blend more.

RYB

RYB

While precisely emulating paint mixing was not entirely the focus, the RYB space does do a better job than other color spaces.

>>> Color.interpolate(['color(--ryb 0 0 1)', 'color(--ryb 0 1 0)'], space='ryb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5be2f10>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112e42290>
 >>> Color.interpolate(['color(--ryb 1 0 0)', 'color(--ryb 0 1 0)'], space='ryb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5a248d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d85dd0>
 >>> Color.interpolate(['color(--ryb 1 0 1)', 'color(--ryb 0 1 0)'], space='ryb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab7108ad0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112ba9950>
 

It should also be noted that when mixing all the colors, you do not get black, but a muddy brown, much like with paint. To be precise, the color black is not defined within this color space.

>>> Color.interpolate(['color(--ryb 0 0 0)', 'color(--ryb 1 1 1)'], space='ryb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab650dd10>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128f5850>
 >>> Color.interpolate(['color(--ryb 0 0 1)', 'color(--ryb 1 1 1)'], space='ryb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5db4c10>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c661d0>
 

It should be noted that the RYB model does a great job at translating colors within the RYB color gamut, but translation of colors outside the gamut will have poor conversions.

Learn more.

Channel Aliases

Channels Aliases
r red
y yellow
b blue

Input/Output

RYB is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ryb:

color(--ryb r y b / a)  // Color function
 

The string representation of the color object and the default string output use the color(--ryb r y b / a) form.

>>> Color("ryb", [1, 0, 0])
@@ -24,9 +24,9 @@
 
 Color.register(RYB())
 

RYB Biased

The biased RYB from Gosset and Chen's paper can be used via the ryb-biased color space, CSS custom name --ryb-biased. It has the same channel ranges as ryb, but conversions will be biased to the corners of the RYB cube.

>>> Color.interpolate(Color('ryb', [1, 0, 0]).harmony('wheel', space='ryb'), space='ryb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5d6d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1126fc650>
 >>> Color.interpolate(Color('ryb-biased', [1, 0, 0]).harmony('wheel', space='ryb-biased'), space='ryb-biased')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5b543d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112f5ef50>
 

Space can be registered via:

from coloraide import Color as Base
 from coloraide.spaces.ryb import RYBBiased
@@ -37,4 +37,4 @@
 

Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/srgb/index.html b/colors/srgb/index.html index 5e3d640c4..eaa466396 100644 --- a/colors/srgb/index.html +++ b/colors/srgb/index.html @@ -31,4 +31,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/srgb_linear/index.html b/colors/srgb_linear/index.html index 7b6e54ba3..70e1802bf 100644 --- a/colors/srgb_linear/index.html +++ b/colors/srgb_linear/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/ucs/index.html b/colors/ucs/index.html index 71d36f8db..ee244d9f6 100644 --- a/colors/ucs/index.html +++ b/colors/ucs/index.html @@ -15,4 +15,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/xyb/index.html b/colors/xyb/index.html index 50caa2e72..a5eb152ba 100644 --- a/colors/xyb/index.html +++ b/colors/xyb/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/xyy/index.html b/colors/xyy/index.html index c60a9fbbf..cf2738cac 100644 --- a/colors/xyy/index.html +++ b/colors/xyy/index.html @@ -13,4 +13,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/xyz_d50/index.html b/colors/xyz_d50/index.html index 333107963..e34962108 100644 --- a/colors/xyz_d50/index.html +++ b/colors/xyz_d50/index.html @@ -14,4 +14,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/colors/xyz_d65/index.html b/colors/xyz_d65/index.html index 4db1cd93a..825853d5f 100644 --- a/colors/xyz_d65/index.html +++ b/colors/xyz_d65/index.html @@ -15,4 +15,4 @@
Last update: September 10, 2023
\ No newline at end of file +
Last update: September 10, 2023
\ No newline at end of file diff --git a/compositing/index.html b/compositing/index.html index 4ec6037be..6a98a612c 100644 --- a/compositing/index.html +++ b/compositing/index.html @@ -319,4 +319,4 @@
Last update: March 31, 2023
\ No newline at end of file +
Last update: March 31, 2023
\ No newline at end of file diff --git a/contrast/index.html b/contrast/index.html index 651dd50b6..1b4c7894b 100644 --- a/contrast/index.html +++ b/contrast/index.html @@ -50,4 +50,4 @@
Last update: June 25, 2023
\ No newline at end of file +
Last update: June 25, 2023
\ No newline at end of file diff --git a/demos/index.html b/demos/index.html index a67082d0e..36e53d665 100644 --- a/demos/index.html +++ b/demos/index.html @@ -1,4 +1,4 @@ ColorAide Demos - ColorAide Documentation

ColorAide Demos

Online Color Picker

Color Picker

Use ColorAide to pick a color in any of the color spaces available. ACES color spaces have been arbitrarily limited has they have ginormous headroom.

Try it out

Interactive 3D Color Space Models

3D Color Models

Generate interactive 3D color models in the browser using ColorAide and Plotly! Most color spaces are supported, but color spaces with more than 3 color components (not including alpha) are not supported. Colors can be generated in a number of color gamuts, though a few models are restricted to their own color space for practical reasons.

Try it out


Last update: April 15, 2023
\ No newline at end of file +
Last update: April 15, 2023
\ No newline at end of file diff --git a/distance/index.html b/distance/index.html index b9e1a2fd2..688c9f497 100644 --- a/distance/index.html +++ b/distance/index.html @@ -6,7 +6,7 @@ Color("red").distance("blue", space="lab")

Delta E

The delta_e function gives access to various ∆E implementations, which are just different algorithms to calculate distance. Some are simply Euclidean distance withing a certain color space, some are far more complex.

If no method is specified, the default implementation is ∆E*ab (CIE76) which uses a simple Euclidean distancing algorithm on the CIELab color space. It is fast, but not as accurate as later iterations of the algorithm as CIELab is not actually as perceptually uniform as it was thought when CIELab was originally developed.

>>> Color("red").delta_e("blue")
 176.3084955965824
 

When method is set, the specified ∆E algorithm will be used instead. For instance, below we use ∆E*00 which is a more complex algorithm that accounts for the CIELab's weakness in perceptually uniformity. It does come at the cost of being a little slower.

>>> Color("red").delta_e("blue", method="2000")
-52.87819528592645
+52.878195285926445
 

Distancing and Symmetry

It should be noted that not all distancing algorithms are symmetrical. Some are order dependent.

Delta E CIE76

The ∆Eab distancing algorithm is registered in Color by default

Delta E Symmetrical Name Parameters
∆E*ab (CIE76) 76 space='lab-d65'

One of the first approaches to color distancing and is actually just Euclidean distancing in the CIELab color space.

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color("red").delta_e("blue", method="76")
 176.3084955965824
 >>> Color("red").delta_e("blue", method="76", space='lab')
@@ -23,9 +23,9 @@
 73.82677591958294
 

Delta E CIEDE2000

The ∆E*00 distancing algorithm is registered in Color by default

Delta E Symmetrical Name Parameters
∆E*00 (CIEDE2000) 2000 kl=1, kc=1, kh=1, space='lab-d65'

Since the 1994 definition did not adequately resolve the perceptual uniformity issue, the CIE refined their definition, adding five corrections:

  • A hue rotation term (RT), to deal with the problematic blue region (hue angles in the neighborhood of 275°)
  • Compensation for neutral colors (the primed values in the LCh differences)
  • Compensation for lightness (SL)
  • Compensation for chroma (SC)
  • Compensation for hue (SH)

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color("red").delta_e("blue", method="2000")
-52.87819528592645
+52.878195285926445
 >>> Color("red").delta_e("blue", method="2000", space='lab')
-55.79977339019779
+55.79977339019778
 

Delta E HyAB

The ∆EHyAB distancing algorithm is registered in Color by default

Delta E Symmetrical Name Parameters
∆EHyAB (HyAB) hyab space="lab-d65"

A combination of a Euclidean metric in hue and chroma with a city‐block metric to incorporate lightness differences. It can be used on any Lab like color space, the default being CIELab D65.

Delta E OK

The ∆Eok distancing algorithm is registered in Color by default

Delta E Symmetrical Name Parameters
∆Eok ok scalar=1

A color distancing algorithm that performs Euclidean distancing in the Oklab color space. This is used in the OkLCh Chroma gamut mapping algorithm. The scalar parameter allows you to scale the result up if desired.

Delta E ITP

The ∆Eitp distancing algorithm is not registered in Color by default

Delta E Symmetrical Name Parameters
∆Eitp (ICtCp) itp scalar=720

Various algorithms are designed for and perform decently in the SDR range, but ∆Eitp aims to provide good distancing in the HDR range using the ICtCp color space (must be registered in order to use ∆Eitp). It was determined that a scalar of 240 was more comparable to the average ∆E*00 result from the JND data set and 720 equates them to a JND.

Both the ICtCp color space and the ∆E algorithm must be registered to use.

from coloraide import Color as Base
 from coloraide.distance.delta_e_itp import DEITP
@@ -73,9 +73,9 @@
 ... 
 >>> Color.register(DE2000(space='lab'), overwrite=True)
 >>> Color('red').delta_e('blue', method='2000')
-55.79977339019779
+55.79977339019778
 >>> Color('red').delta_e('blue', method='2000', space='lab-d65')
-52.87819528592645
+52.878195285926445
 

Last update: September 11, 2023
\ No newline at end of file +
Last update: September 11, 2023
\ No newline at end of file diff --git a/filters/index.html b/filters/index.html index 18c92e490..29dbcd49c 100644 --- a/filters/index.html +++ b/filters/index.html @@ -561,4 +561,4 @@
Last update: June 14, 2023
\ No newline at end of file +
Last update: June 14, 2023
\ No newline at end of file diff --git a/gamut/index.html b/gamut/index.html index a44767c70..d263881f6 100644 --- a/gamut/index.html +++ b/gamut/index.html @@ -140,4 +140,4 @@ color.in_pointer_gamut()

Tip

Much like in_gamut(), in_pointer_gamut() allows adjusting tolerance as well via the tolerance parameter.


Last update: September 11, 2023
\ No newline at end of file +
Last update: September 11, 2023
\ No newline at end of file diff --git a/harmonies/index.html b/harmonies/index.html index 71ded7dd6..03eb80c6a 100644 --- a/harmonies/index.html +++ b/harmonies/index.html @@ -46,4 +46,4 @@ Steps(Custom('red').harmony('split'))

Warning

Remember that every color space is different. Some may rotate hues in a different direction and some may just not be very compatible for extracting harmonies from.

Additionally, a color space may not handle colors beyond its gamut well, for such color spaces, it is important to work within that spaces gamut opposed to picking colors outside of the gamut and relying on gamut mapping.


Last update: September 1, 2023
\ No newline at end of file +
Last update: September 1, 2023
\ No newline at end of file diff --git a/index.html b/index.html index bf7322bed..cd94477fa 100644 --- a/index.html +++ b/index.html @@ -6,4 +6,4 @@
Last update: June 13, 2023
\ No newline at end of file +
Last update: June 13, 2023
\ No newline at end of file diff --git a/interpolation/index.html b/interpolation/index.html index 10c283acb..362aaec71 100644 --- a/interpolation/index.html +++ b/interpolation/index.html @@ -10,16 +10,16 @@ ["rebeccapurple", "lch(85% 100 85)"], space='lch' )

Piecewise Interpolation

Piecewise interpolation takes the idea of linear interpolation and then applies it to multiple colors. As drawing a straight line through a series of points greater than two can be difficult to achieve, piecewise interpolation creates straight lines between each color in a chain of colors.

Piecewise Interpolation

When the interpolate method receives more that two colors, the interpolation will utilize piecewise interpolation and interpolation will be broken up between each pair of colors. The function, just like when interpolating between two colors, still operates by default in the domain of [0, 1], only it will now apply to the entire range of colors.

Piecewise interpolation simply breaks up a series of data points into segments in order to apply interpolation individually on each segment.

>>> Color.interpolate(['black', 'red', 'white'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d7fb10>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bff010>
 

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 at these locations.

Continuous Interpolation

Continuous interpolation is registered in Color by Default

In this document, we use the term "continuous" in two ways when talking about interpolation: continuous vs discrete and the interpolation method whose literal name is continuous.

The interpolate method only creates continuous interpolations, meaning that for any point along the interpolation line, you will get a unique color. Continuous interpolation in this sense directly contrasts with with discrete interpolation which provides quantized color results where multiple inputs are associated with a limited set of colors along the interpolation line.

The continuous interpolation method is simply a piecewise, linear interpolation method that interpolates defined channels continuously across one more undefined channels.

Normal, piecewise interpolation only considers a single segments under interpolation at a time. When channels are undefined, the undefined channel on one end of a segment will adopt the value of the other defined channel on the other side of the segment, and if that other channel is also undefined, then any color interpolated between the two will also have no defined value for the channel. This approach to interpolation never considers any context beyond the segment it is looking at.

The continuous interpolation method is a linear piecewise approach created for ColorAide that will actually interpolate through undefined channels, using context from all the colors to be interpolated. What this means is that if you have multiple colors, and one or more of the colors have the same channel undefined, the colors with that channel defined will have those values interpolated across the undefined gaps across all the segments. This is probably better illustrated with an example.

In this example, we have 3 colors. The end colors both define lightness, but the middle color is undefined. We can see when we use normal, linear piecewise interpolation that we get a discontinuity. But with continuous linear interpolation, we get a smooth interpolation of the lightness through the undefined channel.

>>> colors = [
 ...     Color('oklab', [0, 0, 0]),
 ...     Color('oklab', [NaN, -0.03246, -0.31153]),
 ...     Color('oklab', [1, 0, 0])
 ... ]
 >>> Color.interpolate(colors, space='oklab', method='linear')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c38490>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b77ed0>
 >>> Color.interpolate(colors, space='oklab', method='continuous')
-<coloraide.interpolate.continuous.InterpolatorContinuous object at 0x7fbab5cc6d90>
+<coloraide.interpolate.continuous.InterpolatorContinuous object at 0x112bcf950>
 

Cubic Spline Interpolation

Linear interpolation is nice because it is easy to implement, and due to its straight forward nature, pretty fast. With that said, it doesn't always have the smoothest transitions. It turns out that there are other piecewise ways to interpolate that can yield smoother results.

Inspired by some efforts seen on the web and in the great JavaScript library Culori, ColorAide implements a number of spline based interpolation methods.

Because splines require taking into account more than two colors at a time, all spline based interpolation methods are built off of the continuous interpolation approach of handling undefined values.

B-Spline

B-Spline interpolation is registered in Color by Default

B-spline

B-spline is a piecewise spline similar to Bezier curves. It utilizes "control points" that help shape the interpolation path through a series of colors. Like Bezier Curves, the path does not pass through the control points, but it is clamped at the start and end. Essentially, the interpolation path passes through both end colors and bends that path along the way towards the other colors being used as control points.

It can be used by specifying bspline as the interpolation method.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='bspline')
-<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x7fbab5ee1850>
+<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x112c8f0d0>
 

Natural

Natural interpolation is registered in Color by Default

Natural

The "natural" spline is the same as the B-spline approach except an algorithm is applied that uses the colors as data points and calculates new control points such that the interpolation passes through all the data points. This means that the path will pass through all the colors. The resultant spline has the continuity and properties of a natural spline, hence the name.

One down side is that it can overshoot or undershoot a bit, and can occasionally cause the interpolation path to pass out of gamut if interpolating on an edge.

It can be used by specifying natural as the interpolation method.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='natural')
-<coloraide.interpolate.bspline_natural.InterpolatorNaturalBSpline object at 0x7fbab5d62210>
+<coloraide.interpolate.bspline_natural.InterpolatorNaturalBSpline object at 0x112895e50>
 

Monotone

Monotone interpolation is registered in Color by Default

Monotone

The "monotone" spline is a piecewise interpolation spline that passes through all its data points and helps to preserve monotonicity. As far as we are concerned, the important thing to note is that it greatly reduces any overshoot or undershoot in the interpolation.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='monotone')
-<coloraide.interpolate.monotone.InterpolatorMonotone object at 0x7fbab709a750>
+<coloraide.interpolate.monotone.InterpolatorMonotone object at 0x1124df190>
 

Catmull-Rom

Catmull-Rom interpolation is not registered in Color by Default

Catmull-Rom

Lastly, the Catmull-Rom spline is another "interpolating" spline that passes through all of its data points, similar to the "natural" spline, but it but does not share the same continuity and properties of a "natural" spline.

Much like the "natural" spline, it can overshoot or undershoot.

Catmull-Rom is not registered by default, but can be registered as shown below and then used by specifying catrom as the interpolation method.

>>> from coloraide import Color
 >>> from coloraide.interpolate.catmull_rom import CatmullRom
 >>> class Custom(Color): ...
 ... 
 >>> Custom.register(CatmullRom())
 >>> Custom.interpolate(['red', 'green', 'blue', 'orange'], method='catrom')
-<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x7fbab5e035d0>
+<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x112c39e50>
 

Discrete Interpolation

New 2.5

So far, we've only shown examples of continuous interpolation methods. To clarify, we are using "continuous" in a slightly different way than we discussed earlier. When we say "continuous" here, we simply mean that the colors in the interpolation smoothly transition from one color to the other. But when creating charts or graphs, some times you'd like to categorize data such that a range of values correspond to a specific color. For this, we can use discrete, which like intrpolate, returns an interpolation object, but the the ranges will be discrete.

By default, ranges are calculated directly form the input colors. So if you had three colors, the interpolation would be broken up into 3 ranges. Compare this with the the "continuous" interpolation we methods we showed earlier.

>>> Color.discrete(['red', 'green', 'blue'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d71ed0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b8fbd0>
 >>> Color.interpolate(['red', 'green', 'blue'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d575d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bcff90>
 

If we specify step, we can create a larger or smaller color scale using the input colors to interpolate the new color scale. And we can use any of the aforementioned interpolation methods to help generate this new discrete scale.

>>> Color.discrete(['red', 'green', 'blue'], steps=5, method='catrom')
-<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x7fbab5f1e890>
+<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x112ba3f10>
 

What makes this really useful is if you combine it with custom domains to process data. By default, the domain is [0, 1], but we can change this to directly correlate the data with our quantized color samples. For instance, let's use a series of discrete colors to represent temperature. Additionally, let's use domain to associate a temperature ranges with the given colors. Now when we input a temperature value, it will align with our discrete color scale.

>>> i = Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'], domain=[-32, 32, 60, 85, 95])
 >>> i(-32)
 color(--oklab 0.45201 -0.03246 -0.31153 / 1)
@@ -78,16 +78,16 @@
 >>> i(100)
 color(--oklab 0.62796 0.22486 0.12585 / 1)
 >>> i
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab6e799d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1116b5350>
 

Additionally, color scales can be limited using the padding parameter.

>>> Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dbeb50>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1124ece10>
 >>> Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'], padding=[0.25, 0])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5cb0790>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c489d0>
 

As discrete() is built on steps(), it can take all the same arguments. Check out steps() to learn more.

Hue Interpolation

In interpolation, hues are handled special allowing us to control the way in which hues are evaluated. By default, the shortest angle between two hues is targeted for interpolation, but the hue option allows us to redefine this behavior in a number of interesting ways: shorter, longer, increasing, decreasing, and specified. Below, we can see how the interpolation varies using shorter vs longer (interpolate between the longest angle).

>>> i = Color.interpolate(
 ...     ["lch(52% 58.1 22.7)", Color("lch(56% 49.1 257.1)").mask("hue", invert=True)],
@@ -117,7 +117,7 @@
 ...     space='lch',
 ...     hue="shorter"
 ... )
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d7e410>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112be5b50>
 

Interpolating with Alpha

Interpolating color channels is pretty straight forward and uses traditional linear interpolation logic, but when introducing transparency to a color, interpolation uses a concept known as premultiplication which alters the normal interpolation process.

Premultiplication is a technique that tends to produce better results when two colors have differing transparency. It essentially accounts for the transparency and uses it to weight how may a given color channel will contribute to the interpolation. A more transparent color's channels will naturally contribute less.

Consider the following example. Normally, when transitioning to a "transparent" color, the colors will be more gray during the transition. This is because transparent is actually black. But when using premultiplication, the transition looks just as one would expect as the transparent color's channels are weighted less due to the high transparency.

>>> Color.interpolate(['white', 'transparent'], space='srgb', premultiplied=False)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5eba890>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112883910>
 >>> Color.interpolate(['white', 'transparent'], space='srgb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec5010>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b97610>
 

As a final example, below we have an opaque orange and a blue that is quite transparent. Logically, the blue shouldn't have as big an affect on the overall color as it is so faint, and yet, in the un-premultiplied example, when mixing the colors equally, we see that the resultant color is also equally influenced by the hue of both colors. In the premultiplied example, we see that orange is still quite dominant at 50% as it is fully opaque.

>>> Color('orange').mix(Color('blue').set('alpha', 0.25), space='srgb', premultiplied=False)
 color(srgb 0.5 0.32353 0.5 / 0.625)
@@ -173,9 +173,9 @@
 color(srgb 0.8 0.51765 0.2 / 0.625)
 

If we interpolate it, we can see the difference in transition.

>>> Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb', premultiplied=False)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec37d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1117e5510>
 >>> Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c63bd0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e4a90>
 

There may be some cases where it is desired to use no premultiplication in alpha blending. One could simply be that you need to mimic the same behavior of a system that does not use premultiplied interpolation. If so, simply set premultiplied to False as shown above.

Mixing

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 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.

>>> Color("red").mix(Color("blue"))
 color(--oklab 0.53998 0.0962 -0.09284 / 1)
@@ -310,16 +310,16 @@
 ...     ["green", "blue"],
 ...     progress=ease_in
 ... )
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c80910>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c66510>
 >>> Color.interpolate(
 ...     ["green", "blue"]
 ... )
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed8b50>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c77bd0>
 >>> Color.interpolate(
 ...     ["green", "blue"],
 ...     progress=ease_out
 ... )
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5e750>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c835d0>
 

Additionally, easing functions can be injected inline which allows a user to control how easing is performed between specific sub-interpolations within piecewise interpolation.

>>> Color.interpolate(["red", "green", ease_out, "blue"])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5eba890>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128f7e50>
 

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 an exponential "ease in" to alpha while allowing all other channels to interpolate normally.

>>> ease_in_expo = cubic_bezier(0.950, 0.050, 0.795, 0.035)
 >>> Color.interpolate(
 ...     ["lch(50% 50 0)", "lch(90% 50 260 / 0.5)"],
@@ -341,7 +341,7 @@
 ...         'alpha': ease_in_expo
 ...     }
 ... )
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec3310>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c3a150>
 

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 within the domain of [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.

>>> from coloraide import stop
 >>> Color.interpolate(['orange', 'purple', 'green'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec5910>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e60d0>
 >>> Color.interpolate(['orange', stop('purple', 0.25), 'green'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c5b310>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128ef010>
 

Color stops follow the rules as laid out in the CSS spec.

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.

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.

>>> from coloraide import hint
 >>> Color.interpolate(['orange', 'purple', 'green'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec2410>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1124e37d0>
 >>> Color.interpolate(['orange', hint(0.75), 'purple', 'green'])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d314d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128fe810>
 

Padding

New 2.6

Particularly when interpolating a color scale, it can be useful to "resize" the area of the color scale being evaluated. This can generally be done using the padding parameter. Consider the following example using the ColorBrewer scale OrRd.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']
 >>> Color.interpolate(scale, space='srgb')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ee1690>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c062d0>
 >>> Color.discrete(scale, space='srgb', steps=5)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5eb5e50>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c74390>
 >>> Color.interpolate(scale, space='srgb', padding=0.25)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5c1d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112cb5ad0>
 >>> Color.discrete(scale, space='srgb', steps=5, padding=0.25)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d7eed0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112cb56d0>
 

Padding can be applied to both sides by specifying a single number, or it can be controlled per side by sending in a sequence of two values.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']
 >>> Color.discrete(scale, space='srgb', steps=5, padding=[0.25, 0])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed0d90>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128c3610>
 

Negative padding is allowed as well.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']
 >>> Color.discrete(scale, space='srgb', steps=5, padding=[-0.25, 0])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec73d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e5a10>
 

If the result extends past the limits, extrapolate needs to be enabled or the values will be clamped to the ends.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']
 >>> Color.discrete(scale, space='srgb', steps=5, padding=[1, 1])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5edbb10>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c5fb90>
 >>> Color.discrete(scale, space='srgb', steps=5, padding=[1, 1], extrapolate=True)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d85a10>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c5ea90>
 

Domains

By default, interpolation has an input domain of [0, 1]. This domain applies to an entire interpolation, even ones that span multiple colors. Generally, this is sufficient and can be used to generate color scales, mixes, and steps in any way that a user needs. When generating colors that should align with data, custom domains can be quite helpful.

For instance, associating colors with temperature.

>>> i = Color.interpolate(
@@ -420,7 +420,7 @@
 >>> i(89)
 color(--oklab 0.7268 0.12391 0.14717 / 1)
 >>> i
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5db4150>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112894910>
 

Lastly, it is important to note that this affects stops as well, mainly stops applied to interpolation endpoints. When an endpoint is moved inwards via a color stop, the end range of the interpolation is clamped, extending the star and end color. But when extrapolation is enabled, a color stop on an endpoint essentially moves the start and end interpolation. And since there are no other colors on either end to interpolate with, extrapolation occurs.

>>> Color.interpolate([stop('red', 0.25), stop('blue', 0.75)])
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5da6190>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128784d0>
 >>> Color.interpolate([stop('red', 0.25), stop('blue', 0.75)], extrapolate=True)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d33390>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b8d9d0>
 

Undefined/NaN Handling

Color spaces that have hue coordinates often have rules about when the hue is considered relevant. For instance, in the HSL color space, if saturation is zero, the hue is essentially powerless. This is because the color is "without color" or achromatic; therefore, the hue can have no affect on the actual color.

ColorAide will generally respect the values a user provides, so if an achromatic HSL color is given a hue of 270 degrees, ColorAide will accept it, but the hue will not affect the color in any meaningful way.

During conversions, such context is lost, and if an achromatic color is converted to the color space like HSL, the resultant color will have a hue that is noted as undefined. This is simply because there is no good hue for achromatic colors as they play no part in the color. Any hue is actually incorrect as achromatic colors have no real hue. Instead, colors will be returned with a value that represents that the hue is missing or undefined, or maybe better worded, could not be defined.

Many libraries, like d3-color, chroma.js, and color.js, represent null hues with NaN (not a number). This is usually done to make color interpolation easier. Some, like d3-color, are a bit more liberal with NaN and will target special cases that are above and beyond the normal rules to help ensure good interpolation. For instance, they not only mark hue undefined on HSL colors when saturation is zero, but they'll mark saturation as NaN when lightness indicates "black" or "white".

ColorAide also uses NaN, or in Python float('nan'), to represent undefined channels. In certain situations, when a hue is deemed undefined, the hue value will be set to coloraide.NaN, which is just a constant containing float('nan').

When performing linear interpolation, where only two color's channels are ever being evaluated together at a given time, if one color's channel has a NaN, the other color's channel will be used as the result. If both colors have a NaN for the same channel, then NaN will be returned.

Continuous NaN Handling

NaN handling is a bit different for the Continuous and Cubic Spline interpolation approaches. Linear only evaluates colors at a given time, while the others will take into consideration more than two colors. Because the context is much wider and more complicated, NaN values will often get context from both sides.

Notice that in this example, because white's saturation is zero, the hue is undefined. Because the hue is undefined, when the color is mixed with a second color (green), the hue of the second color is used.

>>> color = Color('white').convert('hsl')
 >>> color[:-1]
@@ -538,28 +538,28 @@
 

ColorAide, by default, expects the user to be aware that undefined values are lost if conversion is required for interpolation. This is mainly because the intent of the color can be changed during this process, but some users may find the automatic carrying-forward more convenient. For this reason, ColorAide has implemented carrying-forward as an optional feature via the carryforward option.

In this example, interpolating without carrying-forward results in an interpolation between a purplish color and white. Using carrying-forward, we get a purplish color with an undefined green channel. The green channel takes on the white's green channel giving us an interpolation between a more greenish color and white.

>>> Color.interpolate(['color(srgb 0.5 none 0.8)', 'white'], space='display-p3')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d867d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bfd750>
 >>> Color.interpolate(['color(srgb 0.5 none 0.8)', 'white'], space='display-p3', carryforward=True)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d720d0>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bcd5d0>
 

Depending on the color space, carrying-forward may have better or worse results.

The following table shows channel components supported for carryforward. Spaces may use different names for their channels, but if they are derived from the related space classes, their channels are supported. For instance, xyz is derived from RGBish, so x, y, and z is treated like super saturated r, g, and b.

Space Type Channel Equivalents
RGBish r, g, b
LABish l
LCHish l, c, h
HSLish h, s, l
HSVish h, s, v
Cylindrical h

Carrying-forward is applied within categories.

Category Components
Reds r
Greens g
Blues b
Lightness l
Colorfulness c, s
Hue h
Opponent a a
Opponent b b
Value v

Powerless Hues

Experimental

This feature is provided to give parity with CSS behavior. As the spec is still in flux, behavior is subject to change or feature could be removed entirely. Use at your own risk.

Normally, ColorAide respects the user's explicitly defined hues. This gives the user power to do things like masking off all channels but the hue to interpolate only the hue.

>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ee0090>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112683790>
 

But when doing this, a user must explicitly define the hue as achromatic if they want the hue to be ignored. Conversions of achromatic colors to a cylindrical space will, in most cases, have the hue automatically set to undefined.

>>> Color.interpolate(['oklch(1 0 0)', 'oklch(0.75 0.2 180)'], space='oklch')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ebac90>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bedad0>
 >>> Color.interpolate(['oklch(1 0 None)', 'oklch(0.75 0.2 180)'], space='oklch')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dd3190>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128c6750>
 

CSS has the concept of powerless hues which causes explicitly defined hues to be powerless (or act as undefined) when a color is considered achromatic. This means a user never has to think about achromatic hues, so even if the erroneously define a hue, they will automatically be treated as undefined when interpolating. ColorAide implements this behavior via the powerless option.

>>> Color.interpolate(['oklch(1 0 0)', 'oklch(0.75 0.2 180)'], space='oklch', powerless=True)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dd3350>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112682e50>
 >>> Color.interpolate(['oklch(1 0 None)', 'oklch(0.75 0.2 180)'], space='oklch', powerless=True)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dd1810>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c80c50>
 

The one downside is that control over the hue will be diminished to some degree as ColorAide will no longer respect a user's explicit hue if the color is determined to be achromatic.

>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified')
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed3310>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bec850>
 >>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified', powerless=True)
-<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5e450>
+<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bb2910>
 

Last update: August 24, 2023
\ No newline at end of file +
Last update: August 24, 2023
\ No newline at end of file diff --git a/manipulation/index.html b/manipulation/index.html index e9a15b82c..5e040b6bf 100644 --- a/manipulation/index.html +++ b/manipulation/index.html @@ -287,4 +287,4 @@ c.normalize()
Last update: June 13, 2023
\ No newline at end of file +
Last update: June 13, 2023
\ No newline at end of file diff --git a/playground/index.html b/playground/index.html index bca6b9853..58fab656f 100644 --- a/playground/index.html +++ b/playground/index.html @@ -1,4 +1,4 @@ Playground - ColorAide Documentation
\ No newline at end of file +
Last update: August 16, 2023
\ No newline at end of file diff --git a/plugins/cat/index.html b/plugins/cat/index.html index 711d30510..699c3c499 100644 --- a/plugins/cat/index.html +++ b/plugins/cat/index.html @@ -24,4 +24,4 @@
Last update: July 22, 2022
\ No newline at end of file +
Last update: July 22, 2022
\ No newline at end of file diff --git a/plugins/cct/index.html b/plugins/cct/index.html index d6db9f67c..a423108f3 100644 --- a/plugins/cct/index.html +++ b/plugins/cct/index.html @@ -25,4 +25,4 @@
Last update: June 23, 2023
\ No newline at end of file +
Last update: June 23, 2023
\ No newline at end of file diff --git a/plugins/contrast/index.html b/plugins/contrast/index.html index b508ea21b..efdb7e341 100644 --- a/plugins/contrast/index.html +++ b/plugins/contrast/index.html @@ -11,4 +11,4 @@
Last update: July 22, 2022
\ No newline at end of file +
Last update: July 22, 2022
\ No newline at end of file diff --git a/plugins/delta_e/index.html b/plugins/delta_e/index.html index 042dfec4c..5bf463cd6 100644 --- a/plugins/delta_e/index.html +++ b/plugins/delta_e/index.html @@ -11,4 +11,4 @@
Last update: July 22, 2022
\ No newline at end of file +
Last update: July 22, 2022
\ No newline at end of file diff --git a/plugins/filter/index.html b/plugins/filter/index.html index 907227818..9f7d861a3 100644 --- a/plugins/filter/index.html +++ b/plugins/filter/index.html @@ -13,4 +13,4 @@
Last update: February 17, 2023
\ No newline at end of file +
Last update: February 17, 2023
\ No newline at end of file diff --git a/plugins/fit/index.html b/plugins/fit/index.html index 77fe4c199..41b2b3332 100644 --- a/plugins/fit/index.html +++ b/plugins/fit/index.html @@ -12,4 +12,4 @@

Reserved Name

clip is a special, reserved name and the associated plugin cannot be overridden. Another clip plugin can be written, but it cannot override the original.


Last update: March 7, 2023
\ No newline at end of file +
Last update: March 7, 2023
\ No newline at end of file diff --git a/plugins/index.html b/plugins/index.html index 573dfdd4c..6d5198960 100644 --- a/plugins/index.html +++ b/plugins/index.html @@ -1,4 +1,4 @@ ColorAide Plugins - ColorAide Documentation

ColorAide Plugins

ColorAide implements extendable portions of the Color object as plugins. This makes adding things such as new ∆E methods or even new color spaces quite easy. Currently, ColorAide implements the following areas as plugins:

While these documents will touch on each plugin, looking at the source code will provide a better view on how plugins are actually used as all functionality for all of these categories are implemented as plugins in ColorAide.


Last update: June 13, 2023
\ No newline at end of file +
Last update: June 13, 2023
\ No newline at end of file diff --git a/plugins/interpolate/index.html b/plugins/interpolate/index.html index 01dd88c1f..fdce49e8a 100644 --- a/plugins/interpolate/index.html +++ b/plugins/interpolate/index.html @@ -38,4 +38,4 @@

__init__ usually shouldn't be changed as it handles the general initialization for all interpolations. It could be extended with super() to set some class specific initialization flags for specific features, but generally, interpolation specific setup logic should be done in Interpolator.setup(). This is often used to restructure data points to a more agreeable format for a given interpolation method, precalculate premultiplication, or normalize undefined values when required. There are cases where ColorAide may update data points and re-call setup() directly. As an example. setup() can be recalled when a continuous interpolation is converted to a discretized one.

Interpolator.interpolate is where the actual interpolation takes place. It expects an index from [1, n], the index referencing the second color out of the two colors to be interpolated. point, usually between [0, 1], represents the point on the interpolation line between the two colors under evaluation.

While point is usually a value between [0, 1], where 0 would be the color stop to the left, and 1 would be the color stop to the right, if point exceeds the range of [0, 1], it can be assumed that the request is on the far left or far right of all color stops and could be beyond the absolute range of the entire color interpolation chain.

By default, extrapolation is disabled between all colors in an interpolation chain, and any point that exceeds the range of [0, 1], before easing functions are applied, will be clamped. If extrapolate is set to $!py True, the points will not be clamped between any colors, in which case, it is an easing functions responsibility to ensure a value between 0 or 1 if extreme values are not desired.

Check out the source code to see some example plugins.


Last update: July 25, 2023
\ No newline at end of file +
Last update: July 25, 2023
\ No newline at end of file diff --git a/plugins/space/index.html b/plugins/space/index.html index f2e75c48b..c3903bef9 100644 --- a/plugins/space/index.html +++ b/plugins/space/index.html @@ -321,4 +321,4 @@

As all ColorAide color spaces are defined as plugins, there should be ample examples to help someone start writing a new color space.


Last update: August 21, 2023
\ No newline at end of file +
Last update: August 21, 2023
\ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json index 2bd2740ae..33cfd1b75 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Introduction","text":""},{"location":"#what-is-coloraide","title":"What is ColorAide?","text":"

ColorAide is a pure Python, object oriented approach to colors.

>>> from coloraide import Color\n>>> Color.steps(['lch(75% 50 0)', 'lch(75% 50 300)'], steps=8, space='lch', hue='longer')\n[color(--lch 75 50 0 / 1), color(--lch 75 50 42.857 / 1), color(--lch 75 50 85.714 / 1), color(--lch 75 50 128.57 / 1), color(--lch 75 50 171.43 / 1), color(--lch 75 50 214.29 / 1), color(--lch 75 50 257.14 / 1), color(--lch 75 50 300 / 1)]\n

ColorAide particularly has a focus on the following:

  • Accurate colors.

  • Proper round tripping (where reasonable).

  • Be generally easy to pick up for the average user.

  • Support modern CSS color spaces and syntax.

  • Make accessible many new and old non-CSS color spaces.

  • Provide a number of useful utilities such as interpolation, color distancing, blending, gamut mapping, filters, correlated color temperature, color vision deficiency simulation, color harmonies, etc.

  • Provide a plugin API to extend supported color spaces and approaches to various utilities.

  • Allow users to configure defaults to their liking.

ColorAide is not meant to be the one library to replace all other color libraries. There are many great libraries out there such such as: Colour Science, Colorio, Python Color Math, and many others. Some focus on the scientific aspects of colors and provide a wealth of various spaces, illuminants, access to complex color space visualizers, and numerous esoteric tools. Some are highly focused on speed. Some are powerful, but can be more complex to pick up by the average user.

At its heart, ColorAide was designed for convenience, flexibility, and to be very easy to pick up and work with. There are, of course, some trade offs with speed when using a pure Python, object oriented approach, but there are also many advantages as well. ColorAide might not always be the tool for every job, but hopefully it is a great tool all the same.

"},{"location":"#installation","title":"Installation","text":"

ColorAide can be installed via Python's pip:

$ pip install coloraide\n
"},{"location":"advanced/","title":"Advanced Topics","text":"

Colors are complicated, and sometimes it may not be understood why colors or color transformations yield the results that they do. Here we'd like to cover more advanced or specific topics that don't fit well in existing topics or are too verbose to be included elsewhere.

"},{"location":"advanced/#round-trip-accuracy","title":"Round Trip Accuracy","text":"

In general, ColorAide is careful to provide good round trip conversions where practical. What this means is that we try to maintain a high level of accuracy so that when a color is converted to a different color and back that it will be very close, if not exactly, the same.

In general, we are able to keep decent round tripping by not not clipping values during conversion and maintaining as high a level of precision as we can, but there are some cases where the high level of round trip accuracy cannot be maintained, or even at all. There are even reasons where we willfully choose to sacrifice some accuracy for convenience in order to uphold intuitive expectations for the user.

If you are a color scientist or you work in certain industries, there are definite reasons to uphold accuracy at all costs, but sometimes, you just want the colors to do the what you expect them to do. ColorAide tries to live in the space between. We try to provide accurate color round tripping except when it comes at the cost of practicality.

"},{"location":"advanced/#limitations-of-the-color-space","title":"Limitations of The Color Space","text":"

One situation that can affect round tripping is when one color model cannot properly handle a color due to its gamut being beyond the conversion algorithm's capabilities.

Consider a wide gamut, HDR color space like Jzazbz. Jzazbz is an unbounded color space with plenty of headroom for HDR. Now, let's compare it to HSLuv, an SDR color space derived from the Luv color space and confined to the sRGB gamut. It is essentially a more perceptually uniform version of HSL, but the algorithm specifically requires lightness to be clamped to the SDR range. If we convert an HDR color from Jzazbz to HSLuv, round trip will be broken as the color space simply does not support the HDR range.

>>> jz = Color('color(--jzazbz 0.25 0 0)')\n>>> jz\ncolor(--jzazbz 0.25 0 0 / 1)\n>>> hsluv = jz.convert('hsluv')\n>>> hsluv\ncolor(--hsluv none 0 100 / 1)\n>>> hsluv.convert('jzazbz')\ncolor(--jzazbz 0.22207 -0.00016 -0.00012 / 1)\n
If a color space algorithm does not support a specific color, the conversion may be clamped or come back with an unexpected value.

"},{"location":"advanced/#floating-point-math","title":"Floating Point Math","text":"

Floating point math can also be responsible for some differences in round tripping. Floating point issues are not specific to this library or even the language of Python, but to all computers in general. For example, computers cannot store infinite repeating decimals to properly represent all floating point numbers.

What this means is that no matter how much floating point precision you maintain, some error is introduced when doing floating point operations. Certain rounding conventions are used in order to average out the errors to stay as close as possible to the intended, real value, but it does not prevent floating point errors. This is simply the nature of computers and floating point math.

>>> color = Color('white')\n>>> color[:]\n[1.0, 1.0, 1.0, 1.0]\n>>> color.convert('prophoto-rgb').convert('srgb')[:]\n[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]\n
"},{"location":"advanced/#special-handling-cylindrical-spaces","title":"Special Handling: Cylindrical Spaces","text":"

Sometimes, round trip accuracy can be compromised further for practical reasons. A common case where we make compromises is with cylindrical color models.

ColorAide aims to make colors easy to use, but the one case that can frustrate users is interpolating with an achromatic color using a cylindrical color space.

Achromatic colors do not have a hue, but all conversions end up yielding something for hue, even it it has no practical meaning. This can cause odd color shifts when interpolating with an achromatic color. In order to get logical results when doing interpolation, we detect when a color is achromatic (or very close to achromatic) and set the hues to undefined. This helps us to identify achromatic cases and helps us to prevent weird color shifts when interpolating between achromatic colors. Only if a user manually defines a hue do we respect it.

>>> Color.interpolate(['lch(75 100 180)', 'lch(75 0 0)'], space='lch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d54a50>\n>>> Color.interpolate(['lch(75 100 180)', 'lch(75 0 none)'], space='lch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5d850>\n

Because of floating point issues, conversions to cylindrical color spaces do not always satisfy the requirements to be recognized as achromatic colors.

As an example, HSL colors are achromatic when the sRGB color it is derived from has all color channels equal to each other. Let's say we convert the color darkgray to the XYZ D65 color space and then back again. We can see that what was once a color with all color channels equal to each other is now a color that has color channels very nearly equal to each other.

>>> c1 = Color('darkgray')\n>>> c1[:-1]\n[0.6627450980392157, 0.6627450980392157, 0.6627450980392157]\n>>> c2 = c1.convert('xyz-d65').convert('srgb')\n>>> c2[:-1]\n[0.6627450980392157, 0.6627450980392156, 0.6627450980392156]\n

These two colors are intended to be the same, but one satisfies the requirement to have the HSL hue set to NaN, but the other does not. This is a case where accuracy vs practicality comes into play. We all know the color is essentially still darkgray, and that is what the user intends. To allow this to work seamlessly, we apply a little leniency to the achromatic rules and state that if the color is very, very close to being achromatic, we will consider it achromatic, and we sacrifice a little accuracy to gain practicality. Or maybe it is better to say that we compensate for the natural inaccuracies that exist.

>>> Color('darkgray').convert('hsl')[:-1]\n[nan, 0.0, 0.6627450980392157]\n>>> Color('darkgray').convert('xyz-d65').convert('hsl')[:-1]\n[nan, 3.2919403637141254e-16, 0.6627450980392156]\n

This problem can exist in various scenarios in pretty much all cylindrical color spaces. Some have tighter algorithms and may give really good results with sRGB, but then when converting from some other color space we'll see maybe not as tight a translation to and from.

Additionally, some color spaces have very dynamic achromatic responses, as an interesting example, let's consider CAM16 JMh. This color space actually has its lower limit for achromatic colors gradually rise higher and higher as lightness increases. Not only that, the achromatic line actually passes mainly through hue ~209.5 for most achromatic colors lighter than black.

>>> Color('color(srgb 0 0 0)').convert('cam16-jmh', norm=False)[:]\n[0.0, 0.0, 0.0, 1.0]\n>>> Color('color(srgb 0.5 0.5 0.5)').convert('cam16-jmh', norm=False)[:]\n[42.841343691205466, 1.4635354939944947, 209.5351055763844, 1.0]\n>>> Color('color(srgb 1 1 1)').convert('cam16-jmh', norm=False)[:]\n[99.99999999999997, 2.236898445770595, 209.53333446353506, 1.0]\n

This can make it hard to specify a simple chroma check for achromatic colors. Simply lowering the chroma or changing the hue can make the color no longer achromatic.

>>> white = Color('color(srgb 1 1 1)')\n>>> white.convert('cam16-jmh', norm=False).convert('srgb').to_string(hex=True)\n'#ffffff'\n>>> white.convert('cam16-jmh', norm=False).set('h', 0).convert('srgb').to_string(hex=True)\n'#fffdfe'\n>>> white.convert('cam16-jmh', norm=False).set('m', 0.0).convert('srgb').to_string(hex=True)\n'#fffefd'\n

For these types of color spaces, ColorAide will map the achromatic response with a spline and use it as a reference to give detect achromatic values for undefined chroma and hue.

>>> Color('cam16-jmh', [100, NaN, NaN]).convert('srgb').to_string(hex=True)\n'#ffffff'\n>>> Color('cam16-jmh', [50, NaN, NaN]).convert('srgb').to_string(hex=True)\n'#919191'\n>>> Color('cam16-jmh', [20, NaN, NaN]).convert('srgb').to_string(hex=True)\n'#424242'\n

Depending on how well we can fit the achromatic response, the better the accuracy, but we do purposely allow some wiggle room to ensure we can capture achromatic colors within the threshold of the spline's accuracy. This can introduce some loss of accuracy, but makes working with achromatic colors in difficult spaces like CAM16 JMh more reasonable.

>>> gray = Color('cam16-jmh', [50, NaN, NaN])\n>>> gray.normalize()\ncolor(--cam16-jmh 50 1.5823 none / 1)\n>>> Color.interpolate([gray, 'green'], space='cam16-jmh')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5f1d890>\n
"},{"location":"average/","title":"Color Averaging","text":"

Color averaging is the process of calculating an average color from a set of other colors by taking the mean of each color channel.

Averaging under ColorAide can take as many colors as desired and will return a color that represents the average. This is not to be confused with interpolation which employs a different technique, but in certain situations, it can sort of function like mixing multiple colors.

"},{"location":"average/#rectangular-space-averaging","title":"Rectangular Space Averaging","text":"

ColorAide, by default, averages in rectangular color spaces, the default being Linear sRGB. If desired, other color spaces can be used, such as perceptually uniform spaces like Oklab.

>>> Color.average(['red', 'blue'])\ncolor(srgb-linear 0.5 0 0.5 / 1)\n>>> Color.average(['red', 'blue'], space='srgb')\ncolor(srgb 0.5 0 0.5 / 1)\n>>> Color.average(['red', 'blue'], space='oklab')\ncolor(--oklab 0.53998 0.0962 -0.09284 / 1)\n

Averaging is not restricted to any certain amount of colors.

>>> Color.average(['red', 'yellow', 'orange', 'green'])\ncolor(srgb-linear 0.75 0.39803 0 / 1)\n
"},{"location":"average/#cylindrical-space-averaging","title":"Cylindrical Space Averaging","text":"

ColorAide can average colors in rectangular spaces and cylindrical spaces. When applying averaging in a cylindrical space, hues will be averaged taking the circular mean.

Cylindrical averaging may not provide as good of results as using rectangular spaces, but is provided to provide a sane approach if a cylindrical space is used.

>>> Color.average(['orange', 'yellow', 'red'])\ncolor(srgb-linear 1 0.45875 0 / 1)\n>>> Color.average(['orange', 'yellow', 'red'], space='hsl')\ncolor(--hsl 33.227 1 0.5 / 1)\n

Because calculations are done in a cylindrical space, the averaged colors can be different than what is acquired with rectangular space averaging.

>>> Color.average(['purple', 'green', 'blue'])\ncolor(srgb-linear 0.07195 0.07195 0.40529 / 1)\n>>> Color.average(['purple', 'green', 'blue'], space='hsl')\ncolor(--hsl -120 1 0.33399 / 1)\n
"},{"location":"average/#averaging-with-transparency","title":"Averaging with Transparency","text":"

ColorAide, by default, will account for transparency when averaging colors. Colors which are more transparent will have less of an impact on the average. This is done by premultiplying the colors before averaging.

>>> Steps([Color('darkgreen'), Color('color(srgb 0 0.50196 0 / 1)'), Color('color(srgb 0 0 1)')])\n[color(srgb 0 0.39216 0 / 1), color(srgb 0 0.50196 0 / 1), color(srgb 0 0 1 / 1)]\n>>> for i in range(12):\n...     Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'])\n... \ncolor(srgb-linear 0 0.06372 0.5 / 0.66667)\ncolor(srgb-linear 0 0.07033 0.47826 / 0.69697)\ncolor(srgb-linear 0 0.0764 0.45833 / 0.72727)\ncolor(srgb-linear 0 0.08198 0.44 / 0.75758)\ncolor(srgb-linear 0 0.08713 0.42308 / 0.78788)\ncolor(srgb-linear 0 0.09189 0.40741 / 0.81818)\ncolor(srgb-linear 0 0.09632 0.39286 / 0.84848)\ncolor(srgb-linear 0 0.10044 0.37931 / 0.87879)\ncolor(srgb-linear 0 0.10429 0.36667 / 0.90909)\ncolor(srgb-linear 0 0.10789 0.35484 / 0.93939)\ncolor(srgb-linear 0 0.11126 0.34375 / 0.9697)\ncolor(srgb-linear 0 0.11443 0.33333 / 1)\n

If you'd like to average the channels without taking transparency into consideration, simply set premultiplied to False.

>>> Steps([Color('darkgreen'), Color('color(srgb 0 0.50196 0 / 1)'), Color('color(srgb 0 0 1)')])\n[color(srgb 0 0.39216 0 / 1), color(srgb 0 0.50196 0 / 1), color(srgb 0 0 1 / 1)]\n>>> for i in range(12):\n...     Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'], premultiplied=False)\n... \ncolor(srgb-linear 0 0.11443 0.33333 / 0.66667)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.69697)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.72727)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.75758)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.78788)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.81818)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.84848)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.87879)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.90909)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.93939)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.9697)\ncolor(srgb-linear 0 0.11443 0.33333 / 1)\n
"},{"location":"average/#averaging-with-undefined-values","title":"Averaging with Undefined Values","text":"

When averaging with undefined values, ColorAide will not consider the undefined values in the average. This is mainly provided for averaging cylindrical colors, particularly achromatic colors.

>>> Color.average(['white', 'color(srgb 0 0 1)'], space='hsl')\ncolor(--hsl -120 0.5 0.75 / 1)\n

Implied achromatic hues are only considered undefined if powerless is enabled. This is similar to how interpolation works. By default, explicitly defined hues are respected if working directly in the averaging color space.

>>> Color.average(['hsl(30 0 100)', 'hsl(240 100 50 / 1)'], space='hsl')\ncolor(--hsl -45 0.5 0.75 / 1)\n>>> Color.average(['hsl(30 0 100)', 'hsl(240 100 50 / 1)'], space='hsl', powerless=True)\ncolor(--hsl -120 0.5 0.75 / 1)\n

While undefined logic is intended to handle achromatic hues, this logic will be applied to any channel. It should be noted that no attempt to carry forward the undefined values through conversion is made at this time. Conversions will remove any undefined status unless the channel is an achromatic hues.

>>> for i in range(12):\n...     Color.average(['darkgreen', f'color(srgb 0 none 0 / {i / 11})', 'color(srgb 0 0 1)'])\n... \ncolor(srgb-linear 0 0.06372 0.5 / 0.66667)\ncolor(srgb-linear 0 0.06095 0.47826 / 0.69697)\ncolor(srgb-linear 0 0.05841 0.45833 / 0.72727)\ncolor(srgb-linear 0 0.05607 0.44 / 0.75758)\ncolor(srgb-linear 0 0.05392 0.42308 / 0.78788)\ncolor(srgb-linear 0 0.05192 0.40741 / 0.81818)\ncolor(srgb-linear 0 0.05006 0.39286 / 0.84848)\ncolor(srgb-linear 0 0.04834 0.37931 / 0.87879)\ncolor(srgb-linear 0 0.04673 0.36667 / 0.90909)\ncolor(srgb-linear 0 0.04522 0.35484 / 0.93939)\ncolor(srgb-linear 0 0.04381 0.34375 / 0.9697)\ncolor(srgb-linear 0 0.04248 0.33333 / 1)\n

When premultiplied is enabled, premultiplication will not be applied to a color if its alpha is undefined.

>>> Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / none)', 'color(srgb 0 0 1)'], space='srgb')\ncolor(srgb 0 0.29804 0.33333 / 1)\n
"},{"location":"cat/","title":"Chromatic Adaptation","text":"

Chromatic adaptation is the human visual system's ability to adjust to changes in illumination in order to preserve the appearance of object colors. It is responsible for the stable appearance of object colors despite the wide variation of light which might be reflected from an object and observed by our eyes. A chromatic adaptation transform (CAT) emulates this important aspect of color perception in color appearance models.

In short, colors look different under different lighting, and CATs are used to predict what a color should look like from one lighting source to another.

"},{"location":"cat/#illuminants","title":"Illuminants","text":"

Viewing a color in daylight will look different than viewing it by candle light. Color spaces usually define a reference illuminant that clarifies the assumed lighting for the given space. For instance, sRGB is a color space defined with an illuminant of D65 (light in the shade - no direct sunlight - at noon). On the other hand, the ProPhoto RGB space uses a D50 illuminant (direct sunlight at noon).

When translating a color from one illuminant to another, it is desirable to ensure that the color under the original illuminant appears as it should under the new illuminant, just as it would in real life. CATs are used to predict what the new color under the new illuminant should be in order to fulfill these requirements.

For a quick example, let's take the color blue under sRGB (D65 white point) and the same blue under Pro Photo RGB (D50 white point). If we take the raw chromaticity points from the color under each color space and use them to generate a color, both under the same color space (in this case sRGB), we can see that the values are different. We can see the values are different.

>>> d65 = Color('blue').split_chromaticity()\n>>> d50 = Color('blue').convert('prophoto-rgb').split_chromaticity()\n>>> color_d50 = Color.chromaticity('srgb', d50)\n>>> color_d65 = Color.chromaticity('srgb', d65)\n>>> Row([color_d50, color_d65])\n[color(srgb 0.12557 0.05823 0.88102 / 1), color(srgb 0 0 1 / 1)]\n

The same color looks different because it is reflecting a different light source. The illuminant of a color space can affect how the color appears, and each illuminant has a different color temperature which can provide a warmer or cooler color tone to the colors under a particular color space.

We can visualize this concept a bit more clearly by taking the raw chromaticities from the D50 and D65 white and scaling them both under the same color space. Here we will take both the D50 and D65 white point and first scale them under the D65 sRGB color space and then scale them under the D50 Pro Photo color space. Notice that the D50 white (Pro Photo) has a red shift when rendered under sRGB, but the D65 white (sRGB) has a blue shift under Pro Photo. Relative to each other, the D65 white has a cooler temperature than D50, and this changes the color.

>>> d65 = Color('srgb', [1, 1, 1]).split_chromaticity()\n>>> d50 = Color('prophoto-rgb', [1, 1, 1]).split_chromaticity()\n>>> color_d50 = Color.chromaticity('srgb', d50, scale=True)\n>>> color_d65 = Color.chromaticity('srgb', d65, scale=True)\n>>> Row([color_d50, color_d65])\n[color(srgb 1 0.92084 0.80569 / 1), color(srgb 1 1 1 / 1)]\n>>> color_d50 = Color.chromaticity('prophoto-rgb', d50, scale=True)\n>>> color_d65 = Color.chromaticity('prophoto-rgb', d65, scale=True)\n>>> Row([color_d50, color_d65])\n[color(prophoto-rgb 1 1 1 / 1), color(prophoto-rgb 0.82447 0.84559 0.97953 / 1)]\n

In order to account for the differences in illuminants, we use chromatic adaptation to modify the chromaticities of the color so that that they account for the different illuminant and appear as they should under the new light source. This happens automatically when we do call convert(). We can see that the white point gets adjusted such that the D50 white looks like the D65 white when in sRGB and D65 white looks like D50 white under Pro Photo.

>>> color_d50 = Color('prophoto-rgb', [1, 1, 1]).convert('srgb')\n>>> color_d65 = Color('srgb', [1, 1, 1])\n>>> Row([color_d50, color_d65])\n[color(srgb 1 1 1 / 1), color(srgb 1 1 1 / 1)]\n

Generally, chromatic adaptation takes place within the XYZ color space. So in ColorAide, any color transform that must account for the differences of illuminants between two color spaces must go through chromatic adaptation, and it must occur in the XYZ color space. ColorAide satisfies this by making the registration of the XYZ D65 color space mandatory and using it as the transition color space when chromatic adaptation is needed.

For instance, if a color space such as Pro Photo is being translated to sRGB, Pro Photo will first be transformed to XYZ D50, then it will be chromatically adapted to XYZ D65, next it will be transformed sRGB.

So, we can actually do this manually and compare the results to convert() which automatically handles chromatic adaptation. In order to do this, we need to provide the specified \"white point\" for the source color and the \"white point\" for the destination color along with the XYZ coordinates we wish to transform. ColorAide uses the Bradford CAT by default, so we will specify that CAT for consistency.

>>> from coloraide import cat\n>>> xyzd50 = Color('prophoto-rgb', [1, 1, 1]).convert('xyz-d50').coords()\n>>> xyzd50\n[0.9642956764295677, 1.0, 0.8251046025104602]\n>>> xyzd65 = Color.chromatic_adaptation(cat.WHITES['2deg'][\"D50\"], cat.WHITES['2deg'][\"D65\"], xyzd50, method='bradford')\n>>> manual = Color('xyz-d65', xyzd65).convert('srgb')\n>>> auto = Color('prophoto-rgb', [1, 1, 1]).convert('srgb')\n>>> manual, auto\n(color(srgb 1 1 1 / 1), color(srgb 1 1 1 / 1))\n

ColorAide, currently defines the following illuminants for both 2\u02da observer and 10\u02da observer, but most people are probably only concerned with D65 and D50 (2\u02da degree observer) which are the only the illuminants used in the default color spaces provided by ColorAide. Illuminants are not restricted to what is listed below, but those are the ones available by default.

Illuminants A B C D50 D55 D65 D75 E F2 F7 F11"},{"location":"cat/#supported-cats","title":"Supported CATs","text":"

There are various CATs, all varying in complexity and accuracy. We will not go through all of them and instead will leave that up to the user to research as needed. Suffice it to say, the Bradford CAT is currently the industry standard (in most cases), but there are a variety of options available, and research continues to try and improve upon CATs of the past to come up with better CATs for the future.

Currently, ColorAide mainly supports von Kries type CATs (named after an early 20th century color scientist), or CATs that are similar to and/or are built upon the original von Kries CAT. We also do not currently support every known von Kries CAT out there, but a good number are available. In the future, support may be expanded.

CAT bradford von-kries xyz-scaling sharp cat02 cat16 cmccat97 cmccat2000"},{"location":"cat/#changing-the-default-cat","title":"Changing the Default CAT","text":"

Changing the default CAT is easy and follows the same pattern as the rest of the available class overrides. Simply derive a new Color() class from the original and override the CHROMATIC_ADAPTATION property with the name of the desired CAT. Afterwards, all color transforms will use the specified CAT.

>>> class Custom(Color):\n...     CHROMATIC_ADAPTATION = 'cat02'\n... \n>>> d50 = Custom('color(xyz-d50 0.11627 0.07261 0.23256 / 1)')\n>>> d65 = d50.convert('xyz-d65')\n>>> d50, d65\n(color(xyz-d50 0.11627 0.07261 0.23256 / 1), color(xyz-d65 0.12476 0.07614 0.30581 / 1))\n
"},{"location":"chromaticity/","title":"Chromaticity Coordinates","text":"

Colors are generally composed of two parts, luminance and chromaticity. Luminance refers to the brightness while chromaticity refers to the hue and colorfulness.

Separating out chromaticity from luminance, we can create a 2D from the chromaticity where we are able to plot the full spectrum of visible color. Over time, there have been multiple approaches to expressing chromaticity, the most common being: CIE 1931, CIE 1964, or CIE 1976.

1931 xy Chromaticity Diagram1960 uv Chromaticity Diagram1931 u'v' Chromaticity Diagram

Chromaticity coordinates are an important part of color science and are often used to define characteristics of color spaces, including gamuts and white points. This is often why depictions of white points and gamuts are overlaid onto chromaticity diagrams.

When combined with luminance, we can add depth when viewing a gamut within the chromaticity space.

"},{"location":"chromaticity/#getting-chromaticity-coordinates","title":"Getting Chromaticity Coordinates","text":"

ColorAide provides a few ways to access chromaticity. The first method, split_chromaticity(), allows for decomposing a color into it's two basic parts: chromaticity and luminance. The result is a 3 coordinates list containing the 2D chromaticity coordinates followed by the luminance. By default, values are exported in the format u'v'Y, where u'v' is the chromaticity coordinates in the CIE 1976 system and Y is the luminance taken directly from XYZ.

>>> Color('red').split_chromaticity()\n[0.4507042253521127, 0.522887323943662, 0.21263900587151024]\n

If chromaticity coordinates are desired in a different format, any of the following can be manually specified.

Key Output Description xy-1931 [x, y, Y] Chromaticity in the CIE 1931 xy system and luminance. uv-1960 [u, v, Y] Chromaticity in the CIE 1960 uv system and luminance. uv-1976 [u', v', Y] Chromaticity in the CIE 1976 u'v' system and luminance.
>>> Color('red').split_chromaticity('xy-1931')\n[0.64, 0.33, 0.21263900587151024]\n

All results are returned with chromaticities being relative to the current color's white point. This allows you to get the true chromaticities of that color space. If a pair of white point chromaticities are provided, the values will be chromatically adapted to match the given white point. white must be specified as an xy chromaticity pair. If you have chromaticity values in a non-xy pair, see converting chromaticity coordinates to learn how to convert them to the expected format.

>>> from coloraide import cat\n>>> Color('red').split_chromaticity(white=cat.WHITES['2deg']['D50'])\n[0.45718361011203096, 0.5248532543501369, 0.22249317711056513]\n

Tip

If you ever need to get the white point from an already registered, supported color space, ColorAide makes these available via white(). The value is returned by default as the tristimulus values (XYZ coordinates), but it can also be returned as any of the supported chromaticity coordinate formats by specifying the desired output.

>>> Color('red').white()\n[0.9504559270516716, 1.0, 1.0890577507598784]\n>>> Color('red').white('uv-1960')\n[0.1978300066428368, 0.312213329959194]\n

If all that is desired is the 2D chromaticity coordinates, you can also use the two, simple convenience methods: xy() and uv(). xy() will return chromaticity in the CIE 1931 xy system and uv() will return chromaticity within the CIE 1976 u'v' system (default) or the CIE 1960 uv system, uv output is controlled by explicitly passing the desired year of the uv system.

>>> Color('red').xy()\n[0.64, 0.33]\n>>> Color('red').uv()\n[0.4507042253521127, 0.522887323943662]\n>>> Color('red').uv('1960')\n[0.4507042253521127, 0.3485915492957747]\n

The white parameter is also accepted by xy() and uv().

Luminance

ColorAide also allows for grabbing luminance via the luminance() method. It should be noted that by default this function returns luminance relative to the D65 white point as it is common for people to use luminance normalized like this, but if you'd like to quickly get luminance and have it relative to the current color's white point, just set white to None and ColorAide will calculate the value relative to the current color.

>>> Color('red').luminance(white=None)\n0.21263900587151024\n

New 2.4

  • split_chromaticity() is new in 2.4.
  • Chromaticity specifier in white() is new in 2.4.
  • white parameter of luminance() is new in 2.4.
"},{"location":"chromaticity/#create-color-from-chromaticity-coordinates","title":"Create Color From Chromaticity Coordinates","text":"

New 2.4

ColorAide also provides an easy way to create colors from chromaticity coordinates. chromaticity() is a generalized method that takes a color space to create the color in and a set of chromaticity coordinates. The coordinates should be supplied using the same white point as the targeted color space. Chromaticity coordinates can be passed as 2D coordinates without luminance or with luminance. When passed with luminance the color should be identical to the original.

>>> uvY = Color('red').uv()\n>>> Color.chromaticity('srgb', uvY)\ncolor(srgb 1.9559 0 0 / 1)\n

If only 2D chromaticity points are given, Y will be assumed as 1. When luminance is maxed out like this, it may be desirable to normalize/scale the color in a linear RGB space to make the color displayable. This can be done by enabling scale which, by default, scales the color in linear sRGB. If a wider gamut is needed, you can change it via scale_space. Using a non-linear RGB space is not recommended as non-linear spaces will cause the chromaticity coordinates to shift.

>>> uv = Color('red').uv()\n>>> Color.chromaticity('srgb', uv, scale=True)\ncolor(srgb 1 0 0 / 1)\n>>> uv = Color('display-p3', [1, 0, 0]).uv()\n>>> Color.chromaticity('display-p3', uv, scale=True, scale_space='display-p3-linear')\ncolor(display-p3 1 0 0 / 1)\n

This generally preserves chromaticity, scaling luminance, but if a color is out of gamut, the chromaticity of the resultant color will be affected.

Tip

There is no RGB color space that perfectly encompasses the entire visible gamut. No matter what scaling space is selected, colors outside the gamut of the scaling space will not exactly match the specified chromaticity coordinates. If exact values are needed, scaling should be avoided. If displaying the colors is desired, then sacrificing accuracy of the colors by scaling or some other gamut mapping method is necessary. Scaling/normalization is how we colorize all of our chromaticity diagrams in these documents.

It is important to be consistent with white point usage. If we wanted to create a color in sRGB with ProPhoto chromaticities, it is important that we create the color first under a color space that uses the same white point. ProPhoto uses D50 and sRGB uses D65. So if we had ProPhoto chromaticities, it makes sense to first create the color under ProPhoto and then convert to sRGB.

>>> c1 = Color('prophoto-rgb', [1, 0, 0])\n>>> c2 = Color.chromaticity('prophoto-rgb', c1.split_chromaticity()).convert('srgb')\n>>> c1, c2.convert('prophoto-rgb')\n(color(prophoto-rgb 1 0 0 / 1), color(prophoto-rgb 1 0 0 / 1))\n

With that said, there may be times when you have chromaticity coordinates that use a white point which no current supported color space supports. chromaticity() does provide a way of gamut mapping such coordinates by simply specifying the white point of the provided chromaticity coordinates. To illustrate, we'll take the same example, but this time create a color under sRGB directly with ProPhoto chromaticities, the difference is that we will pass ProPhoto's white point.

>>> c1 = Color('prophoto-rgb', [1, 0, 0])\n>>> c2 = Color.chromaticity('srgb', c1.split_chromaticity(), white=c1.white('xy-1931'))\n>>> c1, c2.convert('prophoto-rgb')\n(color(prophoto-rgb 1 0 0 / 1), color(prophoto-rgb 1 0 0 / 1))\n
"},{"location":"chromaticity/#converting-chromaticity-coordinates","title":"Converting Chromaticity Coordinates","text":"

New 2.4

ColorAide normally expects you are working with chromaticity points that are compatible with at least one of the registered color spaces. In general, the API is set up with this expectation to make things easy for users. Normally, the user will not need to manually specify a white point, but it is possible that a user may be working with or exporting chromaticity coordinates to/from an unsupported, external space using an altogether different white point. We may need to specify that white point, but it may be in a format that ColorAide doesn't expect. Luckily, ColorAide provides a simple way to convert from between various chromaticity formats and convert to and from tristimulus (XYZ) values.

>>> from coloraide import util\n>>> Color('white').xy()\n[0.3127, 0.329]\n>>> Color.convert_chromaticity('uv-1976', 'xy-1931', Color('white').uv())\n[0.3126999999999999, 0.32899999999999985, 1.0]\n>>> Color.convert_chromaticity('uv-1960', 'xy-1931', Color('white').uv('1960'))\n[0.3126999999999999, 0.3289999999999999, 1.0]\n>>> Color.convert_chromaticity('xyz', 'xy-1931', Color('white').convert('xyz-d65').coords())\n[0.3127, 0.329, 0.9999999999999999]\n

Tip

When converting from XYZ tristimulus values to chromaticity values, the color black resolves to [0, 0] in xy, uv, or u'v'. This does not align with other achromatic values within the color space if displaying in 3D. This isn't technically incorrect as any chromaticity pair with zero luminance will be equal to black in XYZ. ColorAide normally accounts for this and aligns the point using the targeted color's white point, but when manually converting using convert_chromaticity(), such context is unavailable. If you are converting external XYZ values to chromaticity coordinates and would like to align, black on the achromatic axis, simply pass in the white point for context.

>>> black = Color('black')\n>>> black.xy()\n[0.3127, 0.329]\n>>> Color.convert_chromaticity('xyz', 'xy-1931', black.convert('xyz-d65').coords())\n[0.0, 0.0, 0.0]\n>>> Color.convert_chromaticity('xyz', 'xy-1931', black.convert('xyz-d65').coords(), white=black.white('xy-1931'))\n[0.3127, 0.329, 0.0]\n
"},{"location":"color/","title":"The Color Object","text":"

The Color object is where all the magic of ColorAide happens and provides access to all the color manipulation methods available. The Color object is used to represent a given color within a particular color space. In order to perform most operations, you will need to create a color instance to begin.

There are a number of ways to instantiate new colors. Here we will cover the basics of creating colors, cloning colors, converting colors, and a few other Color class specific topics.

"},{"location":"color/#importing","title":"Importing","text":"

The Color object contains all the logic to create and manipulate colors. It can be imported from coloraide.

from coloraide import Color\n

By default, the Color object registers only a subset of the available color spaces and features that are shipped with ColorAide. This keeps the object a bit lighter and provides the more commonly used color spaces and features. Color spaces, additional color distancing algorithms, gamut mapping algorithms, etc. are implemented via plugins. The normal way to get access to these additional spaces and features is to subclass the Color object and resister the desired spaces and features that are needed, but if you just want to explore all that ColorAide offers, you can import the ColorAll object from everything.

from coloraide.everything import ColorAll as Color\n

Custom Color Objects

To add more plugins or tweak color defaults, see Custom Color Classes for more.

"},{"location":"color/#creating-colors","title":"Creating Colors","text":"

Once the Color class is imported, colors can be created using various forms of input, including: numerical inputs, dictionaries, CSS color strings, and even other Color instances.

"},{"location":"color/#numerical-inputs","title":"Numerical Inputs","text":"

The quickest way to create a color is by simply specifying the color space, color coordinates, and the optional alpha channel. Numerical inputs require very little processing, but it should be noted that inputs must be specified according to the way the color points are stored. Some people may be aware of the old CSS convention of specifying sRGB colors with a range of 0 - 255, but ColorAide stores these as values between 0 - 1. If transparency is omitted, transparency is assumed to be fully opaque, or a value of 1.

>>> Color(\"srgb\", [0.5, 0, 1], 0.3)\ncolor(srgb 0.5 0 1 / 0.3)\n>>> Color(\"srgb\", [0.5, 0, 1])\ncolor(srgb 0.5 0 1 / 1)\n
"},{"location":"color/#dictionary-inputs","title":"Dictionary Inputs","text":"

It may be desired to store and retrieve colors from some serialized format such as JSON. To make this easier, ColorAide allows exporting and importing colors via dictionaries as well.

Dictionaries must define the space key and the coords key containing values for all of the color channels. The alpha channel is kept separate and can be omitted, and if so, will be assumed as 1.

>>> d = Color('red').to_dict()\n>>> print(d)\n{'space': 'srgb', 'coords': [1.0, 0.0, 0.0], 'alpha': 1.0}\n>>> Color(d)\ncolor(srgb 1 0 0 / 1)\n
"},{"location":"color/#string-inputs","title":"String Inputs","text":"

By default, ColorAide accepts input strings as outlined in the CSS color specification. Accepted syntax includes legacy CSS color formats as defined in CSS Level 3, but also allows for CSS Level 4 Color syntax!

>>> Color(\"red\")\ncolor(srgb 1 0 0 / 1)\n>>> Color(\"#00ff00\")\ncolor(srgb 0 1 0 / 1)\n>>> Color(\"rgb(0 0 255 / 1)\")\ncolor(srgb 0 0 1 / 1)\n

ColorAide supports all the color spaces as defined in the CSS Level 4 Color spec, but is not restricted to only supported CSS colors. In order to support color strings for all colors, ColorAide allows for non-CSS color spaces to be represented via the Level 4 CSS color() function. Essentially, we've adopted the color() function as the universal way in which to serialize color strings.

It should also be noted that color() can be used to describe any color regardless of whether it is supported in the CSS spec in this way or not. For any color that is not explicitly supported in CSS via the color() function, ColorAide will allow using this form if the color space uses a -- prefix for the color space identifier. Check the documentation of the given color space to discover the appropriate CSS identifier name.

>>> Color('color(--hsl 130 40% 75% / 0.5)')\ncolor(--hsl 130 0.4 0.75 / 0.5)\n
"},{"location":"color/#color-instance-inputs","title":"Color Instance Inputs","text":"

If another color instance is passed as the input, a new color object will be created using the color data from the input. This essentially clones the passed object.

>>> c1 = Color('red')\n>>> c2 = Color(c1)\n>>> c1, c2\n(color(srgb 1 0 0 / 1), color(srgb 1 0 0 / 1))\n

You can also use the new method to generate new colors from already instantiated color objects.

>>> color1 = Color(\"red\")\n>>> color1\ncolor(srgb 1 0 0 / 1)\n>>> color1.new(\"blue\")\ncolor(srgb 0 0 1 / 1)\n

Tip

If the Color class has be subclassed, this is an easy way to convert between the different subclasses, assuming the registered color spaces are compatible between the two different Color classes.

"},{"location":"color/#random","title":"Random","text":"

If you'd like to generate a random color, simply call Color.random with a given color space and one will be generated.

>>> [Color.random('srgb') for _ in range(10)]\n[color(srgb 0.36945 0.94249 0.8054 / 1), color(srgb 0.62098 0.56303 0.51077 / 1), color(srgb 0.56704 0.55184 0.27834 / 1), color(srgb 0.88936 0.56696 0.69625 / 1), color(srgb 0.258 0.6967 0.09435 / 1), color(srgb 0.26608 0.0159 0.92105 / 1), color(srgb 0.73206 0.23182 0.01587 / 1), color(srgb 0.49447 0.23812 0.13037 / 1), color(srgb 0.95768 0.22528 0.84887 / 1), color(srgb 0.81995 0.7889 0.3919 / 1)]\n

Ranges are based on the color space's defined channel range. For color spaces with defined gamuts, the values will be confined to appropriate ranges. For color space's without defined gamuts, the ranges may be quite arbitrary in some cases. For color spaces with no hard, defined gamut, or gamuts that that far exceed practical usage it is recommend to fit the colors to whatever gamut you'd like, or simply use a target space with a clear defined gamut.

>>> Color.random('lab').fit('srgb')\ncolor(--lab 59.478 66.62 71.212 / 1)\n

Lastly, if you'd like to further constrain the limits, you can provide a list of constraints. A constraint should be a sequence of two values specifying the minimum and maximum for the channel. If None is provided, that constraint will be ignored. If the list doesn't have enough values, those missing indexes will be ignored. If the list has too many values, those extra values will be ignored.

>>> Color.random('srgb', limits=[(0.25, 0.75)] * 3)\ncolor(srgb 0.47308 0.61544 0.6777 / 1)\n
"},{"location":"color/#cloning","title":"Cloning","text":"

The clone method is an easy way to duplicate the current color object.

Here we clone the green color object, giving us two.

>>> c1 = Color(\"green\")\n>>> c1\ncolor(srgb 0 0.50196 0 / 1)\n>>> c1.clone()\ncolor(srgb 0 0.50196 0 / 1)\n
"},{"location":"color/#updating","title":"Updating","text":"

A color can be \"updated\" using another color input. When an update occurs, the current color space is updated from the data of the second color, but the color space does not change. Using update is the equivalent of converting the second color to the color space of the first and then updating all the coordinates (including alpha). The input parameters are identical to the new method, so we can use a color object, a color string, dictionary, or even raw data points.

Here we update the color red to the color blue:

>>> Color(\"red\")\ncolor(srgb 1 0 0 / 1)\n>>> Color(\"red\").update(Color(\"blue\"))\ncolor(srgb 0 0 1 / 1)\n

Here we update the sRGB red with the color lch(80% 50 130).

>>> Color(\"red\").update(\"lch(80% 50 130)\")\ncolor(srgb 0.60392 0.8398 0.48396 / 1)\n
"},{"location":"color/#mutating","title":"Mutating","text":"

\"Mutating\" is similar to updating except that it will update the color and the color space from another color. The input parameters are identical to the new method, so we can use a color object, a color string, dictionary, or even raw data points.

In this example, the red color object literally becomes the specified CIELCh color of lch(80% 50 130).

>>> Color(\"red\").mutate(\"lch(80% 50 130)\")\ncolor(--lch 80 50 130 / 1)\n
"},{"location":"color/#converting","title":"Converting","text":"

Colors can be converted to other color spaces as needed. Converting will always return a new color unless the in_place parameter is set to True, in which case, the current color will be mutated to the new converted color and a reference to itself is returned.

For instance, if we had a color yellow, and we needed to work with it in another color space, we could simply call the convert method with the desired color space.

>>> Color('yellow').convert(\"lab\")\ncolor(--lab 97.607 -15.75 93.394 / 1)\n

Notes on Round Trip Accuracy

"},{"location":"color/#color-matching","title":"Color Matching","text":"

As previously mentioned, the Color() object can parse CSS style string inputs. The string matching logic is exposed via the match method. We can simply pass match a string, and, if the string is a valid color, a ColorMatch object will be returned. The ColorMatch object has a simple structure that contains the matched color as a Color object, and the start and end points it was located at.

>>> Color.match(\"red\")\nColorMatch(color=color(srgb 1 0 0 / 1), start=0, end=3)\n

By default it matches at the start of the buffer and returns a color if it finds one. If desired, we can do a fullmatch which requires the entire buffer to match a color.

>>> Color.match(\"red and yellow\")\nColorMatch(color=color(srgb 1 0 0 / 1), start=0, end=3)\n>>> Color.match(\"red and yellow\", fullmatch=True)\n

We can also adjust the start position of the search. In this case, by adjusting the start position to 8 characters later, we will match yellow instead of red.

>>> Color.match(\"red and yellow\", start=8)\nColorMatch(color=color(srgb 1 1 0 / 1), start=8, end=14)\n

A method to find all colors in a buffer is not currently provided as looping through all the color spaces and matching all potential colors on every character is not really efficient. Additionally, some buffers may require additional context that is not available to the match function. If such behavior is desired, it is recommended to apply some additional logic to sniff out areas with high likelihood of having a color.

In the following example, we construct a regular expression to find places within the buffer that potentially have a valid color. As the buffer is an HTML document we also want to incorporate some context to avoid matching HTML entities or color names that are part of a CSS variable.

Once we've crafted our regular expression, we can search the buffer to find locations in the buffer that are likely to be colors. Then we can run Color.match() on those positions within the buffer to see if we find a valid color. This ends up being much more efficient!

>>> import re\n>>> from coloraide import Color\n>>> RE_COLOR_START = re.compile(\n...     r\"\"\"(?ix)\n...     (?:\n...         # CSS functions\n...         \\b(?<![-#&$])(?:color\\((?!\\s*-)|(?:hsla?|(?:ok)?(?:lch|lab)|hwb|rgba?)\\()|\n...         # Color words\n...         \\b(?<![-#&$])[\\w]{3,}(?![(-])\\b|\n...         # Hex codes\n...         (?<![&])\\#\n...     )\n...     \"\"\"\n... )\n>>> text = \"\"\"\n... <html>\n... <head>\n... <style>\n... body {\n...     background-color: red;\n...     color: yellow;\n... }\n... </style>\n... </head>\n... <body>\n... <p>This is a test <span style=\"background-color: #000088; color: lch(75% 50 50)\">test</span></p>\n... </body>\n... </html>\n... \"\"\"\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...         mcolor.color.to_string()\n... \n'rgb(255 0 0)'\n'rgb(255 255 0)'\n'rgb(0 0 136)'\n'lch(75 50 50)'\n
"},{"location":"color/#custom-color-classes","title":"Custom Color Classes","text":"

The Color object was created to be extensible and has implemented various functionalities as plugins. Things like color spaces, distancing algorithms, filters, etc. are all implemented as plugins. In order to keep things light, ColorAide does not register all the of the plugins by default unless the user as imported the ColorAll object.

Additionally, ColorAide has implemented a number of defaults that can be tweaked within the Color class to alter how things are handled.

Creating a custom class allows for a user to change some of the default settings and add or remove plugins to gain access to more color spaces, distancing algorithms, filters, and other functionality.

In general, it is always recommended to subclass the Color object when setting up custom preferences or adding or removing plugins. This prevents modifying the base class which may affect other libraries relying on the module. When Color is subclassed, it is safe to then update global overrides or register and deregister plugins without the worry of affecting the base class.

"},{"location":"color/#override-default-settings","title":"Override Default Settings","text":"

ColorAide has a number of preferences that can be altered in the Color class. Most of these options can be configured on demand when calling into a related function that uses them, but it may be useful to set them up one time on a new Color object.

>>> class Color2(Color):\n...     PRECISION = 3\n... \n>>> Color('rgb(128.12345 0 128.12345)').to_string()\n'rgb(128.12 0 128.12)'\n>>> Color2('rgb(128.12345 0 128.12345)').to_string()\n'rgb(128 0 128)'\n
Properties Defaults Description FIT \"lch-chroma\" The default gamut mapping method used by the Color object. INTERPOLATE \"oklab\" The default color space used for interpolation. DELTA_E \"76\" The default \u2206E algorithm used. This applies to when delta_e() is called without specifying a method or when using color distancing to separate color when using the interpolation method called steps. PRECISION 5 The default precision for string outputs. CHROMATIC_ADAPTATION \"bradford\" Chromatic adaptation method used when converting between two color spaces with different white points. See Chromatic Adaptation for more information. HARMONY \"oklch\" Default color space to use for calculating color harmonies. This should be a cylindrical color space. CONTRAST \"wcag21\" Default contrast algorithm. AVERAGE \"average\" Default color space for averaging. CCT \"robertson-1968 Default CCT method. POWERLESS False Experimental option that controls default powerless interpolation behavior. If True, if a color is considered achromatic, but has an explicit hue, it will be treated as powerless during interpolation. CARRYFORWARD False Experimental option that controls default carrying-forward behavior during interpolation. If True, supported, undefined values will be carried over to the interpolating color space after conversion."},{"location":"color/#plugins","title":"Plugins","text":"

Currently, color spaces, delta E methods, chromatic adaptation, filters, contrast, interpolation, and gamut mapping methods are exposed as plugins. As previously mentioned, Color does not register all plugins, and ColorAll is often more than a user needs by default. Registering exactly what you need is normally the recommend approach when more functionality is required.

While we won't go into a lot of details about creating plugins here, we will go over how to register new plugins and deregister existing plugins. To learn more about creating plugins, checkout the plugin documentation.

Registration is performed by the register method. It can take a single plugin or a list of plugins. Based on the plugin's type, The Color object will determine how to properly register the plugin. If the plugin attempts to overwrite a plugin already registered with the same name (as dictated by the plugin) the operation will fail. If overwrite is set to True, the overwrite will not fail and the new plugin will be registered with the specified name in place of the existing plugin.

>>> from coloraide import Color\n>>> from coloraide.spaces.xyy import xyY\n>>> try:\n...     Color('red').convert('xyy')\n... except:\n...     print('Nope')\n... \nNope\n>>> class Custom(Color): ...\n... \n>>> Custom.register(xyY())\n>>> Custom('red').convert('xyy')\ncolor(--xyy 0.64 0.33 0.21264 / 1)\n

Used in conjunction with default settings override, we can not only change a default \u2206E, but we can alter a \u2206E method's configuration by registering it with different defaults:

>>> Color('red').delta_e('blue', method='cmc')\n108.56925233888809\n>>> from coloraide.distance.delta_e_cmc import DECMC\n>>> class Custom(Color):\n...     DELTA_E = \"cmc\"\n... \n>>> Custom.register(DECMC(l=1, c=1), overwrite=True)\n>>> Custom('red').delta_e('blue')\n109.7597278634398\n

If a deregistration is desired, the deregister method can be used. It takes a string that describes the plugin to deregister: category:name.

Valid categories are space, delta-e, cat, contrast, filter, interpolate, fit, and cct.

If the given plugin is not found, an error will be thrown, but if this notification is found to be unnecessary, silent can be enabled and the there will be no error thrown.

>>> class Custom(Color): ...\n... \n>>> Custom.deregister('space:lab-d65')\n>>> try:\n...     Custom('red').convert('lab-d65')\n... except ValueError:\n...     print('Could not convert to Lab D65 as it is no longer registered')\n... \nCould not convert to Lab D65 as it is no longer registered\n

Use of * with deregister will remove all plugins. Use of category:* will remove all plugins of that category.

"},{"location":"compositing/","title":"Compositing and Blending","text":"

Alpha compositing and blending are tied together under the umbrella of compositing. Each is just an aspect of the overall compositing of colors. Blend is run first, followed by alpha compositing.

ColorAide implements both alpha compositing and blending as described in the Compositing and Blending Level 1 specification. Alpha composting is based on Porter Duff compositing. By default, the compose method uses the normal blend mode and the source-over Porter Duff operator.

"},{"location":"compositing/#blending","title":"Blending","text":"

Blending is the aspect of compositing that calculates the mixing of colors where the source element and backdrop overlap. Conceptually, the colors in the source element (top layer) are blended in place with the backdrop (bottom layer).

There are various blend modes, the most common is the normal blend mode which is the default blending mode for browsers. The normal blend mode simply returns the top layer's color when one is overlaid onto another.

But there are many blend modes that could be used, all of which yield different results. If we were to apply a multiply blend mode, we would get something very different:

When composing, the blend mode can be controlled separately in ColorAide. Here, we again use the multiply example and replicate it in ColorAide. To apply blending in ColorAide, simply call compose with a backdrop color, and the calling color will be used as the source.

Display P3sRGB
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, blend='multiply', space=\"display-p3\")\ncolor(display-p3 0.32281 0.23703 0.54084 / 1)\n
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, blend='multiply', space=\"srgb\")\ncolor(srgb 0.02713 0.18668 0.55765 / 1)\n

Tip

compose() can output the results in any color space you need by setting out_space.

>>> Color('#07c7ed').compose(Color('#fc3d99'), blend='multiply', space='srgb', out_space='hsl')\ncolor(--hsl 221.95 0.90722 0.29239 / 1)\n

Display Differences

As some browsers apply compositing based on the display's current color space, we've provided examples in both sRGB and Display P3 so that the examples can be compared on different displays. Which of the above matches your browser?

ColorAide allows you to blend a source over multiple backdrops quite easily as well. Simply send in a list, and the colors will be blended from right to left with the right most color being on the bottom of the stack, and the base color being used as the source (on the very top).

Display P3sRGB
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c3 = Color('#f5d311')\n>>> c1, c2, c3\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1), color(srgb 0.96078 0.82745 0.06667 / 1))\n>>> c1.compose([c2, c3], blend='multiply', space=\"display-p3\")\ncolor(display-p3 0.3031 0.19729 0.15625 / 1)\n
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c3 = Color('#f5d311')\n>>> c1, c2, c3\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1), color(srgb 0.96078 0.82745 0.06667 / 1))\n>>> c1.compose([c2, c3], blend='multiply', space=\"srgb\")\ncolor(srgb 0.02606 0.15447 0.03718 / 1)\n

Lastly, if for any reason, it is desired to compose with blending disabled (e.g. just run alpha compositing), then you can simply set blend to False.

multiply is just one of many blend modes that are offered in ColorAide, check out Blend Modes to learn about other blend modes.

"},{"location":"compositing/#alpha-compositing","title":"Alpha Compositing","text":"

Alpha compositing or alpha blending is the process of combining one image with a background to create the appearance of partial or full transparency.

When dealing with layers, there are many possible ways to handle them:

Porter Duff compositing covers all possible configurations of layers. Many of these configurations can be useful for all sorts of operations, such as masking. While this library supports all of them, the most commonly used one is source-over which is used to implement simple alpha compositing to simulate semi-transparent layers on top of each other.

Given two colors, ColorAide can replicate this behavior and determine the resultant color by applying compositing. We will use the demonstration above and replicate the result in the example below. Below we set the source color to rgb(7 199 237 / 0.5) and the backdrop color to #fc3d99 and run it through the compose method. It should be noted that the default blend mode of normal is used in conjunction by default.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, space=\"display-p3\")\ncolor(display-p3 0.63261 0.53855 0.75261 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, space=\"srgb\")\ncolor(srgb 0.50784 0.5098 0.76471 / 1)\n

Display Differences

As some browsers apply compositing based on the display's current color space, we've provided examples in both sRGB and Display P3 so that the examples can be compared on different displays. Which of the above matches your browser?

While the average user will be content with the default alpha compositing, Porter Duff offers many other configurations. If desired, we can change the Porter Duff operator used and apply different composite logic. For instance, in this case we can get the resultant of the backdrop over the source color by setting the operator to destination-over. As the backdrop is fully opaque, we just get the backdrop color unaltered.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, operator='destination-over', space=\"display-p3\")\ncolor(display-p3 0.91078 0.30832 0.59266 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, operator='destination-over', space=\"srgb\")\ncolor(srgb 0.98824 0.23922 0.6 / 1)\n

You can also apply alpha compositing to multiple layers at once. Simply send in a list of colors as the backdrop, and the colors will be composed from right to left with the right most color being on the bottom of the stack and the base color (the source) being on the very top.

Here we are using the normal blend mode and 50% transparency on all the circles with an opaque white background. We will calculate the center color where all three layers overlap.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> bg = Color('white')\n>>> c1, c2, c3, bg\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 0.5), color(srgb 0.96078 0.82745 0.06667 / 0.5), color(srgb 1 1 1 / 1))\n>>> c1.compose([c2, c3, bg], blend='normal', space=\"display-p3\")\ncolor(display-p3 0.64728 0.69051 0.76555 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> bg = Color('white')\n>>> c1, c2, c3, bg\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 0.5), color(srgb 0.96078 0.82745 0.06667 / 0.5), color(srgb 1 1 1 / 1))\n>>> c1.compose([c2, c3, bg], blend='normal', space=\"srgb\")\ncolor(srgb 0.50588 0.67843 0.74804 / 1)\n

Lastly, if for any reason, it is desired to run compose with alpha compositing disabled (e.g. just run blending), then you can simply set operator to False.

Check out Compositing Operators to learn about the many variations that are supported.

"},{"location":"compositing/#complex-compositing","title":"Complex Compositing","text":"

We've covered alpha compositing and blending and have demonstrated their use with simple two color examples and multi-layered examples, but what about different blend modes mixed with alpha compositing?

In this example, we will consider three circles, each with a unique color: #07c7ed, #fc3d99, and #f5d311. We apply 50% transparency to all the circles and place them on a white background. We then perform a multiply blend on all the circles but isolate them so the multiply blend does not apply to the background. The circles are all represented with CSS. We will now try and replicate the colors with ColorAide.

So in the code below, we work our way from the bottom of the stack to the top. Since the background is isolated from the multiply blending, in each region, we start by performing a normal blend on the bottom circle against the background. We then apply multiply blending on each color that is stacked on top. We've provided both the P3 and sRGB outputs to make it easy to compare in case your browser blends in one instead of the other.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> cw2 = c2.compose('white', blend='normal', space='display-p3')\n>>> cw3 = c3.compose('white', blend='normal', space='display-p3')\n>>> r1 = c2.compose(cw3, blend='multiply', space='display-p3')\n>>> r2 = c1.compose(cw2, blend='multiply', space='display-p3')\n>>> r3 = c1.compose(cw3, blend='multiply', space='display-p3')\n>>> r1, r2, r3\n(color(display-p3 0.92621 0.59932 0.5132 / 1), color(display-p3 0.64701 0.57853 0.76151 / 1), color(display-p3 0.65654 0.81024 0.61627 / 1))\n>>> c1.compose([c2, cw3], blend='multiply', space='display-p3')\ncolor(display-p3 0.62725 0.53003 0.49076 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> cw2 = c2.compose('white', blend='normal', space='srgb')\n>>> cw3 = c3.compose('white', blend='normal', space='srgb')\n>>> r1 = c2.compose(cw3, blend='multiply', space='srgb')\n>>> r2 = c1.compose(cw2, blend='multiply', space='srgb')\n>>> r3 = c1.compose(cw3, blend='multiply', space='srgb')\n>>> r1, r2, r3\n(color(srgb 0.97463 0.56615 0.42667 / 1), color(srgb 0.5107 0.55157 0.77176 / 1), color(srgb 0.50365 0.81339 0.51451 / 1))\n>>> c1.compose([c2, cw3], blend='multiply', space='srgb')\ncolor(srgb 0.50069 0.50399 0.41161 / 1)\n

Results may vary depending on the browser, but we can see (ignoring rounding differences) that the colors match up. This was performed on Chrome in macOS using a display that uses display-p3.

"},{"location":"compositing/#blend-modes","title":"Blend Modes","text":""},{"location":"compositing/#normal","title":"Normal","text":"

The blending formula simply selects the source color.

Specified as 'normal'.

"},{"location":"compositing/#multiply","title":"Multiply","text":"

The source color is multiplied by the destination color and replaces the destination. The resultant color is always at least as dark as either the source or destination color. Multiplying any color with black results in black. Multiplying any color with white preserves the original color.

Specified as 'multiply'.

"},{"location":"compositing/#screen","title":"Screen","text":"

Multiplies the complements of the backdrop and source color values, then complements the result. The result color is always at least as light as either of the two constituent colors. Screening any color with white produces white; screening with black leaves the original color unchanged. The effect is similar to projecting multiple photographic slides simultaneously onto a single screen.

Specified as 'screen'.

"},{"location":"compositing/#overlay","title":"Overlay","text":"

Multiplies or screens the colors, depending on the backdrop color value. Source colors overlay the backdrop while preserving its highlights and shadows. The backdrop color is not replaced but is mixed with the source color to reflect the lightness or darkness of the backdrop.

Specified as 'overlay'.

"},{"location":"compositing/#darken","title":"Darken","text":"

Selects the darker of the backdrop and source colors. The backdrop is replaced with the source where the source is darker; otherwise, it is left unchanged.

Specified as 'darken'.

"},{"location":"compositing/#lighten","title":"Lighten","text":"

Selects the lighter of the backdrop and source colors. The backdrop is replaced with the source where the source is lighter; otherwise, it is left unchanged.

Specified as 'lighten'.

"},{"location":"compositing/#color-dodge","title":"Color Dodge","text":"

Brightens the backdrop color to reflect the source color. Painting with black produces no changes.

Specified as 'color-dodge'.

"},{"location":"compositing/#color-burn","title":"Color Burn","text":"

Darkens the backdrop color to reflect the source color. Painting with white produces no change.

Specified as 'color-burn'.

"},{"location":"compositing/#hard-light","title":"Hard Light","text":"

Multiplies or screens the colors, depending on the source color value. The effect is similar to shining a harsh spotlight on the backdrop.

Specified as 'hard-light'.

"},{"location":"compositing/#soft-light","title":"Soft Light","text":"

Darkens or lightens the colors, depending on the source color value. The effect is similar to shining a diffused spotlight on the backdrop.

Specified as 'soft-light'.

"},{"location":"compositing/#difference","title":"Difference","text":"

Subtracts the darker of the two constituent colors from the lighter color. Painting with white inverts the backdrop color; painting with black produces no change.

Specified as 'difference'.

"},{"location":"compositing/#exclusion","title":"Exclusion","text":"

Produces an effect similar to that of the Difference mode but lower in contrast. Painting with white inverts the backdrop color; painting with black produces no change.

Specified as 'exclusion'.

"},{"location":"compositing/#hue","title":"Hue","text":"

Creates a color with the hue of the source color and the saturation and luminosity of the backdrop color.

Specified as 'hue'.

"},{"location":"compositing/#saturation","title":"Saturation","text":"

Creates a color with the saturation of the source color and the hue and luminosity of the backdrop color. Painting with this mode in an area of the backdrop that is a pure gray (no saturation) produces no change.

Specified as 'saturation'.

"},{"location":"compositing/#luminosity","title":"Luminosity","text":"

Creates a color with the luminosity of the source color and the hue and saturation of the backdrop color. This produces an inverse effect to that of the Color mode. This mode is the one you can use to create monochrome \"tinted\" image effects like the ones you can see in different website headers.

Specified as 'luminosity'.

"},{"location":"compositing/#color","title":"Color","text":"

Creates a color with the hue and saturation of the source color and the luminosity of the backdrop color. This preserves the gray levels of the backdrop and is useful for coloring monochrome images or tinting color images.

Specified as 'color'.

"},{"location":"compositing/#compositing-operators","title":"Compositing Operators","text":""},{"location":"compositing/#clear","title":"Clear","text":"

No regions are enabled.

Source Destination Result

Specified as 'clear'.

"},{"location":"compositing/#copy","title":"Copy","text":"

Only the source will be present.

Source Destination Result

Specified as 'copy'.

"},{"location":"compositing/#destination","title":"Destination","text":"

Only the destination will be present.

Source Destination Result

Specified as 'destination'.

"},{"location":"compositing/#source-over","title":"Source Over","text":"

Source is placed over the destination.

Source Destination Result

Specified as 'source-over'.

"},{"location":"compositing/#destination-over","title":"Destination Over","text":"

Destination is placed over the source.

Source Destination Result

Specified as 'destination-over'.

"},{"location":"compositing/#source-in","title":"Source In","text":"

The source that overlaps the destination, replaces the destination.

Source Destination Result

Specified as 'source-in'.

"},{"location":"compositing/#destination-in","title":"Destination In","text":"

Destination which overlaps the source, replaces the source.

Source Destination Result

Specified as 'destination-in'.

"},{"location":"compositing/#source-out","title":"Source Out","text":"

Source is placed, where it falls outside of the destination.

Source Destination Result

Specified as 'source-out'.

"},{"location":"compositing/#destination-out","title":"Destination Out","text":"

Destination is placed, where it falls outside of the source.

Source Destination Result

Specified as 'destination-out'.

"},{"location":"compositing/#source-atop","title":"Source Atop","text":"

Source which overlaps the destination, replaces the destination. Destination is placed elsewhere.

Source Destination Result

Specified as 'source-atop'.

"},{"location":"compositing/#destination-atop","title":"Destination Atop","text":"

Destination which overlaps the source replaces the source. Source is placed elsewhere.

Source Destination Result

Specified as 'destination-atop'.

"},{"location":"compositing/#xor","title":"XOR","text":"

Destination which overlaps the source replaces the source. Source is placed elsewhere.

Source Destination Result

Specified as 'xor'.

"},{"location":"compositing/#lighter","title":"Lighter","text":"

Display the sum of the source image and destination image.

Source Destination Result

Specified as 'lighter'.

"},{"location":"contrast/","title":"Contrast","text":"

ColorAide provides a number of utilities related to luminance and contrast.

"},{"location":"contrast/#relative-luminance","title":"Relative Luminance","text":"

In the CIE XYZ and xyY color spaces, the Y parameter is linear to changes in the volume of light. Specifically this refers to the amount of reflected light where 1.0 is assumed to be a perfect reflector in relation to the reference white.

The luminance method exposes access to this value to make it quick and easy to query the relative luminance.

>>> Color(\"black\").luminance()\n0.0\n>>> Color(\"white\").luminance()\n0.9999999999999999\n>>> Color(\"blue\").luminance()\n0.07219231536073371\n

It should be noted that this luminance is relative to the XYZ D65 color space by default as this is how it is defined in the WCAG 2.1. What this means is that luminance is equivalent to the Y value of XYZ D65. We follow this convention as many people expect it in this format.

Luminance and WCAG 2.1

Luminance as described in the WCAG 2.1 spec is essentially the exact same as what the luminance method returns. The only difference is the lower precision by which they calculate the value:

>>> r, g, b = Color('purple')[:-1]\n>>> r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4\n>>> g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4\n>>> b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4\n>>> l = (0.2126 * r + 0.7152 * g + 0.0722 * b)\n>>> print(l)\n0.06147707043243851\n>>> Color('purple').convert('xyz-d65')['y']\n0.06148383144929487\n

If you'd like to have luminance in relation to a given color's white point, you can set white to None. If you'd like to get the luminance relative to some other white point, you can specify the white point as xy chromaticity points.

>>> from coloraide import cat\n>>> Color('prophoto-rgb', [1, 0, 0]).luminance()\n0.26831828062163154\n>>> Color('prophoto-rgb', [1, 0, 0]).luminance(white=None)\n0.2880711282292934\n>>> Color('prophoto-rgb', [1, 0, 0]).luminance(white=cat.WHITES['2deg']['E'])\n0.2905597743108222\n

New 2.4

The white parameter is new in 2.4.

"},{"location":"contrast/#contrast_1","title":"Contrast","text":"

Chromatic contrast refers to the ability to see the difference of a colored object against a colored background. This can be very important when it comes to visual design and other fields. Determining contrast has been explored in many different ways and, depending on the application, there may be approaches to contrast that are more favorable. In the context of the web, contrast often refers to the ability to see text on a colored background and is of particular importance to those that suffer from visual impairments or disabilities.

It should be noted that as we talk about contrast, we will refer to the colors as the text and background as this is often the context in which such a function is used. As far as ColorAide is concerned, the text is always the calling color and the background is the input parameter. It is important to note this as not all contrast algorithms are symmetrical, and can differ depending which color is referenced as text, and which is referenced as the background.

At this time, ColorAide only offers a handful of contrast approaches, and they can be by using the contrast() method.

>>> Color(\"blue\").contrast(\"red\")\n2.149390533243867\n

To select different contrast methods, simply use the method parameter.

>>> Color(\"blue\").contrast(\"red\", method='wcag21')\n2.149390533243867\n
Methods Symmetrical Description wcag21 WCAG 2.1 contrast ratio. lstar Color difference between two tones in the HCT color space."},{"location":"contrast/#wcag-21-contrast-ratio","title":"WCAG 2.1 Contrast Ratio","text":"

The WCAG 2.1 contrast ratio is registered in Color by default

ColorAide implements the color contrast ratio as outlined in the WCAG 2.1 spec. This is currently, the default contrast method. It is not without fault, but is currently the standard outlined for the web.

The contrast ratio as outlined in the WCAG 2.1 specification is simply the ratio of color luminance from a foreground and background color and is very easy to determine if you are able to acquire the luminance of the two colors.

# Where `l1` is the lighter luminance and `l2` the darker\ncontrast_ratio = (l1 + 0.05) / (l2 + 0.05)\n

This method can be used by specifying wcag21 as the contrast method.

>>> Color(\"blue\").contrast(\"red\", method='wcag21')\n2.149390533243867\n
"},{"location":"contrast/#lstar-lightness-difference","title":"Lstar Lightness Difference","text":"

The Lstar contrast method is not registered in Color by default

Google's Material Design uses a new color space called HCT. It uses the hue and chroma from CAM16 and the tone/lightness from CIELab. For contrast, they determined using tones that are \"far enough apart\" in the HCT color space was a good indication of sufficient contrast. Since HCT tone is exactly the same as CIELab's lightness (also known as L*), we've referred to this approach as Lstar.

Lstar's color difference approach to contrast is quite simple, it's literally the difference between two color's lightness as provided by CIELab. This method does not care which color is text or background.

>>> Color('hct', [30, 20, 70]).contrast(Color('hct', [30, 20, 50]), method='lstar')\n20.000001474145677\n

In order to use this contrast method, the plugin must be registered. This assumes the CIELab color space is currently registered, which it is by default.

from coloraide import Color as Base\nfrom coloraide.contrast.lstar import LstarContrast\n\nclass Color(Base): ...\n\nColor.register(LstarContrast())\n
"},{"location":"distance/","title":"Color Distance and Delta E","text":"

The difference or distance between two colors allows for a quantified analysis of how far apart two colors are from one another. This metric is of particular interest in the field of color science, but it has practical applications in color libraries working with colors.

Usually, color distance is applied to near perceptual uniform color spaces in order to obtain a metric regarding a color's visual, perceptual distance from another color. This can be useful in gamut mapping or even determining that colors are close enough or far enough away from each other.

"},{"location":"distance/#color-distance","title":"Color Distance","text":"

ColorAide provides a simple euclidean color distance function. By default, it evaluates the distance in the CIELab color space, but it can be configured to evaluate in any color space, such as Oklab, etc. It may be less useful in some color spaces compared to others. Some spaces may not be well suited, such as cylindrical spaces. Some spaces might not be as very perceptually uniform as others requiring more complex algorithms.

>>> Color(\"red\").distance(\"blue\", space=\"srgb\")\n1.4142135623730951\n>>> Color(\"red\").distance(\"blue\", space=\"lab\")\n184.0190486209969\n
"},{"location":"distance/#delta-e","title":"Delta E","text":"

The delta_e function gives access to various \u2206E implementations, which are just different algorithms to calculate distance. Some are simply Euclidean distance withing a certain color space, some are far more complex.

If no method is specified, the default implementation is \u2206E*ab (CIE76) which uses a simple Euclidean distancing algorithm on the CIELab color space. It is fast, but not as accurate as later iterations of the algorithm as CIELab is not actually as perceptually uniform as it was thought when CIELab was originally developed.

>>> Color(\"red\").delta_e(\"blue\")\n176.3084955965824\n

When method is set, the specified \u2206E algorithm will be used instead. For instance, below we use \u2206E*00 which is a more complex algorithm that accounts for the CIELab's weakness in perceptually uniformity. It does come at the cost of being a little slower.

>>> Color(\"red\").delta_e(\"blue\", method=\"2000\")\n52.87819528592645\n

Distancing and Symmetry

It should be noted that not all distancing algorithms are symmetrical. Some are order dependent.

"},{"location":"distance/#delta-e-cie76","title":"Delta E CIE76","text":"

The \u2206Eab distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*ab\u00a0(CIE76) 76 space='lab-d65'

One of the first approaches to color distancing and is actually just Euclidean distancing in the CIELab color space.

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"76\")\n176.3084955965824\n>>> Color(\"red\").delta_e(\"blue\", method=\"76\", space='lab')\n184.0190486209969\n
"},{"location":"distance/#delta-e-cmc-1984","title":"Delta E CMC (1984)","text":"

The \u2206Ecmc distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*cmc\u00a0(CMC\u00a0l:c\u00a0(1984)) cmc l=2, c=1, space='lab-d65'

Delta E CMC is based on the CIELCh color space. The CMC calculation mathematically defines an ellipsoid around the standard color with semi-axis corresponding to hue, chroma and lightness.

Parameter Acceptability Perceptibility l 2 1 c 1 1

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"cmc\")\n108.56925233888809\n>>> Color(\"red\").delta_e(\"blue\", method=\"cmc\", space='lab')\n114.2301281201658\n
"},{"location":"distance/#delta-e-cie94","title":"Delta E CIE94","text":"

The \u2206E*94 distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*94\u00a0(CIE94) 94 kl=1, k1=0.045, k2=0.015, space='lab-d65'

The 1976 definition was extended to address perceptual non-uniformities, while retaining the CIELab color space, by the introduction of application-specific weights derived from an automotive paint test's tolerance data.

Parameter Graphic\u00a0Arts Textiles kl 1 2 k1 0.045 0.048 k2 0.015 0.014

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"94\")\n70.57699903580162\n>>> Color(\"red\").delta_e(\"blue\", method=\"94\", space='lab')\n73.82677591958294\n
"},{"location":"distance/#delta-e-ciede2000","title":"Delta E CIEDE2000","text":"

The \u2206E*00 distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*00\u00a0(CIEDE2000) 2000 kl=1, kc=1, kh=1, space='lab-d65'

Since the 1994 definition did not adequately resolve the perceptual uniformity issue, the CIE refined their definition, adding five corrections:

  • A hue rotation term (RT), to deal with the problematic blue region (hue angles in the neighborhood of 275\u00b0)
  • Compensation for neutral colors (the primed values in the LCh differences)
  • Compensation for lightness (SL)
  • Compensation for chroma (SC)
  • Compensation for hue (SH)

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"2000\")\n52.87819528592645\n>>> Color(\"red\").delta_e(\"blue\", method=\"2000\", space='lab')\n55.79977339019779\n
"},{"location":"distance/#delta-e-hyab","title":"Delta E HyAB","text":"

The \u2206EHyAB distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206EHyAB\u00a0(HyAB) hyab space=\"lab-d65\"

A combination of a Euclidean metric in hue and chroma with a city\u2010block metric to incorporate lightness differences. It can be used on any Lab like color space, the default being CIELab D65.

"},{"location":"distance/#delta-e-ok","title":"Delta E OK","text":"

The \u2206Eok distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Eok ok scalar=1

A color distancing algorithm that performs Euclidean distancing in the Oklab color space. This is used in the OkLCh Chroma gamut mapping algorithm. The scalar parameter allows you to scale the result up if desired.

"},{"location":"distance/#delta-e-itp","title":"Delta E ITP","text":"

The \u2206Eitp distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Eitp\u00a0(ICtCp) itp scalar=720

Various algorithms are designed for and perform decently in the SDR range, but \u2206Eitp aims to provide good distancing in the HDR range using the ICtCp color space (must be registered in order to use \u2206Eitp). It was determined that a scalar of 240 was more comparable to the average \u2206E*00 result from the JND data set and 720 equates them to a JND.

Both the ICtCp color space and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_itp import DEITP\nfrom coloraide.spaces.ictcp import ICtCp\n\nclass Color(Base): ...\n\nColor.register([ICtCp(), DEITP()])\n
"},{"location":"distance/#delta-e-z","title":"Delta E Z","text":"

The \u2206Ez distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Ez\u00a0(Jzazbz) jz

Performs Euclidean distancing in the Jzazbz color space, useful for the HDR range.

Both the Jzazbz color space and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_z import DEZ\nfrom coloraide.spaces.jzazbz import Jzazbz\n\nclass Color(Base): ...\n\nColor.register([Jzazbz(), DEZ()])\n
"},{"location":"distance/#delta-e-99o","title":"Delta E 99o","text":"

The \u2206E99o distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E99o\u00a0(DIN99o) 99o

\u2206E99o performs Euclidean distancing in the DIN99o color space.

Both the DIN99o color space and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_99o import DE99o\nfrom coloraide.spaces.din99o import DIN99o\n\nclass Color(Base): ...\n\nColor.register([DIN99o(), DE99o()])\n
"},{"location":"distance/#delta-e-cam16","title":"Delta E CAM16","text":"

The \u2206Ecam16 distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Ecam16 cam16 model='ucs'

The CAM16 UCS uniform color space applies an additional nonlinear transformation to lightness and colorfulness so that a color difference metric \u0394E can be based more closely on Euclidean distance. This algorithm performs distancing using the CAM16 UCS color space. If desired model can be changed to use the SCD or LCD model for \"small\" and \"large\" distancing respectively

Parameter Default Small Large model ucs scd lcd

The one or more of the CAM16 (UCS/SCD/LCD) color spaces and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_cam16 import DECAM16\nfrom coloraide.spaces.cam16_ucs import CAM16UCS, CAM16SCD, CAM16LCD\n\nclass Color(Base): ...\n\nColor.register([CAM16UCS(), CAM16SCD(), CAM16LCD(), DECAM16()])\n
"},{"location":"distance/#delta-e-hct","title":"Delta E HCT","text":"

The \u2206Ehct distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206EHCT hct

This takes the HCT color space C and H components (CAM16's M and h) and converts them to CAM16 UCS M and h, and applies Euclidean distancing on them along with the T component (CIELab's L*). This is necessary for the HCT Chroma gamut mapping approach.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_hct import DEHCT\nfrom coloraide.spaces.HCT import HCT\n\nclass Color(Base): ...\n\nColor.register([HCT(), DEHCT()])\n
"},{"location":"distance/#finding-closest-color","title":"Finding Closest Color","text":"

ColorAide implements a simple way to find the closest color, given a list of colors, to another color. The method is called closest and takes a list of colors that are to be compared to the calling color object. The first color with the smallest distance between the calling color object and itself will be considered the nearest/closest color.

Consider the following example. Here we provide a list of colors to compare against red. After comparing all the colors, the closest ends up being maroon.

>>> Color('red').closest(['pink', 'yellow', 'green', 'blue', 'purple', 'maroon'])\ncolor(srgb 0.50196 0 0 / 1)\n

The default distancing method is used if one is not supplied, but others can be used:

>>> Color('red').closest(['pink', 'yellow', 'green', 'blue', 'purple', 'maroon'], method='2000')\ncolor(srgb 0.50196 0 0 / 1)\n
"},{"location":"distance/#configuring-delta-e-defaults","title":"Configuring Delta E Defaults","text":"

A number of distancing algorithms have configurable features that can be set on demand. If you'd like to have these options set by default, you create a custom class and register the the plugins with the defaults of your choice.

In this example, we will configure \u2206E*00 to use CIE Lab D50 instead of D65 by default.

>>> from coloraide import Color as Base\n>>> from coloraide.distance.delta_e_2000 import DE2000\n>>> class Color(Base):\n...     ...\n... \n>>> Color.register(DE2000(space='lab'), overwrite=True)\n>>> Color('red').delta_e('blue', method='2000')\n55.79977339019779\n>>> Color('red').delta_e('blue', method='2000', space='lab-d65')\n52.87819528592645\n
"},{"location":"filters/","title":"Filters","text":"

ColorAide implements a number of filters with each filter being provided as a plugin. Filters simply apply some logic to transform a color in some specific way. Filters can be used to lighten colors, adjust saturation, or completely change the color. Filters can even be used to simulate things like color vision deficiencies.

"},{"location":"filters/#w3c-filter-effects","title":"W3C Filter Effects","text":"

The W3C Filter Effects Module Level 1 specification outline a number of filters for use in SVG and CSS. ColorAide implements all the filters that directly apply to colors. By default, filters are applied in the Linear sRGB color space, but can be applied in sRGB if requested. All other color spaces will throw an error.

NormalBrightnessSaturateContrastOpacityInvertHue RotateSepiaGrayscale

To apply a specific filter in ColorAide, just call the filter() method with the name of the filter you wish to use. If an amount is not provided, the default according to the W3C spec will be used instead.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('brightness', 0.5).clip() for c in colors])\n[color(srgb-linear 0.5 0 0 / 1), color(srgb-linear 0.5 0.07796 0 / 1), color(srgb-linear 0.5 0.27286 0 / 1), color(srgb-linear 0.5 0.5 0 / 1), color(srgb-linear 0.04542 0.20186 0 / 1), color(srgb-linear 0 0.04579 0.04542 / 1), color(srgb-linear 0 0 0.5 / 1), color(srgb-linear 0.01595 0 0.20539 / 1), color(srgb-linear 0.11038 0.01225 0.19066 / 1), color(srgb-linear 0.4275 0.11161 0.4275 / 1)]\n>>> Steps([c.filter('saturate', 0.5).clip() for c in colors])\n[color(srgb-linear 0.6065 0.1065 0.1065 / 1), color(srgb-linear 0.66224 0.24021 0.16224 / 1), color(srgb-linear 0.8016 0.57446 0.3016 / 1), color(srgb-linear 0.964 0.964 0.464 / 1), color(srgb-linear 0.19943 0.35587 0.15401 / 1), color(srgb-linear 0.03601 0.0818 0.08143 / 1), color(srgb-linear 0.036 0.036 0.536 / 1), color(srgb-linear 0.03413 0.01818 0.22357 / 1), color(srgb-linear 0.15637 0.05825 0.23666 / 1), color(srgb-linear 0.62914 0.31325 0.62914 / 1)]\n>>> Steps([c.filter('contrast', 0.8).clip() for c in colors])\n[color(srgb-linear 0.9 0.1 0.1 / 1), color(srgb-linear 0.9 0.22474 0.1 / 1), color(srgb-linear 0.9 0.53658 0.1 / 1), color(srgb-linear 0.9 0.9 0.1 / 1), color(srgb-linear 0.17267 0.42298 0.1 / 1), color(srgb-linear 0.1 0.17326 0.17267 / 1), color(srgb-linear 0.1 0.1 0.9 / 1), color(srgb-linear 0.12552 0.1 0.42862 / 1), color(srgb-linear 0.2766 0.1196 0.40506 / 1), color(srgb-linear 0.78399 0.27858 0.78399 / 1)]\n>>> Steps([c.filter('opacity', 0.5).clip() for c in colors])\n[color(srgb-linear 1 0 0 / 0.5), color(srgb-linear 1 0.15593 0 / 0.5), color(srgb-linear 1 0.54572 0 / 0.5), color(srgb-linear 1 1 0 / 0.5), color(srgb-linear 0.09084 0.40373 0 / 0.5), color(srgb-linear 0 0.09158 0.09084 / 0.5), color(srgb-linear 0 0 1 / 0.5), color(srgb-linear 0.0319 0 0.41077 / 0.5), color(srgb-linear 0.22076 0.0245 0.38133 / 0.5), color(srgb-linear 0.85499 0.22323 0.85499 / 0.5)]\n>>> Steps([c.filter('invert', 1).clip() for c in colors])\n[color(srgb-linear 0 1 1 / 1), color(srgb-linear 0 0.84407 1 / 1), color(srgb-linear 0 0.45428 1 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.90916 0.59627 1 / 1), color(srgb-linear 1 0.90842 0.90916 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.9681 1 0.58923 / 1), color(srgb-linear 0.77924 0.9755 0.61867 / 1), color(srgb-linear 0.14501 0.77677 0.14501 / 1)]\n>>> Steps([c.filter('hue-rotate', 90).clip() for c in colors])\n[color(srgb-linear 0 0.356 0 / 1), color(srgb-linear 0 0.48932 0 / 1), color(srgb-linear 0 0.82259 0.20639 / 1), color(srgb-linear 0 1 0.856 / 1), color(srgb-linear 0 0.37753 0.52519 / 1), color(srgb-linear 0.09084 0.05913 0.14404 / 1), color(srgb-linear 1 0 0.144 / 1), color(srgb-linear 0.41077 0 0.04084 / 1), color(srgb-linear 0.38133 0.01908 0 / 1), color(srgb-linear 0.85499 0.31483 0 / 1)]\n>>> Steps([c.filter('sepia', 1).clip() for c in colors])\n[color(srgb-linear 0.393 0.349 0.272 / 1), color(srgb-linear 0.51291 0.45597 0.35526 / 1), color(srgb-linear 0.81266 0.72337 0.56342 / 1), color(srgb-linear 1 1 0.806 / 1), color(srgb-linear 0.34617 0.30866 0.2403 / 1), color(srgb-linear 0.08759 0.07808 0.0608 / 1), color(srgb-linear 0.189 0.168 0.131 / 1), color(srgb-linear 0.09017 0.08014 0.06249 / 1), color(srgb-linear 0.17767 0.15791 0.12308 / 1), color(srgb-linear 0.66927 0.59517 0.46377 / 1)]\n>>> Steps([c.filter('grayscale', 1).clip() for c in colors])\n[color(srgb-linear 0.2126 0.2126 0.2126 / 1), color(srgb-linear 0.32412 0.32412 0.32412 / 1), color(srgb-linear 0.6029 0.6029 0.6029 / 1), color(srgb-linear 0.9278 0.9278 0.9278 / 1), color(srgb-linear 0.30806 0.30806 0.30806 / 1), color(srgb-linear 0.07205 0.07205 0.07205 / 1), color(srgb-linear 0.0722 0.0722 0.0722 / 1), color(srgb-linear 0.03644 0.03644 0.03644 / 1), color(srgb-linear 0.09199 0.09199 0.09199 / 1), color(srgb-linear 0.40315 0.40315 0.40315 / 1)]\n

Tip

filter() can output the results in any color space you need by setting out_space.

>>> Color('#07c7ed').filter('grayscale', 1, out_space='hsl')\ncolor(--hsl none 0 0.71528 / 1)\n
"},{"location":"filters/#color-vision-deficiency-simulation","title":"Color Vision Deficiency Simulation","text":"

Color blindness or color vision deficiency (CVD) affects approximately 1 in 12 men (8%) and 1 in 200 women. CVD affects millions of people in the world, and many people have no idea that they are color blind and not seeing the full spectrum that others see.

CVD simulation allows those who do not suffer with one of the many different variations of color blindness, to simulate what someone with a CVD would see. Keep in mind that these are just approximations, and that a given type of CVD can be quite different from person to person in severity.

The human eye has 3 types of cones that are used to perceive colors. Each of these cones can become deficient, either through genetics, or other means. Each type of cone is responsible for perceiving different wavelengths of light. A CVD occurs when one or more of these cones are missing or not functioning properly. There are severe cases where one of the three cones will not perceive color at all, and there are others where the cones may just be less sensitive.

"},{"location":"filters/#dichromacy","title":"Dichromacy","text":"

Dichromacy is a type of CVD that has the characteristics of essentially causing the person to only have two functioning cones for perceiving colors. This essentially flattens the color spectrum into a 2D plane. Protanopia describes the CVD where the cone responsible for long wavelengths does not function, deuteranopia describes the CVD affecting the cone responsible for processing medium wavelengths, and tritanopia describes deficiencies with the cone responsible for short wavelengths.

NormalProtanopiaDeuteranopiaTritanopia

One misconception is that people with CVD have a color blindness for just red and green or something similar as that can often be how it is described, and while the statement is true that certain people with CVD may have trouble with red and green, they often can have trouble with other colors as well.

The LMS color space was created to mimic the response of the human eye. Each channel represents one of the 3 cones with each cone responsible for seeing light waves of different frequencies: long (L), medium (M), and short (S). Protanopia represents deficiencies with the L cone, deuteranopia with the M cone, and tritanopia with the S cone. Any color whose properties only vary in the properties specific to a person's deficient cone(s) will have the potential to cause confusion for that person.

Consider the example below. We generate 3 different color series, each specifically targeting a specific deficiency. This is done by generating a series of colors that have all properties equal except that they have variance in a different cone response. The first row varies only with the L cone response, the second only with the M cone response, and the third only with the S cone response. We then apply the filters for protanopia, deuteranopia, and tritanopia. We can see that while many of the colors are altered, the row that targets the deficient cone specific to the CVD all appear to be of the same color making it difficult to distinguish between any of them.

NormalProtanopiaDeuteranopiaTritanopia
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.clip() for c in confusing_colors])\n[color(srgb 0 0.73785 0.0565 / 1), color(srgb 0.48453 0.72034 0.0466 / 1), color(srgb 0.66519 0.70225 0.03524 / 1), color(srgb 0.79774 0.68354 0.02349 / 1), color(srgb 0.90633 0.66414 0.01175 / 1), color(srgb 1 0.64398 0 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.clip() for c in confusing_colors])\n[color(srgb 1 0.40107 0.70629 / 1), color(srgb 0.90666 0.5039 0.70182 / 1), color(srgb 0.7985 0.58535 0.69731 / 1), color(srgb 0.66664 0.65439 0.69277 / 1), color(srgb 0.48742 0.71511 0.68818 / 1), color(srgb 0.04172 0.76977 0.68356 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.clip() for c in confusing_colors])\n[color(srgb 0.18039 0.5451 0.34118 / 1), color(srgb 0.26982 0.51394 0.56225 / 1), color(srgb 0.33367 0.4802 0.70642 / 1), color(srgb 0.38543 0.44317 0.81991 / 1), color(srgb 0.42983 0.40182 0.91581 / 1), color(srgb 0.46916 0.35444 1 / 1)]\n
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.filter('protan').clip() for c in confusing_colors])\n[color(srgb-linear 0.44718 0.44718 0.00253 / 1), color(srgb-linear 0.44632 0.44632 0.00253 / 1), color(srgb-linear 0.44545 0.44545 0.00252 / 1), color(srgb-linear 0.44459 0.44459 0.00252 / 1), color(srgb-linear 0.44372 0.44372 0.00252 / 1), color(srgb-linear 0.44286 0.44286 0.00251 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.filter('protan').clip() for c in confusing_colors])\n[color(srgb-linear 0.23099 0.23099 0.46046 / 1), color(srgb-linear 0.28318 0.28318 0.45292 / 1), color(srgb-linear 0.33538 0.33538 0.44537 / 1), color(srgb-linear 0.38758 0.38758 0.43782 / 1), color(srgb-linear 0.43978 0.43978 0.43027 / 1), color(srgb-linear 0.49197 0.49197 0.42273 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.filter('protan').clip() for c in confusing_colors])\n[color(srgb-linear 0.23224 0.23224 0.09438 / 1), color(srgb-linear 0.20829 0.20829 0.27557 / 1), color(srgb-linear 0.18435 0.18435 0.45676 / 1), color(srgb-linear 0.16041 0.16041 0.63795 / 1), color(srgb-linear 0.13646 0.13646 0.81914 / 1), color(srgb-linear 0.11252 0.11252 1 / 1)]\n
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.filter('deutan').clip() for c in confusing_colors])\n[color(srgb-linear 0.35631 0.35631 0.0158 / 1), color(srgb-linear 0.39626 0.39626 0.00984 / 1), color(srgb-linear 0.43622 0.43622 0.00387 / 1), color(srgb-linear 0.47617 0.47617 0 / 1), color(srgb-linear 0.51612 0.51612 0 / 1), color(srgb-linear 0.55607 0.55607 0 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.filter('deutan').clip() for c in confusing_colors])\n[color(srgb-linear 0.38725 0.38725 0.43764 / 1), color(srgb-linear 0.38833 0.38833 0.43756 / 1), color(srgb-linear 0.38941 0.38941 0.43748 / 1), color(srgb-linear 0.3905 0.3905 0.43739 / 1), color(srgb-linear 0.39158 0.39158 0.43731 / 1), color(srgb-linear 0.39266 0.39266 0.43723 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.filter('deutan').clip() for c in confusing_colors])\n[color(srgb-linear 0.1906 0.1906 0.10046 / 1), color(srgb-linear 0.17799 0.17799 0.28 / 1), color(srgb-linear 0.16539 0.16539 0.45953 / 1), color(srgb-linear 0.15278 0.15278 0.63907 / 1), color(srgb-linear 0.14018 0.14018 0.8186 / 1), color(srgb-linear 0.12757 0.12757 0.99814 / 1)]\n
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.filter('tritan').clip() for c in confusing_colors])\n[color(srgb-linear 0.09504 0.41129 0.56924 / 1), color(srgb-linear 0.27752 0.40204 0.46424 / 1), color(srgb-linear 0.46584 0.38712 0.3939 / 1), color(srgb-linear 0.66496 0.36167 0.3878 / 1), color(srgb-linear 0.86409 0.33623 0.38171 / 1), color(srgb-linear 1 0.31078 0.37561 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.filter('tritan').clip() for c in confusing_colors])\n[color(srgb-linear 0.96312 0.16951 0.23789 / 1), color(srgb-linear 0.77356 0.24403 0.28965 / 1), color(srgb-linear 0.584 0.31855 0.34142 / 1), color(srgb-linear 0.39444 0.39306 0.39318 / 1), color(srgb-linear 0.22435 0.44862 0.56064 / 1), color(srgb-linear 0.05436 0.50409 0.7287 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.filter('tritan').clip() for c in confusing_colors])\n[color(srgb-linear 0.06253 0.22391 0.30451 / 1), color(srgb-linear 0.06359 0.22288 0.30244 / 1), color(srgb-linear 0.06464 0.22186 0.30038 / 1), color(srgb-linear 0.0657 0.22083 0.29831 / 1), color(srgb-linear 0.06675 0.2198 0.29624 / 1), color(srgb-linear 0.06781 0.21877 0.29417 / 1)]\n

By default, ColorAide uses the Brettel 1997 method to simulate tritanopia as it is the only option that has decent accuracy for tritanopia. Vi\u00e9not, Brettel, and Mollon 1999 approach is used to simulate protanopia and deuteranopia as it is not only faster than Brettel, but handles extreme case a little better. Machado 2009 has it strengths as well which we will cover in Anomalous Trichromacy.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('protan').clip() for c in colors])\n[color(srgb-linear 0.11238 0.11238 0.00401 / 1), color(srgb-linear 0.25079 0.25079 0.00338 / 1), color(srgb-linear 0.59678 0.59678 0.00182 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.36856 0.36856 0 / 1), color(srgb-linear 0.08129 0.08129 0.09047 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.00358 0.00358 0.4109 / 1), color(srgb-linear 0.04655 0.04655 0.38211 / 1), color(srgb-linear 0.29423 0.29423 0.85752 / 1)]\n>>> Steps([c.filter('deutan').clip() for c in colors])\n[color(srgb-linear 0.29275 0.29275 0 / 1), color(srgb-linear 0.40303 0.40303 0 / 1), color(srgb-linear 0.67871 0.67871 0 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.31213 0.31213 0.00699 / 1), color(srgb-linear 0.06477 0.06477 0.09289 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.00934 0.00934 0.41006 / 1), color(srgb-linear 0.08195 0.08195 0.37694 / 1), color(srgb-linear 0.40818 0.40818 0.84088 / 1)]\n>>> Steps([c.filter('tritan').clip() for c in colors])\n[color(srgb-linear 1 0 0.07589 / 1), color(srgb-linear 1 0.12293 0.20142 / 1), color(srgb-linear 1 0.46132 0.5152 / 1), color(srgb-linear 1 0.85569 0.88089 / 1), color(srgb-linear 0.16172 0.33473 0.42114 / 1), color(srgb-linear 0.00588 0.08585 0.12579 / 1), color(srgb-linear 0 0.1232 0.24795 / 1), color(srgb-linear 0 0.05257 0.08987 / 1), color(srgb-linear 0.17036 0.07355 0.08189 / 1), color(srgb-linear 0.7694 0.30654 0.34642 / 1)]\n

If desired, any of the three available methods can be used.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('tritan', method='brettel').clip() for c in colors])\n[color(srgb-linear 1 0 0.07589 / 1), color(srgb-linear 1 0.12293 0.20142 / 1), color(srgb-linear 1 0.46132 0.5152 / 1), color(srgb-linear 1 0.85569 0.88089 / 1), color(srgb-linear 0.16172 0.33473 0.42114 / 1), color(srgb-linear 0.00588 0.08585 0.12579 / 1), color(srgb-linear 0 0.1232 0.24795 / 1), color(srgb-linear 0 0.05257 0.08987 / 1), color(srgb-linear 0.17036 0.07355 0.08189 / 1), color(srgb-linear 0.7694 0.30654 0.34642 / 1)]\n>>> Steps([c.filter('tritan', method='vienot').clip() for c in colors])\n[color(srgb-linear 1 0 0 / 1), color(srgb-linear 1 0.13398 0.13398 / 1), color(srgb-linear 1 0.46891 0.46891 / 1), color(srgb-linear 1 0.85924 0.85924 / 1), color(srgb-linear 0.14923 0.3469 0.3469 / 1), color(srgb-linear 0.00011 0.09147 0.09147 / 1), color(srgb-linear 0 0.14076 0.14076 / 1), color(srgb-linear 0 0.05782 0.05782 / 1), color(srgb-linear 0.16915 0.07473 0.07473 / 1), color(srgb-linear 0.76363 0.31216 0.31216 / 1)]\n>>> Steps([c.filter('tritan', method='machado').clip() for c in colors])\n[color(srgb-linear 1 0 0.00473 / 1), color(srgb-linear 1 0.06673 0.11254 / 1), color(srgb-linear 1 0.42955 0.38203 / 1), color(srgb-linear 1 0.8524 0.6961 / 1), color(srgb-linear 0.08307 0.36867 0.27955 / 1), color(srgb-linear 0 0.09865 0.09092 / 1), color(srgb-linear 0 0.1476 0.3039 / 1), color(srgb-linear 0 0.05813 0.12498 / 1), color(srgb-linear 0.20711 0.06178 0.13387 / 1), color(srgb-linear 0.90348 0.26694 0.41821 / 1)]\n
"},{"location":"filters/#anomalous-trichromacy","title":"Anomalous Trichromacy","text":"

While Dichromacy is probably the more severe case with only two functional cones, a more common CVD type is anomalous trichromacy. In this case, a person will have three functioning cones, but not all of the cones function with full sensitivity. Sometimes, the sensitivity can be so low, that their ability to perceive color may be close to someone with dichromacy.

While dichromacy may be considered a severity 1, a given case of anomalous trichromacy could be anywhere between 0 and 1, where 0 would be no CVD.

Like dichromacy, the related deficiencies are named in a similar manner: protanomaly (reduced red sensitivity), deuteranomaly (reduced green sensitivity), and tritanomaly (reduced blue sensitivity).

NormalProtanomaly Severity 0.5Protanomaly Severity 0.7Protanomaly Severity 0.9

To represent anomalous trichromacy, ColorAide leans on the Machado 2009 approach which has a more nuanced approach to handling severity levels below 1. This research associated with this method did not really focus on tritanopia though, and Brettel is still a better choice for tritanopia. Instead of relying on the Machado approach for tritanomaly, we instead just use linear interpolation between the severity 1 results and the severity 0 (no CVD) results.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('protan', 0.3).clip() for c in colors])\n[color(srgb-linear 0.63032 0.06918 0 / 1), color(srgb-linear 0.70293 0.20796 0 / 1), color(srgb-linear 0.88443 0.5549 0 / 1), color(srgb-linear 1 0.95923 0 / 1), color(srgb-linear 0.24525 0.36562 0 / 1), color(srgb-linear 0.03392 0.08521 0.09141 / 1), color(srgb-linear 0 0.04077 1 / 1), color(srgb-linear 0 0.01895 0.41633 / 1), color(srgb-linear 0.11396 0.05262 0.3851 / 1), color(srgb-linear 0.56082 0.29269 0.85987 / 1)]\n>>> Steps([c.filter('protan', 0.5).clip() for c in colors])\n[color(srgb-linear 0.45806 0.09279 0 / 1), color(srgb-linear 0.56403 0.22475 0 / 1), color(srgb-linear 0.82893 0.55464 0 / 1), color(srgb-linear 1 0.9391 0 / 1), color(srgb-linear 0.31598 0.35011 0 / 1), color(srgb-linear 0.04973 0.08304 0.09151 / 1), color(srgb-linear 0 0.0609 1 / 1), color(srgb-linear 0 0.02798 0.42051 / 1), color(srgb-linear 0.06528 0.06444 0.38853 / 1), color(srgb-linear 0.42566 0.32032 0.86561 / 1)]\n>>> Steps([c.filter('protan', 0.9).clip() for c in colors])\n[color(srgb-linear 0.20388 0.11298 0 / 1), color(srgb-linear 0.3583 0.23687 0 / 1), color(srgb-linear 0.74433 0.54658 0 / 1), color(srgb-linear 1 0.90752 0 / 1), color(srgb-linear 0.41835 0.33104 0 / 1), color(srgb-linear 0.07305 0.08116 0.09129 / 1), color(srgb-linear 0 0.09248 1 / 1), color(srgb-linear 0 0.04159 0.42961 / 1), color(srgb-linear 0 0.07967 0.39681 / 1), color(srgb-linear 0.22933 0.35303 0.88092 / 1)]\n

The Brettel and Vi\u00e9not approach can be used for severities below 1 as well, but, like Brettel with tritanopia, they will employ simple linear interpolation between a severity 1 case ans the actual color. It is probably debatable as to whether this approach is sufficient or not.

"},{"location":"filters/#usage-details","title":"Usage Details","text":"

To use filters, a filter name must be given, followed by an optional amount. If an amount is omitted, suitable default will be used. The exact range a given filter accepts varies depending on the filter. If a value exceeds the filter range , the value will be clamped.

Filters Name Default Brightness brightness 1 Saturation saturate 1 Contrast contrast 1 Opacity opacity 1 Invert invert 1 Hue\u00a0rotation hue-rotate 0 Sepia sepia 1 Grayscale grayscale 1 Protan protan 1 Deutan deutan 1 Tritan tritan 1

All of the filters that are supported allow filtering in the Linear sRGB color space and will do so by default. Additionally, the W3C filter effects also support filtering in the sRGB color space. The CVD filters are specifically designed to be applied in the Linear sRGB space, and cannot be used in any other color space.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('sepia', 1, space='srgb-linear').clip() for c in colors])\n[color(srgb-linear 0.393 0.349 0.272 / 1), color(srgb-linear 0.51291 0.45597 0.35526 / 1), color(srgb-linear 0.81266 0.72337 0.56342 / 1), color(srgb-linear 1 1 0.806 / 1), color(srgb-linear 0.34617 0.30866 0.2403 / 1), color(srgb-linear 0.08759 0.07808 0.0608 / 1), color(srgb-linear 0.189 0.168 0.131 / 1), color(srgb-linear 0.09017 0.08014 0.06249 / 1), color(srgb-linear 0.17767 0.15791 0.12308 / 1), color(srgb-linear 0.66927 0.59517 0.46377 / 1)]\n>>> Steps([c.filter('sepia', 1, space='srgb').clip() for c in colors])\n[color(srgb 0.393 0.349 0.272 / 1), color(srgb 0.72473 0.64492 0.50235 / 1), color(srgb 0.98106 0.87359 0.68035 / 1), color(srgb 1 1 0.806 / 1), color(srgb 0.64467 0.57456 0.44736 / 1), color(srgb 0.32034 0.28556 0.22236 / 1), color(srgb 0.189 0.168 0.131 / 1), color(srgb 0.20429 0.18153 0.14152 / 1), color(srgb 0.45304 0.40295 0.31398 / 1), color(srgb 0.93524 0.83226 0.64837 / 1)]\n

Processing Lots of Colors

One logical application for filters is to apply them directly to images. If you are performing these operations on millions of pixels, you may notice that ColorAide, with all of its convenience, may not always be the fastest. There is a cost due to the overhead of convenience and a cost due to the pure Python approach as well. With that said, there are tricks that can dramatically make things much faster in most cases!

functools.lru_cache is your friend in such cases. We actually process all the images on this page with ColorAide to demonstrate the filters. The key to making it a quick and painless process was to cache repetitive operations. When processing images, it is highly likely that you will be performing the same operations on thousands of identical pixels. Caching the work you've already done can speed this process up exponentially.

There are certainly some images that could be constructed in such a way to elicit a worse case scenario where the cache would not be able to compensate as well, but for most images, caching dramatically reduces processing time.

We can crawl the pixels in a file, and using a simple function like below, we will only process a pixel once (at least until our cache fills and we start having to overwrite existing colors).

@lru_cache(maxsize=1024 * 1024)\ndef apply_filter(name, amount, space, method, p, fit):\n\"\"\"Apply filter.\"\"\"\n\n    has_alpha = len(p) > 3\n    color = Color('srgb', [x / 255 for x in p[:3]], p[3] / 255 if has_alpha else 1)\n    if method is not None:\n        # This is a CVD filter that allows specifying the method\n        color.filter(name, amount, space=space, in_place=True, method=method)\n    else:\n        # General filter.\n        color.filter(name, amount, space=space, in_place=True)\n    # Fit the color back into the color gamut and return the results\n    return tuple([int(x * 255) for x in color.fit(method=fit)[:3 if has_alpha else -1]])\n

When processing a 4608x2456 image (15,925,248 pixels) during our testing, it turned a ~7 minute process into a ~25 second process*. Using gamut mapping opposed to simple clipping only increases time by to about ~56 seconds. The much smaller images shown on this page process much, much faster.

The full script can be viewed here.

* Tests were performed using the Pillow library. Results may vary depending on the size of the image, pixel configuration, number of unique pixels, etc. Cache size can be tweaked to optimize the results.

"},{"location":"gamut/","title":"Gamut Mapping","text":"

Many color spaces are designed in such a way that they can only represent colors accurately within a specific range. This range in which a color can accurately be represented is known as the color gamut. While some color spaces are theoretically unbounded, there are many that are designed with distinct ranges.

The sRGB and Display P3 color spaces are both RGB color spaces, but they actually can represent a different amount of colors. Display P3 has a wider gamut and allows for greener greens and redder reds, etc. In the image below, we show four different RGB color spaces, each with varying different gamut sizes. Display P3 contains all the colors in sRGB and extends it even further. Rec. 2020, another RGB color space, is even wider. ProPhoto is so wide that it contains colors that the human eye can't even see.

In order to visually represent a color from a wider gamut color space, such as Display P3, in a more narrow color space, such as sRGB, a suitable color within the more narrow color space must must be selected and be shown in its place. This selecting of a suitable replacement is called gamut mapping.

ColorAide defines a couple methods to help identify when a color is outside the gamut bounds of a color space and to help find a suitable, alternative color that is within the gamut.

"},{"location":"gamut/#checking-gamut","title":"Checking Gamut","text":"

When dealing with colors, it can be important to know whether a color is within its own gamut. The in_gamut function allows for comparing the current color's specified values against the color space's gamut.

Let's assume we have a color rgb(30% 105% 0%). The color is out of gamut due to the green channel exceeding the channel's limit of 100%. When we execute in_gamut, we can see that the color is not in its own gamut.

>>> Color(\"rgb(30% 105% 0%)\").in_gamut()\nFalse\n

On the other hand, some color spaces do not have a limit. CIELab is one such color space. Sometimes limits will be placed on the color space channels for practicality, but theoretically, there are no bounds. When we check a CIELab color, we will find that it is always considered in gamut.

>>> Color(\"lab(200% -20 40 / 1)\").in_gamut()\nTrue\n

While checking CIELab's own gamut isn't very useful, we can test it against a different color space's gamut. By simply passing in the name of a different color space, the current color will be converted to the provided space and then will run in_gamut on the new color. You could do this manually, but using in_gamut in this manner can be very convenient. In the example below, we can see that the CIELab color of lab(200% -20 40 / 1) is outside the narrow gamut of sRGB.

>>> Color(\"lab(200% -20 40 / 1)\").in_gamut('srgb')\nFalse\n
"},{"location":"gamut/#tolerance","title":"Tolerance","text":"

Generally, ColorAide does not round off values in order to guarantee the best possible values for round tripping, but due to limitations of floating-point arithmetic and precision of conversion algorithms, there can be edge cases where colors don't round trip perfectly. By default, in_gamut allows for a tolerance of 0.000075 to account for such cases where a color is \"close enough\". If desired, this \"tolerance\" can be adjusted.

Let's consider CIELab with a D65 white point. The sRGB round trip through CIELab D65 for white does not perfectly convert back to the original color. This is due to the perils of floating point arithmetic.

>>> Color('color(srgb 1 1 1)').convert('lab-d65')[:]\n[100.0, 0.0, 0.0, 1.0]\n>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb')[:]\n[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]\n

We can see that when using a tolerance of zero, and gamut checking in sRGB, that the color is considered out of gamut. This makes sense as the round trip through CIELab D65 and back is so very close, but ever so slightly off. Depending on what you are doing, this may not be an issue up until you are ready to finalize the color, so sometimes it may be desirable to have some tolerance, and other times not.

>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb')[:]\n[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]\n>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb').in_gamut()\nTrue\n>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb').in_gamut(tolerance=0)\nTrue\n

On the topic of tolerance, lets consider some color models that do not handle out of gamut colors very well. There are some color models that are alternate representations of an existing color space. For instance, the cylindrical spaces HSL, HSV, and HWB are just different color models for the sRGB color space. They are are essentially the sRGB color space, just with cylindrical coordinates that isolate certain attributes of the color space: saturation, whiteness, blackness, etc. So their gamut is exactly the same as the sRGB space, because they are the sRGB color space. So it stands to reason that simply using the sRGB gamut check for them should be sufficient, and if we are using strict tolerance, this would be true.

>>> Color('rgb(255 255 255)').in_gamut('srgb', tolerance=0)\nTrue\n>>> Color('hsl(0 0% 100%)').in_gamut('srgb', tolerance=0)\nTrue\n>>> Color('color(--hsv 0 0% 100%)').in_gamut('srgb', tolerance=0)\nTrue\n>>> Color('rgb(255.05 255 255)').in_gamut('srgb', tolerance=0)\nFalse\n>>> Color('hsl(0 0% 100.05%)').in_gamut('srgb', tolerance=0)\nFalse\n>>> Color('color(--hsv 0 0% 100.05%)').in_gamut('srgb', tolerance=0)\nFalse\n

But when we are not using a strict threshold, and we check one of these models only using the sRGB gamut, there are some cases where these cylindrical colors can exhibit coordinates wildly outside of the model's range but still very close to the sRGB gamut.

In this example, we have an sRGB color that is extremely close to being in gamut, but when we convert it to HSL, we can see wildly large saturation.

>>> hsl = Color('color(srgb 1 1.000002 1)').convert('hsl')\n>>> hsl.to_string(fit=False)\n'hsl(120 -100% 100%)'\n>>> hsl.in_gamut('srgb')\nTrue\n

This happens because these cylindrical color models do not represent colors in a very sane way when lightness exceeds the SDR range of 0 - 1. They are simply not designed to extend past such limits in a sane way. So even a slightly out of gamut sRGB color could translate to a value way outside the cylindrical color model's boundaries.

For this reason, gamut checks in the HSL, HSV, or HWB models apply tolerance checks on the color's coordinates in the sRGB color space and the respective cylindrical model ensuring we have coordinates that are close to the color's actual gamut and reasonably close to the cylindrical model's constraints as well.

So, when using HSL as the gamut check, we can see that it ensures the color is not only very close to the sRGB gamut, but that it is also very close the color model's constraints.

>>> hsl = Color('color(srgb 0.9999999999994 1.0000000000002 0.9999999999997)').convert('hsl')\n>>> hsl\ncolor(--hsl none 2.0006 1 / 1)\n>>> hsl.in_gamut('hsl')\nFalse\n

If the Cartesian check is the only desired check, and the strange cylindrical values that are returned are not a problem, srgb can always be specified. tolerance=0 can also be used to constrain the check to values exactly in the gamut.

HSL has a very tight conversion to and from sRGB, so when an sRGB color is precisely in gamut, it will remain in gamut throughout the conversion to and from HSL, both forwards and backwards. On the other hand, there may be color models that have a looser conversion algorithm. There may be cases where it may be beneficial to increase the threshold.

"},{"location":"gamut/#gamut-mapping-colors","title":"Gamut Mapping Colors","text":"

Gamut mapping is the process of taking a color that is out of gamut and adjusting it such that it fits within the gamut. There are various ways to map an out of bound color to an in bound color, each with their own pros and cons. ColorAide offers two methods related to gamut mapping: clip() and fit(). clip() is a dedicated function that performs the speedy, yet naive, approach of simply truncating a color channel's value to fit within the specified gamut, and fit() is a method that allows you to do more advanced gamut mapping approaches that, while slower, generally yield better results.

While clipping won't always yield the best results, clipping is still very important and can be used to trim channel noise after certain mathematical operations or even used in other gamut mapping algorithms if used carefully. For this reason, clip has its own dedicated method for quick access: clip().

>>> Color('rgb(270 30 120)').clip()\ncolor(srgb 1 0.11765 0.47059 / 1)\n

The fit() method, is the generic gamut mapping method that exposes access to all the different gamut mapping methods available. By default, fit() uses a more advanced method of gamut mapping that tries to preserve hue and lightness, hue being the attribute the human eye is most sensitive to. If desired, a user can also specify any currently registered gamut mapping algorithm via the method parameter.

>>> Color('rgb(270 30 120)').fit()\ncolor(srgb 1 0.18505 0.47435 / 1)\n>>> Color('rgb(270 30 120)').fit(method='clip')\ncolor(srgb 1 0.11765 0.47059 / 1)\n

Gamut mapping can also be used to indirectly fit colors in another gamut. For instance, fitting a Display P3 color into an sRGB gamut.

>>> c1 = Color('color(display-p3 1 1 0)')\n>>> c1.in_gamut('srgb')\nFalse\n>>> c1.fit('srgb')\ncolor(display-p3 0.99859 0.9923 0.32854 / 1)\n>>> c1.in_gamut()\nTrue\n

This can also be done with clip().

>>> Color('color(display-p3 1 1 0)').clip('srgb')\ncolor(display-p3 1 1 0.3309 / 1)\n

Indirectly Gamut Mapping a Color Space

When indirectly gamut mapping in another color space, results may vary depending on what color space you are in and what color space you are using to fit the color. The operation may not get the color precisely in gamut. This is because we must convert the color to the gamut mapping space, apply the gamut mapping, and then convert it back to the original color. The process will be subject to any errors that occur in the round trip to and from the targeted space. This is mainly mentioned as fitting in one color space and round tripping back may not give exact results and, in some cases, exceed \"in gamut\" thresholds.

There are actually many different ways to gamut map a color. Some are computationally expensive, some are quite simple, and many do really good in some cases and not so well in others. There is probably no perfect gamut mapping method, but some are better than others.

"},{"location":"gamut/#clip","title":"Clip","text":"

The clip gamut mapping is registered in Color by default and cannot be unregistered

Clipping is a simple and naive approach to gamut mapping. If the color space is bounded by a gamut, clip will compare each channel's value against the bounds for that channel set the value to the limit it exceeds.

Clip can be performed via fit by using the method name clip or by using the clip() method.

>>> c = Color('srgb', [2, 1, 1.5])\n>>> c.fit(method='clip')\ncolor(srgb 1 1 1 / 1)\n>>> c = Color('srgb', [2, 1, 1.5])\n>>> c.clip()\ncolor(srgb 1 1 1 / 1)\n

Clipping is unique to all other clipping methods in that it has its own dedicated method clip() method and that its method name clip is reserved. While not always the best approach for gamut mapping in general, clip is very important to some other gamut mapping and has specific cases where its speed and simplicity are of great value.

"},{"location":"gamut/#lch-chroma","title":"LCh Chroma","text":"

The lch-chroma gamut mapping is registered in Color by default

LCh Chroma uses a combination of chroma reduction and MINDE in the CIELCh color space to bring a color into gamut. By reducing chroma in the CIELCh color space, LCh Chroma can hold hue and lightness in the LCh color space relatively constant. This is currently the default method used.

Note

As most colors in ColorAide use a D65 white point by default, LCh D65 is used as the gamut mapping color space.

The algorithm generally works by performing both clipping and chroma reduction. Using bisection, the chroma is reduced and then the chroma reduced color is clipped. Using \u2206E2000, the distance between the chroma reduced color and the clipped chroma reduced color is measured. If the resultant distance falls within the specified threshold, the clipped color is returned.

Computationally, LCh Chroma is slower to compute than clipping, but generally provides better results. LCh, is not necessarily the best perceptual color space available, but it is generally well understood color space that has been available a long time. It does suffer from a purple shift when dealing with blue colors, but generally can generally colors far out of gamut in a reasonable manner.

While CSS has currently proposed LCh Chroma reduction to be done with OkLCh, and we do offer an OkLCh variant, we currently still use CIELCh as the default until OkLCh can be evaluated more fully.

LCh Chroma is the default gamut mapping algorithm by default, unless otherwise changed, and can be performed by simply calling fit() or by calling fit(method='lch-chroma').

>>> c = Color('srgb', [2, -1, 0])\n>>> c.fit(method='clip')\ncolor(srgb 1 0 0 / 1)\n>>> c = Color('srgb', [2, -1, 0])\n>>> c.fit(method='clip')\ncolor(srgb 1 0 0 / 1)\n
"},{"location":"gamut/#oklch-chroma","title":"OkLCh Chroma","text":"

The lch-chroma gamut mapping is registered in Color by default

The CSS CSS Color Level 4 specification currently recommends using OkLCh as the gamut mapping color space. OkLCh Chroma is performed exactly like LCh Chroma except that it uses the perceptually uniform OkLCh color space as the LCh color space of choice.

OkLCh has the advantage of doing a better job at holding hues uniform than CIELCh.

>>> c = Color('srgb', [2, -1, 0])\n>>> c.fit(method='oklch-chroma')\ncolor(srgb 1 0.60354 0.66617 / 1)\n

OkLCh is a very new color space to be used in the field of gamut mapping. While CIELCh is not perfect, its weakness are known. OkLCh does seem to have certain quirks of its own, and may have more that have yet to be discovered. While we have not made oklch-chroma our default yet, we have exposed the algorithm so users can begin exploring it.

"},{"location":"gamut/#hct-chroma","title":"HCT Chroma","text":"

The hct-chroma gamut mapping is not registered in Color by default

Much like the other LCh chroma reduction algorithms, HCT Chroma performs gamut mapping exactly like LCh Chroma with the exception that it uses the HCT color space as the working LCh color space.

Google's Material Design uses a new color space called HCT. It uses the hue and chroma from CAM16 (JMh) space and the tone/lightness from the CIELab space. HCT takes advantage of the good hue preservation of CAM16 and has the better lightness predictability of CIELab. Using these characteristics, the color space is adept at generating tonal palettes with predictable lightness. This makes it easier to construct UIs with decent contrast. But to do this well, you must work in HCT and gamut map in HCT. For this reason, the HCT Chroma gamut mapping method was added.

HCT Chroma is computationally the most expensive gamut mapping method that is offered. Since the color space used is based on the already computationally expensive CAM16 color space, and is made more expensive by blending that color space with CIELab, it is not the most performant approach, but when used in conjunction with the HCT color space, it can allow creating good tonal palettes:

>>> c = Color('hct', [325, 24, 50])\n>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit='hct-chroma') for tone in tones])\n['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']\n

To HCT Chroma plugin is not registered by default, but can be added by subclassing Color. You must register the \u2206Ehct distancing algorithm and the HCT color space as well.

from coloraide import Color as Base\nfrom coloraide.gamut.fit_hct_chroma import HCTChroma\nfrom coloraide.distance.delta_e_hct import DEHCT\nfrom coloraide.spaces.hct import HCT\n\nclass Color(Base): ...\n\nColor.register([HCT(), DEHCT(), HCTChroma()])\n
"},{"location":"gamut/#why-not-just-clip","title":"Why Not Just Clip?","text":"

In the past, clipping has been the default way in which out of gamut colors have been handled in web browsers. It is fast, and has generally been fine as most browsers have been constrained to using sRGB. But as modern browsers begin to adopt more wide gamut monitors such as Display P3, and CSS grows to support an assortment of wide and ultra wide color spaces, representing the best intent of an out of gamut color becomes even more important.

ColorAide currently uses a default gamut mapping algorithm that performs gamut mapping in the CIELCh color space using chroma reduction coupled with minimum \u2206E (MINDE). This approach is meant to preserve enough of the important attributes of the out of gamut color as is possible, mostly preserving both lightness and hue, hue being the attribute that people are most sensitive to. MINDE is used to abandon chroma reduction and clip the color when the color is very close to being in gamut. MINDE also allows us to catch cases where the geometry of the color space's gamut is such that we may slip by higher chroma options resulting in undesirable, aggressive chroma reduction. While CIELCh is not a perfect color space, and we may use a different color space in the future, this method is generally more accurate that using clipping alone.

Below we have an example of using chroma reduction with MINDE. It can be noted that chroma is reduced until we are very close to being in gamut. The MINDE helps us catch the peak of the yellow shape as, otherwise, we would have continued reducing chroma until we were at a very chroma reduced, pale yellow.

One might see some cases of clipping and think it does a fine job and question why any of this complexity is necessary. In order to demonstrate the differences in gamut mapping vs clipping, see the example below. We start with the color color(display-p3 1 1 0) and interpolate with it in the CIELCh color space reducing just the lightness. This will leave both chroma and hue intact. The Interactive playground below automatically gamut maps the color previews to sRGB, but we'll control the method being used by providing two different Color objects: one that uses lch-chroma (the default) for gamut mapping, and one that uses clip. Notice how clipping, the bottom color set, clips these dark colors and makes them reddish. This is a very undesirable outcome.

>>> yellow = Color('color(display-p3 1 1 0)')\n>>> lightness_mask = Color('lch(0% none none)')\n>>> Row([c.fit('srgb') for c in Color.steps([yellow, lightness_mask], steps=10, space='lch')])\n[color(--lch 97.084 94.208 99.059 / 1), color(--lch 86.627 85.37 97.728 / 1), color(--lch 75.806 76.349 97.429 / 1), color(--lch 64.98 67.345 97.418 / 1), color(--lch 54.144 58.334 97.439 / 1), color(--lch 43.284 49.307 97.535 / 1), color(--lch 32.387 40.256 97.872 / 1), color(--lch 21.425 29.815 98.42 / 1), color(--lch 10.15 15.125 97.724 / 1), color(--lch 0 0 none / 1)]\n>>> yellow = Color('color(display-p3 1 1 0)')\n>>> lightness_mask = Color('lch(0% none none)')\n>>> Row([c.clip('srgb') for c in Color.steps([yellow, lightness_mask], steps=10, space='lch')])\n[color(--lch 97.607 94.712 99.572 / 1), color(--lch 86.823 85.836 100.4 / 1), color(--lch 76.017 76.983 101.52 / 1), color(--lch 65.223 68.164 102.85 / 1), color(--lch 54.47 59.371 104.26 / 1), color(--lch 43.793 50.569 105.45 / 1), color(--lch 33.274 41.659 105.35 / 1), color(--lch 23.119 31.812 100.43 / 1), color(--lch 13.922 21.135 76.154 / 1), color(--lch 4.9924 15.753 29.476 / 1)]\n

There are times when clipping is simply preferred. It is fast, and if you are just trimming noise off channels, it is very useful, but if the idea is to present an in gamut color that tries to preserve as much of the intent of the original color as possible, other methods may be desired. There are no doubt better gamut methods available than what ColorAide offers currently, and more may be added in the future, but ColorAide can also be extended using 3rd party plugins as well.

"},{"location":"gamut/#pointers-gamut","title":"Pointer's Gamut","text":"

New 2.4

The Pointer\u2019s gamut is (an approximation of) the gamut of real surface colors as can be seen by the human eye, based on the research by Michael R. Pointer (1980). What this means is that every color that can be reflected by the surface of an object of any material should be is inside the Pointer\u2019s gamut. This does not include, however, those that do not occur naturally, such as neon lights, etc.

While in the above image, it may appear that most of sRGB is in the gamut, it is important to note that the image is showing the maximum range of the gamut. The actual boundary will be different at different luminance levels.

The gamuts previously discussed are bound by a color space's limits, but the Pointer's gamut applies to colors more generally and was created from observed data via research. Because it doesn't quite fit with the color space gamut API, ColorAide exposes two special functions to test if a color is in the Pointer's gamut and to fit a color to the gamut.

To test if a color is within the gamut, simply call in_pointer_gamut():

>>> Color('red').in_pointer_gamut()\nFalse\n>>> Color('orange').in_pointer_gamut()\nTrue\n

ColorAide also provides a way to fit a color to the Pointer's gamut. The original gamut's data is described in LCh using illuminant C. Using this color space, we can estimate the chroma limit for any color based on it's lightness and hue. We can then reduce the chroma, preserving the lightness and hue. The image below shows the out of Pointer's gamut color red (indicated by the x) which is clamped to the Pointer's gamut by reducing the chroma (indicated by the dot).

ColorAide provides the fit_pointer_gamut() method to perform this \"fitting\" of the color.

>>> color = Color('red')\n>>> color\ncolor(srgb 1 0 0 / 1)\n>>> color.in_pointer_gamut()\nFalse\n>>> color.fit_pointer_gamut()\ncolor(srgb 0.95687 0.18251 0.09074 / 1)\n>>> color.in_pointer_gamut()\nTrue\n

Tip

Much like in_gamut(), in_pointer_gamut() allows adjusting tolerance as well via the tolerance parameter.

"},{"location":"harmonies/","title":"Color Harmonies","text":"

In color theory, color harmony refers to the property that certain aesthetically pleasing color combinations have. Modern day color theory probably starts with the first color wheel created by Isaac Newton. Based on his observations of light with prisms, he formed one the first color wheels. From there, many others built upon this work, sometimes with opposing ideas.

The original color wheel, while inspired by what was observed by light, was created based on experiments with pigments as well. As most know, in paint, red, yellow, and blue are considered primary colors. Newton thought this translated to light as well and stated they were also the primary colors of light. While this isn't actually true, his work was very important in reshaping how people viewed color.

Over time, the color wheel was refined. The traditional model, which we will call an RYB color model, defined 12 colors that made up the wheel: the primary colors, the secondary colors, and the tertiary colors. The secondary colors are created by evenly mixing the primary colors, and the tertiary colors are created by evenly mixing those primary colors with the secondary colors.

The idea of color harmonies originates from the idea that colors, based on their relative position on the wheel, can form more pleasing color combinations.

"},{"location":"harmonies/#which-color-space-is-best-for-color-harmonies","title":"Which Color Space is Best for Color Harmonies?","text":"

As we know, these days, there are many color spaces out there: subtractive models, additive models, perceptually uniform models, high dynamic range models, etc. Many color spaces trying to solve specific issues based on the knowledge at the time.

It should be noted, that the idea of primary colors stems from the idea that there are a set of pure colors from which all colors can be made from. If you've spent any time with paint, you will know that not all colors can be made from red, yellow, and blue. There are colors like cyan and magenta that cannot be made with the traditional primary colors. The early work that helped create the first color wheels was done with the limited paints that was available at the time, and the color harmony concepts were built upon the early RYB color model.

In modern TVs and monitors, the RYB color model is not used. Paint has subtractive properties, but light has additive properties. Electronic screens create all their colors with light based methods that mix red, green, and blue lights. In addition, the human eye perceives colors using red, green, and blue light as well. This is As far as light is concerned, the primary colors are red, green, and blue.

In reality, we could create a color wheel from any of the various color spaces out there and end up with different results. If we were to compose a color wheel based on the common sRGB color space, we could base it off the 3 primary colors of light. Starting with red (0\u02da), we could extract the colors at evenly spaced degrees, 30\u02da to be exact. This would give us our 12 colors for the sRGB color space.

>>> Steps([Color('hsl', [x, 1, 0.5]) for x in range(0, 360, 30)])\n[color(--hsl 0 1 0.5 / 1), color(--hsl 30 1 0.5 / 1), color(--hsl 60 1 0.5 / 1), color(--hsl 90 1 0.5 / 1), color(--hsl 120 1 0.5 / 1), color(--hsl 150 1 0.5 / 1), color(--hsl 180 1 0.5 / 1), color(--hsl 210 1 0.5 / 1), color(--hsl 240 1 0.5 / 1), color(--hsl 270 1 0.5 / 1), color(--hsl 300 1 0.5 / 1), color(--hsl 330 1 0.5 / 1)]\n

From this we can construct an sRGB color wheel.

This is different from the RYB color wheel we showed earlier, and more accurate in relation to how light works, but does it yield better harmonies for colors?

The sRGB color space is additive, just like light, but pigments are subtractive. We can use CMY to generate a subtractive wheel with a far greater range that red, green blue creates by use magenta, yellow, and cyan. But does this create better harmonies?

>>> Steps(Color('magenta').harmony('wheel', space='cmy'))\n[color(--cmy 0 1 0 / 1), color(--cmy 0 1 0.5 / 1), color(--cmy 0 1 1 / 1), color(--cmy 0 0.5 1 / 1), color(--cmy 0 0 1 / 1), color(--cmy 0.5 0 1 / 1), color(--cmy 1 0 1 / 1), color(--cmy 1 0 0.5 / 1), color(--cmy 1 0 0 / 1), color(--cmy 1 0.5 0 / 1), color(--cmy 1 1 0 / 1), color(--cmy 0.5 1 0 / 1)]\n

If we were to select the perceptually uniform OkLCh color space, and seed it with red's lightness and chroma, we'd get the wheel below.

>>> Steps(Color('red').harmony('wheel', space='oklch'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 59.234 / 1), color(--oklch 0.62796 0.25768 89.234 / 1), color(--oklch 0.62796 0.25768 119.23 / 1), color(--oklch 0.62796 0.25768 149.23 / 1), color(--oklch 0.62796 0.25768 179.23 / 1), color(--oklch 0.62796 0.25768 209.23 / 1), color(--oklch 0.62796 0.25768 239.23 / 1), color(--oklch 0.62796 0.25768 269.23 / 1), color(--oklch 0.62796 0.25768 299.23 / 1), color(--oklch 0.62796 0.25768 329.23 / 1), color(--oklch 0.62796 0.25768 359.23 / 1)]\n

This produces colors with visually more uniform lightness, does that mean these are better?

The truth is, what is better or even harmonious can be largely subjective, and everyone has reasons for selecting certain color spaces for a specific task.

Many artists swear by the limited, classical color wheel, others are fine with using the RGB color wheel as it is easy to work with in CSS via the HSL color space, and there are still others that are more interested in perceptually uniform color spaces that aim for more consistent hues and predictable lightness.

As far as ColorAide is concerned, we've chosen to use OkLCh as the color space in which we work in. This is based mainly on the fact that it keeps hue more consistent than some other options, and it allows us to support a wider gamut than options like HSL.

>>> Steps(Color.steps(['black', 'blue', 'white'], steps=11, space='oklch'))\n[color(--oklch 0 0 264.05 / 1), color(--oklch 0.0904 0.06264 264.05 / 1), color(--oklch 0.18081 0.12529 264.05 / 1), color(--oklch 0.27121 0.18793 264.05 / 1), color(--oklch 0.36161 0.25057 264.05 / 1), color(--oklch 0.45201 0.31321 264.05 / 1), color(--oklch 0.56161 0.25057 264.05 / 1), color(--oklch 0.67121 0.18793 264.05 / 1), color(--oklch 0.78081 0.12529 264.05 / 1), color(--oklch 0.8904 0.06264 264.05 / 1), color(--oklch 1 0 264.05 / 1)]\n>>> Steps(Color.steps(['black', 'blue', 'white'], steps=11, space='hsl'))\n[color(--hsl 240 0 0 / 1), color(--hsl 240 0.2 0.1 / 1), color(--hsl 240 0.4 0.2 / 1), color(--hsl 240 0.6 0.3 / 1), color(--hsl 240 0.8 0.4 / 1), color(--hsl 240 1 0.5 / 1), color(--hsl 240 0.8 0.6 / 1), color(--hsl 240 0.6 0.7 / 1), color(--hsl 240 0.4 0.8 / 1), color(--hsl 240 0.2 0.9 / 1), color(--hsl 240 0 1 / 1)]\n>>> Steps(Color.steps(['black', 'blue', 'white'], steps=11, space='lch'))\n[color(--lch 0 0 301.36 / 1), color(--lch 5.9137 26.24 301.36 / 1), color(--lch 11.827 52.481 301.36 / 1), color(--lch 17.741 78.721 301.36 / 1), color(--lch 23.655 104.96 301.36 / 1), color(--lch 29.568 131.2 301.36 / 1), color(--lch 43.655 104.96 301.36 / 1), color(--lch 57.741 78.721 301.36 / 1), color(--lch 71.827 52.481 301.36 / 1), color(--lch 85.914 26.24 301.36 / 1), color(--lch 100 0 301.36 / 1)]\n

While OkLCh is the default, we understand that there are many reasons to use other spaces, so use what you like, we won't judge . If you are a color theory purist, you can use the classical RYB model.

>>> Steps(Color('red').harmony('complement'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 209.23 / 1)]\n>>> Steps(Color('ryb', [1, 0, 0]).harmony('complement', space='ryb'))\n[color(--ryb 1 0 0 / 1), color(--ryb 0 1 1 / 1)]\n

RYB Model

The RYB model has a more limited color gamut than sRGB as the red, yellow and blue primaries cannot make all colors. Additionally, the red, yellow, and blue primaries are not the same as the ones in sRGB, so when using RYB to generate harmonies, make sure you are working directly within RYB to ensure you are not out of gamut.

Tip

harmony() can output the results in any color space you need by setting out_space.

>>> Steps(Color('red').harmony('complement', out_space='srgb'))\n[color(srgb 1 0 0 / 1), color(srgb -0.56631 0.66342 0.85808 / 1)]\n
"},{"location":"harmonies/#supported-harmonies","title":"Supported Harmonies","text":"

ColorAide currently supports 7 theorized color harmonies: monochromatic, complementary, split complementary, analogous, triadic, square, and rectangular. By default, all color harmonies are calculated with the perceptually uniform OkLCh color space, but other color spaces can be used if desired.

"},{"location":"harmonies/#monochromatic","title":"Monochromatic","text":"

The monochromatic harmony pairs various tints and shades of a color together to create pleasing color schemes.

>>> Steps(Color('red').harmony('mono'))\n[color(--oklch 0.37677 0.15461 29.234 / 1), color(--oklch 0.50236 0.20615 29.234 / 1), color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.70236 0.20615 29.234 / 1), color(--oklch 0.77677 0.15461 29.234 / 1)]\n

Achromatic Colors

Pure white and black will not be included in a monochromatic color harmony unless the color is achromatic.

"},{"location":"harmonies/#complementary","title":"Complementary","text":"

Complementary harmonies use a dyad of colors at opposite ends of the color wheel.

>>> Steps(Color('red').harmony('complement'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 209.23 / 1)]\n
"},{"location":"harmonies/#split-complementary","title":"Split Complementary","text":"

Split Complementary is similar to complementary, but actually uses a triad of colors. Instead of just choosing one complement, it splits and chooses two colors on the opposite side that are close, but not adjacent.

>>> Steps(Color('red').harmony('split'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 239.23 / 1), color(--oklch 0.62796 0.25768 -180.77 / 1)]\n
"},{"location":"harmonies/#analogous","title":"Analogous","text":"

Analogous harmonies consists of 3 adjacent colors.

>>> Steps(Color('red').harmony('analogous'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 59.234 / 1), color(--oklch 0.62796 0.25768 -0.76612 / 1)]\n
"},{"location":"harmonies/#triadic","title":"Triadic","text":"

Triadic draws an equilateral triangle between 3 colors on the color wheel. For instance, the primary colors have triadic harmony.

>>> Steps(Color('red').harmony('triad'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 149.23 / 1), color(--oklch 0.62796 0.25768 269.23 / 1)]\n
"},{"location":"harmonies/#tetradic-square","title":"Tetradic Square","text":"

Tetradic color harmonies refer to a group of four colors. One tetradic color harmony can be found by drawing a square between four colors on the color wheel.

>>> Steps(Color('red').harmony('square'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 119.23 / 1), color(--oklch 0.62796 0.25768 209.23 / 1), color(--oklch 0.62796 0.25768 299.23 / 1)]\n
"},{"location":"harmonies/#tetradic-rectangular","title":"Tetradic Rectangular","text":"

The rectangular tetradic harmony is very similar to the square tetradic harmony except that it draws a rectangle between four colors instead of a square.

>>> Steps(Color('red').harmony('rectangle'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 59.234 / 1), color(--oklch 0.62796 0.25768 209.23 / 1), color(--oklch 0.62796 0.25768 239.23 / 1)]\n
"},{"location":"harmonies/#others","title":"Others","text":"

If you have a particular configuration that you are after that is not covered under the defaults, you can use harmony to calculate your own. Simply use the wheel harmony that can generate a color wheel of any size. Simply use a color to seed the wheel, specify the space in which to generate the wheel. Optionally, provide the desired number of colors in the color wheel via the count argument. We can generate a wheel for any color (assuming the color space can properly handle the color). We can even generate an extended color wheel if so desired.

>>> Steps(Color('ryb', [1, 0, 0]).harmony('wheel', space='ryb', count=48))\n[color(--ryb 1 0 0 / 1), color(--ryb 1 0.125 0 / 1), color(--ryb 1 0.25 0 / 1), color(--ryb 1 0.375 0 / 1), color(--ryb 1 0.5 0 / 1), color(--ryb 1 0.625 0 / 1), color(--ryb 1 0.75 0 / 1), color(--ryb 1 0.875 0 / 1), color(--ryb 1 1 0 / 1), color(--ryb 0.875 1 0 / 1), color(--ryb 0.75 1 0 / 1), color(--ryb 0.625 1 0 / 1), color(--ryb 0.5 1 0 / 1), color(--ryb 0.375 1 0 / 1), color(--ryb 0.25 1 0 / 1), color(--ryb 0.125 1 0 / 1), color(--ryb 0 1 0 / 1), color(--ryb 0 1 0.125 / 1), color(--ryb 0 1 0.25 / 1), color(--ryb 0 1 0.375 / 1), color(--ryb 0 1 0.5 / 1), color(--ryb 0 1 0.625 / 1), color(--ryb 0 1 0.75 / 1), color(--ryb 0 1 0.875 / 1), color(--ryb 0 1 1 / 1), color(--ryb 0 0.875 1 / 1), color(--ryb 0 0.75 1 / 1), color(--ryb 0 0.625 1 / 1), color(--ryb 0 0.5 1 / 1), color(--ryb 0 0.375 1 / 1), color(--ryb 0 0.25 1 / 1), color(--ryb 0 0.125 1 / 1), color(--ryb 0 0 1 / 1), color(--ryb 0.125 0 1 / 1), color(--ryb 0.25 0 1 / 1), color(--ryb 0.375 0 1 / 1), color(--ryb 0.5 0 1 / 1), color(--ryb 0.625 0 1 / 1), color(--ryb 0.75 0 1 / 1), color(--ryb 0.875 0 1 / 1), color(--ryb 1 0 1 / 1), color(--ryb 1 0 0.875 / 1), color(--ryb 1 0 0.75 / 1), color(--ryb 1 0 0.625 / 1), color(--ryb 1 0 0.5 / 1), color(--ryb 1 0 0.375 / 1), color(--ryb 1 0 0.25 / 1), color(--ryb 1 0 0.125 / 1)]\n
"},{"location":"harmonies/#changing-the-default-harmony-color-space","title":"Changing the Default Harmony Color Space","text":"

New 2.7

Non-cylindrical space support was added in 2.7.

If you'd like to change the Color() class's default harmony color space, it can be done with class override. Simply derive a new Color() class from the original and override the HARMONY property with the name of a suitable color space. Color spaces must be either a cylindrical space, a Lab-like color space, or what we will call a regular, rectangular space. By \"regular\" we mean a normal 3 channel color space usually with a range of [0, 1]. Afterwards, all color harmony calculations will use the specified color space unless overridden via the method's space parameter.

>>> class Custom(Color):\n...     HARMONY = 'hsl'\n... \n>>> Steps(Custom('red').harmony('split'))\n[color(--hsl 0 1 0.5 / 1), color(--hsl 210 1 0.5 / 1), color(--hsl -210 1 0.5 / 1)]\n

Warning

Remember that every color space is different. Some may rotate hues in a different direction and some may just not be very compatible for extracting harmonies from.

Additionally, a color space may not handle colors beyond its gamut well, for such color spaces, it is important to work within that spaces gamut opposed to picking colors outside of the gamut and relying on gamut mapping.

"},{"location":"interpolation/","title":"Color Interpolation","text":"

Interpolation is a type of estimation that finds new data points based on the range of a discrete set of known data points. When used in the context of color, it is finding one or more colors that reside between any two given colors. This is often used to simulate mixing colors, creating gradients, or even create color palettes.

ColorAide provides a number of useful utilities based on interpolation.

"},{"location":"interpolation/#linear-interpolation","title":"Linear Interpolation","text":"

Linear interpolation is registered in Color by Default

One of the most common, and easiest ways to interpolate data between two points is to use linear interpolation. An easy way of thinking about this concept is to imagine drawing a straight line that connects two colors within a color space. We could then navigate along that line and return colors at different points to simulate mixing colors at various percentages or return the whole range and create a continuous, smooth gradient.

To further illustrate this point, the example below shows a slice of the Oklab color space at a lightness of 70%. On this 2D plane, we select two colors: oklab(0.7 0.15 0.1) and oklab(0.7 -0.03 -0.12). 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: oklab(0.7 0.06 -0.01).

Interpolation performed at 50%

The interpolate method allows a user to create a linear interpolation function using two or more colors. By default, a returned interpolation function accepts numerical input in the domain of [0, 1] and will cause a new color between the specified colors to be 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 discrete, steps, mix, etc.

As an example, below we create an interpolation between rebeccapurple and lch(85% 100 85). We then step through values of 0.0, 0.1, 0.2, etc. This returns colors at various positions on the line that connects the two colors, 0 returning rebeccapurple and 1 returning lch(85% 100 85).

>>> i = Color.interpolate([\"rebeccapurple\", \"lch(85% 100 85)\"], space='lch')\n>>> [i(x / 10).to_string() for x in range(10 + 1)]\n['lch(32.393 61.244 308.86)', 'lch(37.653 65.119 322.47)', 'lch(42.914 68.995 336.09)', 'lch(48.175 72.87 349.7)', 'lch(53.436 76.746 3.3143)', 'lch(58.696 80.622 16.929)', 'lch(63.957 84.497 30.543)', 'lch(69.218 88.373 44.157)', 'lch(74.479 92.249 57.771)', 'lch(79.739 96.124 71.386)', 'lch(85 100 85)']\n

If we create enough steps, we can create a gradient.

>>> i = Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 85)\"],\n...     space='lch'\n... )\n
"},{"location":"interpolation/#piecewise-interpolation","title":"Piecewise Interpolation","text":"

Piecewise interpolation takes the idea of linear interpolation and then applies it to multiple colors. As drawing a straight line through a series of points greater than two can be difficult to achieve, piecewise interpolation creates straight lines between each color in a chain of colors.

When the interpolate method receives more that two colors, the interpolation will utilize piecewise interpolation and interpolation will be broken up between each pair of colors. The function, just like when interpolating between two colors, still operates by default in the domain of [0, 1], only it will now apply to the entire range of colors.

Piecewise interpolation simply breaks up a series of data points into segments in order to apply interpolation individually on each segment.

>>> Color.interpolate(['black', 'red', 'white'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d7fb10>\n

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 at these locations.

"},{"location":"interpolation/#continuous-interpolation","title":"Continuous Interpolation","text":"

Continuous interpolation is registered in Color by Default

In this document, we use the term \"continuous\" in two ways when talking about interpolation: continuous vs discrete and the interpolation method whose literal name is continuous.

The interpolate method only creates continuous interpolations, meaning that for any point along the interpolation line, you will get a unique color. Continuous interpolation in this sense directly contrasts with with discrete interpolation which provides quantized color results where multiple inputs are associated with a limited set of colors along the interpolation line.

The continuous interpolation method is simply a piecewise, linear interpolation method that interpolates defined channels continuously across one more undefined channels.

Normal, piecewise interpolation only considers a single segments under interpolation at a time. When channels are undefined, the undefined channel on one end of a segment will adopt the value of the other defined channel on the other side of the segment, and if that other channel is also undefined, then any color interpolated between the two will also have no defined value for the channel. This approach to interpolation never considers any context beyond the segment it is looking at.

The continuous interpolation method is a linear piecewise approach created for ColorAide that will actually interpolate through undefined channels, using context from all the colors to be interpolated. What this means is that if you have multiple colors, and one or more of the colors have the same channel undefined, the colors with that channel defined will have those values interpolated across the undefined gaps across all the segments. This is probably better illustrated with an example.

In this example, we have 3 colors. The end colors both define lightness, but the middle color is undefined. We can see when we use normal, linear piecewise interpolation that we get a discontinuity. But with continuous linear interpolation, we get a smooth interpolation of the lightness through the undefined channel.

>>> colors = [\n...     Color('oklab', [0, 0, 0]),\n...     Color('oklab', [NaN, -0.03246, -0.31153]),\n...     Color('oklab', [1, 0, 0])\n... ]\n>>> Color.interpolate(colors, space='oklab', method='linear')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c38490>\n>>> Color.interpolate(colors, space='oklab', method='continuous')\n<coloraide.interpolate.continuous.InterpolatorContinuous object at 0x7fbab5cc6d90>\n

Now, if have colors on the side that are not between two defined colors, all those colors will adopt the defined value of the one that is defined. This time we have a single color with all components defined, but all the colors to the left are missing the lightness. All colors with the undefined lightness will assume the lightness of the defined color.

>>> colors = [\n...     Color('oklab', [NaN, 0.22486, 0.12585]),\n...     Color('oklab', [NaN, -0.1403, 0.10768]),\n...     Color('oklab', [0.45201, -0.03246, -0.31153])\n... ]\n>>> Color.interpolate(colors, space='oklab', method='linear')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5af10>\n>>> Color.interpolate(colors, space='oklab', method='continuous')\n<coloraide.interpolate.continuous.InterpolatorContinuous object at 0x7fbab5e005d0>\n
"},{"location":"interpolation/#cubic-spline-interpolation","title":"Cubic Spline Interpolation","text":"

Linear interpolation is nice because it is easy to implement, and due to its straight forward nature, pretty fast. With that said, it doesn't always have the smoothest transitions. It turns out that there are other piecewise ways to interpolate that can yield smoother results.

Inspired by some efforts seen on the web and in the great JavaScript library Culori, ColorAide implements a number of spline based interpolation methods.

Because splines require taking into account more than two colors at a time, all spline based interpolation methods are built off of the continuous interpolation approach of handling undefined values.

"},{"location":"interpolation/#b-spline","title":"B-Spline","text":"

B-Spline interpolation is registered in Color by Default

B-spline is a piecewise spline similar to Bezier curves. It utilizes \"control points\" that help shape the interpolation path through a series of colors. Like Bezier Curves, the path does not pass through the control points, but it is clamped at the start and end. Essentially, the interpolation path passes through both end colors and bends that path along the way towards the other colors being used as control points.

It can be used by specifying bspline as the interpolation method.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='bspline')\n<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x7fbab5ee1850>\n
"},{"location":"interpolation/#natural","title":"Natural","text":"

Natural interpolation is registered in Color by Default

The \"natural\" spline is the same as the B-spline approach except an algorithm is applied that uses the colors as data points and calculates new control points such that the interpolation passes through all the data points. This means that the path will pass through all the colors. The resultant spline has the continuity and properties of a natural spline, hence the name.

One down side is that it can overshoot or undershoot a bit, and can occasionally cause the interpolation path to pass out of gamut if interpolating on an edge.

It can be used by specifying natural as the interpolation method.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='natural')\n<coloraide.interpolate.bspline_natural.InterpolatorNaturalBSpline object at 0x7fbab5d62210>\n
"},{"location":"interpolation/#monotone","title":"Monotone","text":"

Monotone interpolation is registered in Color by Default

The \"monotone\" spline is a piecewise interpolation spline that passes through all its data points and helps to preserve monotonicity. As far as we are concerned, the important thing to note is that it greatly reduces any overshoot or undershoot in the interpolation.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='monotone')\n<coloraide.interpolate.monotone.InterpolatorMonotone object at 0x7fbab709a750>\n
"},{"location":"interpolation/#catmull-rom","title":"Catmull-Rom","text":"

Catmull-Rom interpolation is not registered in Color by Default

Lastly, the Catmull-Rom spline is another \"interpolating\" spline that passes through all of its data points, similar to the \"natural\" spline, but it but does not share the same continuity and properties of a \"natural\" spline.

Much like the \"natural\" spline, it can overshoot or undershoot.

Catmull-Rom is not registered by default, but can be registered as shown below and then used by specifying catrom as the interpolation method.

>>> from coloraide import Color\n>>> from coloraide.interpolate.catmull_rom import CatmullRom\n>>> class Custom(Color): ...\n... \n>>> Custom.register(CatmullRom())\n>>> Custom.interpolate(['red', 'green', 'blue', 'orange'], method='catrom')\n<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x7fbab5e035d0>\n
"},{"location":"interpolation/#discrete-interpolation","title":"Discrete Interpolation","text":"

New 2.5

So far, we've only shown examples of continuous interpolation methods. To clarify, we are using \"continuous\" in a slightly different way than we discussed earlier. When we say \"continuous\" here, we simply mean that the colors in the interpolation smoothly transition from one color to the other. But when creating charts or graphs, some times you'd like to categorize data such that a range of values correspond to a specific color. For this, we can use discrete, which like intrpolate, returns an interpolation object, but the the ranges will be discrete.

By default, ranges are calculated directly form the input colors. So if you had three colors, the interpolation would be broken up into 3 ranges. Compare this with the the \"continuous\" interpolation we methods we showed earlier.

>>> Color.discrete(['red', 'green', 'blue'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d71ed0>\n>>> Color.interpolate(['red', 'green', 'blue'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d575d0>\n

If we specify step, we can create a larger or smaller color scale using the input colors to interpolate the new color scale. And we can use any of the aforementioned interpolation methods to help generate this new discrete scale.

>>> Color.discrete(['red', 'green', 'blue'], steps=5, method='catrom')\n<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x7fbab5f1e890>\n

What makes this really useful is if you combine it with custom domains to process data. By default, the domain is [0, 1], but we can change this to directly correlate the data with our quantized color samples. For instance, let's use a series of discrete colors to represent temperature. Additionally, let's use domain to associate a temperature ranges with the given colors. Now when we input a temperature value, it will align with our discrete color scale.

>>> i = Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'], domain=[-32, 32, 60, 85, 95])\n>>> i(-32)\ncolor(--oklab 0.45201 -0.03246 -0.31153 / 1)\n>>> i(40)\ncolor(--oklab 0.51975 -0.1403 0.10768 / 1)\n>>> i(87)\ncolor(--oklab 0.79269 0.05661 0.16138 / 1)\n>>> i(100)\ncolor(--oklab 0.62796 0.22486 0.12585 / 1)\n>>> i\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab6e799d0>\n

Additionally, color scales can be limited using the padding parameter.

>>> Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dbeb50>\n>>> Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'], padding=[0.25, 0])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5cb0790>\n

As discrete() is built on steps(), it can take all the same arguments. Check out steps() to learn more.

"},{"location":"interpolation/#hue-interpolation","title":"Hue Interpolation","text":"

In interpolation, hues are handled special allowing us to control the way in which hues are evaluated. By default, the shortest angle between two hues is targeted for interpolation, but the hue option allows us to redefine this behavior in a number of interesting ways: shorter, longer, increasing, decreasing, and specified. Below, we can see how the interpolation varies using shorter vs longer (interpolate between the longest angle).

>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask(\"hue\", invert=True)],\n...     space=\"lch\"\n... )\n>>> i(0.2477).to_string()\n'lch(52 58.1 351.59)'\n>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask(\"hue\", invert=True)],\n...     space=\"lch\",\n...     hue=\"longer\"\n... )\n>>> i(0.2477).to_string()\n'lch(52 58.1 80.761)'\n

To help visualize the different hue methods, consider the following evaluation between rebeccapurple and lch(85% 85 805). Below we will demonstrate each of the different hue evaluations. To learn more, check out the CSS level 4 specification which describes each one.

Hue Specified

The specified fix-up was at one time specified in the CSS Color Level 4 specification, but is no longer mentioned there. While CSS no longer supports this hue fix-up, we still do. specified simply does not apply any hue fix-up and will use hues as specified, hence the name.

Interpolating Multiple Colors

The algorithm has been tweaked in order to calculate fix-ups of multiple hues such that they are all relative to each other. This is a requirement for interpolation methods that use cubic splines that evaluate many hues at the same time as opposed to linear, piecewise interpolation that only evaluates two hues at any given time.

shorterlongerincreasingdecreasingspecified
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"shorter\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d7e410>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"longer\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c82650>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"increasing\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5f1e710>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"decreasing\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5f1e710>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"specified\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d31590>\n
"},{"location":"interpolation/#interpolating-with-alpha","title":"Interpolating with Alpha","text":"

Interpolating color channels is pretty straight forward and uses traditional linear interpolation logic, but when introducing transparency to a color, interpolation uses a concept known as premultiplication which alters the normal interpolation process.

Premultiplication is a technique that tends to produce better results when two colors have differing transparency. It essentially accounts for the transparency and uses it to weight how may a given color channel will contribute to the interpolation. A more transparent color's channels will naturally contribute less.

Consider the following example. Normally, when transitioning to a \"transparent\" color, the colors will be more gray during the transition. This is because transparent is actually black. But when using premultiplication, the transition looks just as one would expect as the transparent color's channels are weighted less due to the high transparency.

>>> Color.interpolate(['white', 'transparent'], space='srgb', premultiplied=False)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5eba890>\n>>> Color.interpolate(['white', 'transparent'], space='srgb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec5010>\n

As a final example, below we have an opaque orange and a blue that is quite transparent. Logically, the blue shouldn't have as big an affect on the overall color as it is so faint, and yet, in the un-premultiplied example, when mixing the colors equally, we see that the resultant color is also equally influenced by the hue of both colors. In the premultiplied example, we see that orange is still quite dominant at 50% as it is fully opaque.

>>> Color('orange').mix(Color('blue').set('alpha', 0.25), space='srgb', premultiplied=False)\ncolor(srgb 0.5 0.32353 0.5 / 0.625)\n>>> Color('orange').mix(Color('blue').set('alpha', 0.25), space='srgb')\ncolor(srgb 0.8 0.51765 0.2 / 0.625)\n

If we interpolate it, we can see the difference in transition.

>>> Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb', premultiplied=False)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec37d0>\n>>> Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c63bd0>\n

There may be some cases where it is desired to use no premultiplication in alpha blending. One could simply be that you need to mimic the same behavior of a system that does not use premultiplied interpolation. If so, simply set premultiplied to False as shown above.

"},{"location":"interpolation/#mixing","title":"Mixing","text":"

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 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.

>>> Color(\"red\").mix(Color(\"blue\"))\ncolor(--oklab 0.53998 0.0962 -0.09284 / 1)\n

By default, colors are mixed at 50%, but the percentage can be controlled. Here we mix the color blue into the color red at 20%. With blue at 20% and red at 80%, this gives us a more reddish color.

>>> Color(\"red\").mix(Color(\"blue\"), 0.2)\ncolor(--oklab 0.59277 0.1734 0.03837 / 1)\n

As with all interpolation based functions, if needed, a different color space can be specified with the space parameter or even a different interpolation method via method. mix accepts all the same parameters used in interpolate, though concepts like stops and hints are not allowed with mixing.

>>> Color(\"red\").mix(Color(\"blue\"), space=\"hsl\", method='bspline')\ncolor(--hsl -60 1 0.5 / 1)\n

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 color afterwards.

>>> Color(\"red\").mix(\"blue\", 0.2)\ncolor(--oklab 0.59277 0.1734 0.03837 / 1)\n

Mixing will always return a new color unless in_place is set True.

"},{"location":"interpolation/#steps","title":"Steps","text":"

Interpolation Options

Any options not consumed by mix will be passed to the underlying interpolation function. This includes options 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. Just provide two or more colors, and specify how many steps are wanted.

>>> Color.steps([\"red\", \"blue\"], steps=10)\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.60841 0.19627 0.07725 / 1), color(--oklab 0.58886 0.16768 0.02865 / 1), color(--oklab 0.56931 0.13909 -0.01995 / 1), color(--oklab 0.54976 0.1105 -0.06854 / 1), color(--oklab 0.53021 0.08191 -0.11714 / 1), color(--oklab 0.51066 0.05332 -0.16574 / 1), color(--oklab 0.49111 0.02473 -0.21433 / 1), color(--oklab 0.47156 -0.00387 -0.26293 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1)]\n

If desired, multiple colors can be provided, and steps will be returned for all the interpolated segments. When interpolating multiple colors, piecewise interpolation is used (which is covered in more detail later).

>>> Color.steps([\"red\", \"orange\", \"yellow\", \"green\"], steps=10)\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.68287 0.16878 0.13769 / 1), color(--oklab 0.73778 0.1127 0.14954 / 1), color(--oklab 0.79269 0.05661 0.16138 / 1), color(--oklab 0.85112 0.01395 0.17378 / 1), color(--oklab 0.90955 -0.02871 0.18617 / 1), color(--oklab 0.96798 -0.07137 0.19857 / 1), color(--oklab 0.81857 -0.09435 0.16827 / 1), color(--oklab 0.66916 -0.11732 0.13797 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1)]\n

Steps can also be configured to return colors based on a maximum Delta E distance. This means you can ensure the distance between all colors is no greater than a certain value.

In this example, we specify the color color(display-p3 0 1 0) and interpolate steps between red. The result gives us an array of colors, where the distance between any two colors should be no greater than the Delta E result of 10.

>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.4504 0.99903 -0.32673 / 1), color(srgb -0.37655 0.97943 -0.33694 / 1), color(srgb -0.27847 0.95946 -0.34286 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.23528 0.91833 -0.34574 / 1), color(srgb 0.34809 0.89715 -0.34401 / 1), color(srgb 0.42823 0.87552 -0.34098 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.54849 0.83088 -0.33349 / 1), color(srgb 0.59727 0.80784 -0.32909 / 1), color(srgb 0.64097 0.7843 -0.32423 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.71679 0.73568 -0.3131 / 1), color(srgb 0.74999 0.71057 -0.30681 / 1), color(srgb 0.78053 0.6849 -0.30002 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.83451 0.6318 -0.28486 / 1), color(srgb 0.85826 0.60433 -0.27644 / 1), color(srgb 0.87999 0.57619 -0.26744 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.9177 0.51777 -0.24752 / 1), color(srgb 0.9338 0.48735 -0.2365 / 1), color(srgb 0.9481 0.456 -0.22467 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.97145 0.38994 -0.19821 / 1), color(srgb 0.98055 0.35476 -0.18326 / 1), color(srgb 0.98796 0.31761 -0.16684 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 0.99777 0.23405 -0.12767 / 1), color(srgb 1.0002 0.18378 -0.10158 / 1), color(srgb 1.0009 0.11978 -0.06417 / 1), color(srgb 1 0 0 / 1)]\n

max_steps can be used to limit the results of max_delta_e in case result balloons to an unexpected size. Obviously, this affects the Delta E between the colors inversely. It should be noted that steps are injected equally between every color when satisfying a max Delta E limit in order to avoid shifting the midpoint. In some cases, in order to satisfy both the max_delta_e and the max_steps requirement, the number of steps may even be clipped such that they are less than the max_steps limit. max_steps is set to 1000 by default, but can be set to None if no limit is desired.

>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     max_steps=10\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 1 0 0 / 1)]\n

When specifying a max_delta_e, steps will function as a minimum required steps and will push the delta even smaller if the required steps is greater than the calculated steps via the maximum Delta E limit.

>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     steps=50\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.47276 1.0057 -0.32192 / 1), color(srgb -0.42946 0.99307 -0.33039 / 1), color(srgb -0.37992 0.98024 -0.33661 / 1), color(srgb -0.32082 0.96725 -0.341 / 1), color(srgb -0.24447 0.9541 -0.34385 / 1), color(srgb -0.1198 0.94078 -0.34542 / 1), color(srgb 0.15996 0.92729 -0.34592 / 1), color(srgb 0.26558 0.91362 -0.3455 / 1), color(srgb 0.33665 0.89976 -0.34431 / 1), color(srgb 0.3931 0.88572 -0.34248 / 1), color(srgb 0.44104 0.8715 -0.34036 / 1), color(srgb 0.48324 0.85707 -0.33806 / 1), color(srgb 0.52118 0.84244 -0.33557 / 1), color(srgb 0.55582 0.82761 -0.33289 / 1), color(srgb 0.58776 0.81258 -0.33002 / 1), color(srgb 0.61745 0.79733 -0.32696 / 1), color(srgb 0.64519 0.78187 -0.32371 / 1), color(srgb 0.67123 0.76619 -0.32025 / 1), color(srgb 0.69575 0.75029 -0.31659 / 1), color(srgb 0.7189 0.73416 -0.31273 / 1), color(srgb 0.74079 0.7178 -0.30866 / 1), color(srgb 0.76151 0.7012 -0.30437 / 1), color(srgb 0.78113 0.68437 -0.29987 / 1), color(srgb 0.79972 0.66729 -0.29515 / 1), color(srgb 0.81733 0.64995 -0.2902 / 1), color(srgb 0.834 0.63236 -0.28502 / 1), color(srgb 0.84977 0.6145 -0.2796 / 1), color(srgb 0.86467 0.59636 -0.27393 / 1), color(srgb 0.87872 0.57794 -0.26801 / 1), color(srgb 0.89194 0.55922 -0.26182 / 1), color(srgb 0.90434 0.54018 -0.25536 / 1), color(srgb 0.91596 0.52082 -0.2486 / 1), color(srgb 0.92679 0.50111 -0.24154 / 1), color(srgb 0.93686 0.48103 -0.23415 / 1), color(srgb 0.94616 0.46054 -0.22641 / 1), color(srgb 0.95471 0.43961 -0.2183 / 1), color(srgb 0.96252 0.41819 -0.20979 / 1), color(srgb 0.96959 0.39623 -0.20082 / 1), color(srgb 0.97593 0.37364 -0.19136 / 1), color(srgb 0.98155 0.35032 -0.18134 / 1), color(srgb 0.98644 0.32615 -0.17067 / 1), color(srgb 0.99062 0.30093 -0.15926 / 1), color(srgb 0.99409 0.27439 -0.14694 / 1), color(srgb 0.99685 0.24615 -0.13353 / 1), color(srgb 0.99891 0.21556 -0.11841 / 1), color(srgb 1.0002 0.18152 -0.10034 / 1), color(srgb 1.0009 0.14177 -0.07755 / 1), color(srgb 1.0008 0.09013 -0.04535 / 1), color(srgb 1 0 0 / 1)]\n

steps uses the color class's default \u2206E method to calculate max \u2206E, the current default \u2206E being \u2206E*ab. While using something like \u2206E*00 is far more accurate, it is a much more expensive operation. If desired, the class's default \u2206E can be changed via subclassing the color object and and changing DELTA_E class variable or by manually specifying the method via the delta_e parameter.

\u2206E*ab.\u2206E*00
>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     delta_e=\"76\"\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.4504 0.99903 -0.32673 / 1), color(srgb -0.37655 0.97943 -0.33694 / 1), color(srgb -0.27847 0.95946 -0.34286 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.23528 0.91833 -0.34574 / 1), color(srgb 0.34809 0.89715 -0.34401 / 1), color(srgb 0.42823 0.87552 -0.34098 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.54849 0.83088 -0.33349 / 1), color(srgb 0.59727 0.80784 -0.32909 / 1), color(srgb 0.64097 0.7843 -0.32423 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.71679 0.73568 -0.3131 / 1), color(srgb 0.74999 0.71057 -0.30681 / 1), color(srgb 0.78053 0.6849 -0.30002 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.83451 0.6318 -0.28486 / 1), color(srgb 0.85826 0.60433 -0.27644 / 1), color(srgb 0.87999 0.57619 -0.26744 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.9177 0.51777 -0.24752 / 1), color(srgb 0.9338 0.48735 -0.2365 / 1), color(srgb 0.9481 0.456 -0.22467 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.97145 0.38994 -0.19821 / 1), color(srgb 0.98055 0.35476 -0.18326 / 1), color(srgb 0.98796 0.31761 -0.16684 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 0.99777 0.23405 -0.12767 / 1), color(srgb 1.0002 0.18378 -0.10158 / 1), color(srgb 1.0009 0.11978 -0.06417 / 1), color(srgb 1 0 0 / 1)]\n
>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     delta_e=\"2000\"\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.37655 0.97943 -0.33694 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.34809 0.89715 -0.34401 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.59727 0.80784 -0.32909 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.74999 0.71057 -0.30681 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.85826 0.60433 -0.27644 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.9338 0.48735 -0.2365 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.98055 0.35476 -0.18326 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 1.0002 0.18378 -0.10158 / 1), color(srgb 1 0 0 / 1)]\n

And much like interpolate, we can use stops and hints and any of the other supported interpolate features as well.

>>> Color.steps(['orange', stop('purple', 0.25), 'green'], method='bspline', steps=10)\n[color(--oklab 0.79269 0.05661 0.16138 / 1), color(--oklab 0.63434 0.09861 0.05147 / 1), color(--oklab 0.51731 0.10434 -0.01701 / 1), color(--oklab 0.48698 0.08246 -0.02298 / 1), color(--oklab 0.47842 0.05764 -0.01527 / 1), color(--oklab 0.4775 0.02611 0.00011 / 1), color(--oklab 0.48271 -0.01079 0.02163 / 1), color(--oklab 0.49251 -0.05172 0.04775 / 1), color(--oklab 0.50536 -0.09534 0.07695 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1)]\n
"},{"location":"interpolation/#masking","title":"Masking","text":"

If desired, we can mask off specific channels that we do not wish to interpolate. Masking works by cloning the color and setting the specified channels as undefined (internally set to NaN). When interpolating, if one color's channel has a NaN, the other color's channel will be used as the result, keeping that channel at a constant value. If both colors have a NaN for the same channel, then NaN will be returned.

Magic Behind NaN

There are times when NaN values can happen naturally, such as with achromatic colors with hues. To learn more, check out Undefined Handling/NaN Handling.

In the following example, we have a base color of lch(52% 58.1 22.7) which we then interpolate with lch(56% 49.1 257.1). We then mask off the second color's channels except for hue. Applying this logic, we will end up with a range of colors that maintains the same lightness and chroma as the first color, but with different hues. We can see as we step through the colors that only the hue is interpolated.

>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask(['lightness', 'chroma', 'alpha'])],\n...     space=\"lch\"\n... )\n>>> [i(x/10).to_string() for x in range(10)]\n['lch(52 58.1 22.7)', 'lch(52 58.1 10.14)', 'lch(52 58.1 357.58)', 'lch(52 58.1 345.02)', 'lch(52 58.1 332.46)', 'lch(52 58.1 319.9)', 'lch(52 58.1 307.34)', 'lch(52 58.1 294.78)', 'lch(52 58.1 282.22)', 'lch(52 58.1 269.66)']\n

You can also create inverted masks. An inverted mask will mask all except the specified channel.

>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask('hue', invert=True)],\n...     space=\"lch\"\n... )\n>>> [i(x/10).to_string() for x in range(10)]\n['lch(52 58.1 22.7)', 'lch(52 58.1 10.14)', 'lch(52 58.1 357.58)', 'lch(52 58.1 345.02)', 'lch(52 58.1 332.46)', 'lch(52 58.1 319.9)', 'lch(52 58.1 307.34)', 'lch(52 58.1 294.78)', 'lch(52 58.1 282.22)', 'lch(52 58.1 269.66)']\n
"},{"location":"interpolation/#easing-functions","title":"Easing Functions","text":"

When interpolating, whether using linear interpolation or something like B-Spline interpolation, the transitioning between colors is always linear in time, even if the path to those colors is not. For example, if you are interpolating between 2 colors and you request a 0.5 point on that line, it will always be in the middle. This is because, no matter how crooked the path, the rate of change on that path is always linear.

By default, ColorAide uses linear transitions when interpolating, but there are times that a different, more dynamic transition may be desired. This can be achieved by using the progress parameter on any of the interpolation related functions provided by ColorAide.

progress accepts an easing function that takes a single time input and returns a new time input. This allows for a user to augment the rate of change when transitioning from one color to another. Inputs are almost always between 0 - 1 unless extrapolate is enabled and the user has manually input a range beyond 0 - 1. Even a change in domain will not affect the range as once the domain is accounted for, internally the domain [0, 1] is used.

ColorAide provides 5 basic easing functions out of the box along with cubic_bezier which is used to create all of the aforementioned easing function except linear, which simply returns what is given as an input.

Create Your Own Cubic Bezier Easings Online: https://cubic-bezier.com

More Common Cubic Bezier Easings

The following were all acquired from from https://matthewlein.com/tools/ceaser.js.

ease_in_quad = cubic_bezier(0.550, 0.085, 0.680, 0.530)\nease_in_cubic = cubic_bezier(0.550, 0.055, 0.675, 0.190)\nease_in_quart = cubic_bezier(0.895, 0.030, 0.685, 0.220)\nease_in_quint = cubic_bezier(0.755, 0.050, 0.855, 0.060)\nease_in_sine = cubic_bezier(0.470, 0.000, 0.745, 0.715)\nease_in_expo = cubic_bezier(0.950, 0.050, 0.795, 0.035)\nease_in_circ = cubic_bezier(0.600, 0.040, 0.980, 0.335)\nease_in_back = cubic_bezier(0.600, -0.280, 0.735, 0.045)\n\nease_out_quad = cubic_bezier(0.250, 0.460, 0.450, 0.940)\nease_out_cubic = cubic_bezier(0.215, 0.610, 0.355, 1.000)\nease_out_quart = cubic_bezier(0.165, 0.840, 0.440, 1.000)\nease_out_quint = cubic_bezier(0.230, 1.000, 0.320, 1.000)\nease_out_sine = cubic_bezier(0.390, 0.575, 0.565, 1.000)\nease_out_expo = cubic_bezier(0.190, 1.000, 0.220, 1.000)\nease_out_circ = cubic_bezier(0.075, 0.820, 0.165, 1.000)\nease_out_back = cubic_bezier(0.175, 0.885, 0.320, 1.275)\n\nease_in_out_quad = cubic_bezier(0.455, 0.030, 0.515, 0.955)\nease_in_out_cubic = cubic_bezier(0.645, 0.045, 0.355, 1.000)\nease_in_out_quart = cubic_bezier(0.770, 0.000, 0.175, 1.000)\nease_in_out_quint = cubic_bezier(0.860, 0.000, 0.070, 1.000)\nease_in_out_sine = cubic_bezier(0.445, 0.050, 0.550, 0.950)\nease_in_out_expo = cubic_bezier(1.000, 0.000, 0.000, 1.000)\nease_in_out_circ = cubic_bezier(0.785, 0.135, 0.150, 0.860)\nease_in_out_back = cubic_bezier(0.680, -0.550, 0.265, 1.550)\n
LinearEaseEase InEase OutEase In/OutCubic Bezier

Here, we are using the default \"ease in\" and \"ease out\" easing functions provided by ColorAide.

>>> from coloraide import ease_in, ease_out\n>>> Color.interpolate(\n...     [\"green\", \"blue\"],\n...     progress=ease_in\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c80910>\n>>> Color.interpolate(\n...     [\"green\", \"blue\"]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed8b50>\n>>> Color.interpolate(\n...     [\"green\", \"blue\"],\n...     progress=ease_out\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5e750>\n

Additionally, easing functions can be injected inline which allows a user to control how easing is performed between specific sub-interpolations within piecewise interpolation.

>>> Color.interpolate([\"red\", \"green\", ease_out, \"blue\"])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5eba890>\n

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 an exponential \"ease in\" to alpha while allowing all other channels to interpolate normally.

>>> ease_in_expo = cubic_bezier(0.950, 0.050, 0.795, 0.035)\n>>> Color.interpolate(\n...     [\"lch(50% 50 0)\", \"lch(90% 50 260 / 0.5)\"],\n...     progress={\n...         'alpha': ease_in_expo\n...     }\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec3310>\n

We can also set all the channels to an easing function via all and then override specific channels. In this case, we exponentially \"ease out\" on all channels except the red channel, which we then force to be linear.

>>> ease_out_expo = cubic_bezier(0.190, 1.000, 0.220, 1.000)\n>>> Color.interpolate(\n...     [\"color(srgb 0 1 1)\", \"color(srgb 1 0 0)\"],\n...     progress={\n...         'all': ease_out_expo,\n...         'r': linear\n...     },\n...     space='srgb'\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed9510>\n
"},{"location":"interpolation/#color-stops-and-hints","title":"Color Stops and Hints","text":"

Color stops are the position where the transition to and from a color starts and ends. By default, color stops are evenly distributed within the domain of [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.

>>> from coloraide import stop\n>>> Color.interpolate(['orange', 'purple', 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec5910>\n>>> Color.interpolate(['orange', stop('purple', 0.25), 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c5b310>\n

Color stops follow the rules as laid out in the CSS spec.

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.

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.

>>> from coloraide import hint\n>>> Color.interpolate(['orange', 'purple', 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec2410>\n>>> Color.interpolate(['orange', hint(0.75), 'purple', 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d314d0>\n
"},{"location":"interpolation/#padding","title":"Padding","text":"

New 2.6

Particularly when interpolating a color scale, it can be useful to \"resize\" the area of the color scale being evaluated. This can generally be done using the padding parameter. Consider the following example using the ColorBrewer scale OrRd.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.interpolate(scale, space='srgb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ee1690>\n>>> Color.discrete(scale, space='srgb', steps=5)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5eb5e50>\n>>> Color.interpolate(scale, space='srgb', padding=0.25)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5c1d0>\n>>> Color.discrete(scale, space='srgb', steps=5, padding=0.25)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d7eed0>\n

Padding can be applied to both sides by specifying a single number, or it can be controlled per side by sending in a sequence of two values.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[0.25, 0])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed0d90>\n

Negative padding is allowed as well.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[-0.25, 0])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ec73d0>\n

If the result extends past the limits, extrapolate needs to be enabled or the values will be clamped to the ends.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[1, 1])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5edbb10>\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[1, 1], extrapolate=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d85a10>\n
"},{"location":"interpolation/#domains","title":"Domains","text":"

By default, interpolation has an input domain of [0, 1]. This domain applies to an entire interpolation, even ones that span multiple colors. Generally, this is sufficient and can be used to generate color scales, mixes, and steps in any way that a user needs. When generating colors that should align with data, custom domains can be quite helpful.

For instance, associating colors with temperature.

>>> i = Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n>>> i(-32)\ncolor(--oklab 0.45201 -0.03246 -0.31153 / 1)\n>>> i(47)\ncolor(--oklab 0.75988 -0.10337 0.15637 / 1)\n>>> i(89)\ncolor(--oklab 0.7268 0.12391 0.14717 / 1)\n>>> i\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5db4150>\n

It should be noted that you are not constrained to provide the exact same amount of domain values as you have colors and can have differing amounts, but if you want to align specific colors to certain data points, then it helps.

>>> Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5e00650>\n>>> Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d30210>\n

Lastly, domains must be specified in ascending order of values. If a value decreases in magnitude, it will assume the value that comes right before it. This means you cannot put a domain in reverse. If you need to reverse the order, just flip the color order and setup the domain accordingly.

>>> i = Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 20, 95]\n... )\n>>> i.domain\n<bound method Interpolator.domain of <coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5da5f10>>\n>>> i\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5da5f10>\n

Custom domains are most useful when working with discrete or interpolate directly, but you can use it in other methods like steps as well. As steps does not take data point inputs like interpolate, we do not need to use the temperature data as an input except to set the domain, but the steps will be generated with the same alignment relative to the domain range.

>>> Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5efce50>\n>>> Color.discrete(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c82850>\n>>> Color.steps(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     steps=11,\n...     domain=[-32, 32, 60, 85, 95]\n... )\n[color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.46546 -0.05386 -0.22834 / 1), color(--oklab 0.4789 -0.07526 -0.14516 / 1), color(--oklab 0.49234 -0.09666 -0.06197 / 1), color(--oklab 0.50578 -0.11806 0.02122 / 1), color(--oklab 0.51922 -0.13946 0.1044 / 1), color(--oklab 0.71505 -0.11027 0.14728 / 1), color(--oklab 0.91836 -0.079 0.18851 / 1), color(--oklab 0.90067 -0.02222 0.18429 / 1), color(--oklab 0.81162 0.04279 0.1654 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1)]\n

Wile you can technically feed domain into mix, it is probably not as useful. It will respect the domain alignment, but mix always accepts a percentage of [0, 1], regardless of the underlying domain.

"},{"location":"interpolation/#extrapolation","title":"Extrapolation","text":"

By default, ColorAide clamps the entire progress of an interpolation to always be within the domain ([0, 1] by default). In most cases, this is more what most user expects and why this is the default. It should be noted that this does not affect easing functions, as the clamping is done prior to any easing function calls.

If it is desired to extrapolate past 0 and 1, extrapolate can set to True on all interpolation methods.

>>> Color('red').mix('blue', 0.5)\ncolor(--oklab 0.53998 0.0962 -0.09284 / 1)\n>>> Color('red').mix('blue', -0.5, extrapolate=True)\ncolor(--oklab 0.71593 0.35352 0.34453 / 1)\n

As a larger example, we can purposely interpolate over a range with values beyond 0 and 1. Here we extended the range to -0.5 and 1.5.

>>> offset, factor = 0.25, 1.5\n>>> i = Color.interpolate(['red', 'blue'])\n>>> Ramp([i((r * factor / 100) - offset) for r in range(101)])\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62708 0.22358 0.12366 / 1), color(--oklab 0.62444 0.21972 0.1171 / 1), color(--oklab 0.6218 0.21586 0.11054 / 1), color(--oklab 0.61916 0.212 0.10398 / 1), color(--oklab 0.61652 0.20814 0.09742 / 1), color(--oklab 0.61388 0.20428 0.09086 / 1), color(--oklab 0.61124 0.20042 0.0843 / 1), color(--oklab 0.6086 0.19656 0.07774 / 1), color(--oklab 0.60596 0.1927 0.07117 / 1), color(--oklab 0.60332 0.18884 0.06461 / 1), color(--oklab 0.60068 0.18498 0.05805 / 1), color(--oklab 0.59805 0.18112 0.05149 / 1), color(--oklab 0.59541 0.17726 0.04493 / 1), color(--oklab 0.59277 0.1734 0.03837 / 1), color(--oklab 0.59013 0.16954 0.03181 / 1), color(--oklab 0.58749 0.16568 0.02525 / 1), color(--oklab 0.58485 0.16182 0.01869 / 1), color(--oklab 0.58221 0.15796 0.01213 / 1), color(--oklab 0.57957 0.1541 0.00557 / 1), color(--oklab 0.57693 0.15024 -0.00099 / 1), color(--oklab 0.57429 0.14638 -0.00755 / 1), color(--oklab 0.57165 0.14252 -0.01411 / 1), color(--oklab 0.56901 0.13866 -0.02067 / 1), color(--oklab 0.56638 0.1348 -0.02723 / 1), color(--oklab 0.56374 0.13094 -0.0338 / 1), color(--oklab 0.5611 0.12708 -0.04036 / 1), color(--oklab 0.55846 0.12322 -0.04692 / 1), color(--oklab 0.55582 0.11936 -0.05348 / 1), color(--oklab 0.55318 0.1155 -0.06004 / 1), color(--oklab 0.55054 0.11164 -0.0666 / 1), color(--oklab 0.5479 0.10778 -0.07316 / 1), color(--oklab 0.54526 0.10392 -0.07972 / 1), color(--oklab 0.54262 0.10006 -0.08628 / 1), color(--oklab 0.53998 0.0962 -0.09284 / 1), color(--oklab 0.53735 0.09234 -0.0994 / 1), color(--oklab 0.53471 0.08848 -0.10596 / 1), color(--oklab 0.53207 0.08462 -0.11252 / 1), color(--oklab 0.52943 0.08076 -0.11908 / 1), color(--oklab 0.52679 0.0769 -0.12564 / 1), color(--oklab 0.52415 0.07304 -0.1322 / 1), color(--oklab 0.52151 0.06918 -0.13877 / 1), color(--oklab 0.51887 0.06532 -0.14533 / 1), color(--oklab 0.51623 0.06146 -0.15189 / 1), color(--oklab 0.51359 0.05761 -0.15845 / 1), color(--oklab 0.51095 0.05375 -0.16501 / 1), color(--oklab 0.50832 0.04989 -0.17157 / 1), color(--oklab 0.50568 0.04603 -0.17813 / 1), color(--oklab 0.50304 0.04217 -0.18469 / 1), color(--oklab 0.5004 0.03831 -0.19125 / 1), color(--oklab 0.49776 0.03445 -0.19781 / 1), color(--oklab 0.49512 0.03059 -0.20437 / 1), color(--oklab 0.49248 0.02673 -0.21093 / 1), color(--oklab 0.48984 0.02287 -0.21749 / 1), color(--oklab 0.4872 0.01901 -0.22405 / 1), color(--oklab 0.48456 0.01515 -0.23061 / 1), color(--oklab 0.48192 0.01129 -0.23717 / 1), color(--oklab 0.47928 0.00743 -0.24374 / 1), color(--oklab 0.47665 0.00357 -0.2503 / 1), color(--oklab 0.47401 -0.00029 -0.25686 / 1), color(--oklab 0.47137 -0.00415 -0.26342 / 1), color(--oklab 0.46873 -0.00801 -0.26998 / 1), color(--oklab 0.46609 -0.01187 -0.27654 / 1), color(--oklab 0.46345 -0.01573 -0.2831 / 1), color(--oklab 0.46081 -0.01959 -0.28966 / 1), color(--oklab 0.45817 -0.02345 -0.29622 / 1), color(--oklab 0.45553 -0.02731 -0.30278 / 1), color(--oklab 0.45289 -0.03117 -0.30934 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1)]\n>>> i = Color.interpolate(['red', 'blue'], extrapolate=True)\n>>> Ramp([i((r * factor / 100) - offset) for r in range(101)])\n[color(--oklab 0.67194 0.28919 0.23519 / 1), color(--oklab 0.6693 0.28533 0.22863 / 1), color(--oklab 0.66666 0.28147 0.22207 / 1), color(--oklab 0.66402 0.27761 0.21551 / 1), color(--oklab 0.66138 0.27375 0.20895 / 1), color(--oklab 0.65875 0.26989 0.20239 / 1), color(--oklab 0.65611 0.26603 0.19583 / 1), color(--oklab 0.65347 0.26217 0.18927 / 1), color(--oklab 0.65083 0.25831 0.1827 / 1), color(--oklab 0.64819 0.25445 0.17614 / 1), color(--oklab 0.64555 0.2506 0.16958 / 1), color(--oklab 0.64291 0.24674 0.16302 / 1), color(--oklab 0.64027 0.24288 0.15646 / 1), color(--oklab 0.63763 0.23902 0.1499 / 1), color(--oklab 0.63499 0.23516 0.14334 / 1), color(--oklab 0.63235 0.2313 0.13678 / 1), color(--oklab 0.62971 0.22744 0.13022 / 1), color(--oklab 0.62708 0.22358 0.12366 / 1), color(--oklab 0.62444 0.21972 0.1171 / 1), color(--oklab 0.6218 0.21586 0.11054 / 1), color(--oklab 0.61916 0.212 0.10398 / 1), color(--oklab 0.61652 0.20814 0.09742 / 1), color(--oklab 0.61388 0.20428 0.09086 / 1), color(--oklab 0.61124 0.20042 0.0843 / 1), color(--oklab 0.6086 0.19656 0.07774 / 1), color(--oklab 0.60596 0.1927 0.07117 / 1), color(--oklab 0.60332 0.18884 0.06461 / 1), color(--oklab 0.60068 0.18498 0.05805 / 1), color(--oklab 0.59805 0.18112 0.05149 / 1), color(--oklab 0.59541 0.17726 0.04493 / 1), color(--oklab 0.59277 0.1734 0.03837 / 1), color(--oklab 0.59013 0.16954 0.03181 / 1), color(--oklab 0.58749 0.16568 0.02525 / 1), color(--oklab 0.58485 0.16182 0.01869 / 1), color(--oklab 0.58221 0.15796 0.01213 / 1), color(--oklab 0.57957 0.1541 0.00557 / 1), color(--oklab 0.57693 0.15024 -0.00099 / 1), color(--oklab 0.57429 0.14638 -0.00755 / 1), color(--oklab 0.57165 0.14252 -0.01411 / 1), color(--oklab 0.56901 0.13866 -0.02067 / 1), color(--oklab 0.56638 0.1348 -0.02723 / 1), color(--oklab 0.56374 0.13094 -0.0338 / 1), color(--oklab 0.5611 0.12708 -0.04036 / 1), color(--oklab 0.55846 0.12322 -0.04692 / 1), color(--oklab 0.55582 0.11936 -0.05348 / 1), color(--oklab 0.55318 0.1155 -0.06004 / 1), color(--oklab 0.55054 0.11164 -0.0666 / 1), color(--oklab 0.5479 0.10778 -0.07316 / 1), color(--oklab 0.54526 0.10392 -0.07972 / 1), color(--oklab 0.54262 0.10006 -0.08628 / 1), color(--oklab 0.53998 0.0962 -0.09284 / 1), color(--oklab 0.53735 0.09234 -0.0994 / 1), color(--oklab 0.53471 0.08848 -0.10596 / 1), color(--oklab 0.53207 0.08462 -0.11252 / 1), color(--oklab 0.52943 0.08076 -0.11908 / 1), color(--oklab 0.52679 0.0769 -0.12564 / 1), color(--oklab 0.52415 0.07304 -0.1322 / 1), color(--oklab 0.52151 0.06918 -0.13877 / 1), color(--oklab 0.51887 0.06532 -0.14533 / 1), color(--oklab 0.51623 0.06146 -0.15189 / 1), color(--oklab 0.51359 0.05761 -0.15845 / 1), color(--oklab 0.51095 0.05375 -0.16501 / 1), color(--oklab 0.50832 0.04989 -0.17157 / 1), color(--oklab 0.50568 0.04603 -0.17813 / 1), color(--oklab 0.50304 0.04217 -0.18469 / 1), color(--oklab 0.5004 0.03831 -0.19125 / 1), color(--oklab 0.49776 0.03445 -0.19781 / 1), color(--oklab 0.49512 0.03059 -0.20437 / 1), color(--oklab 0.49248 0.02673 -0.21093 / 1), color(--oklab 0.48984 0.02287 -0.21749 / 1), color(--oklab 0.4872 0.01901 -0.22405 / 1), color(--oklab 0.48456 0.01515 -0.23061 / 1), color(--oklab 0.48192 0.01129 -0.23717 / 1), color(--oklab 0.47928 0.00743 -0.24374 / 1), color(--oklab 0.47665 0.00357 -0.2503 / 1), color(--oklab 0.47401 -0.00029 -0.25686 / 1), color(--oklab 0.47137 -0.00415 -0.26342 / 1), color(--oklab 0.46873 -0.00801 -0.26998 / 1), color(--oklab 0.46609 -0.01187 -0.27654 / 1), color(--oklab 0.46345 -0.01573 -0.2831 / 1), color(--oklab 0.46081 -0.01959 -0.28966 / 1), color(--oklab 0.45817 -0.02345 -0.29622 / 1), color(--oklab 0.45553 -0.02731 -0.30278 / 1), color(--oklab 0.45289 -0.03117 -0.30934 / 1), color(--oklab 0.45025 -0.03503 -0.3159 / 1), color(--oklab 0.44762 -0.03889 -0.32246 / 1), color(--oklab 0.44498 -0.04275 -0.32902 / 1), color(--oklab 0.44234 -0.04661 -0.33558 / 1), color(--oklab 0.4397 -0.05047 -0.34214 / 1), color(--oklab 0.43706 -0.05433 -0.3487 / 1), color(--oklab 0.43442 -0.05819 -0.35527 / 1), color(--oklab 0.43178 -0.06205 -0.36183 / 1), color(--oklab 0.42914 -0.06591 -0.36839 / 1), color(--oklab 0.4265 -0.06977 -0.37495 / 1), color(--oklab 0.42386 -0.07363 -0.38151 / 1), color(--oklab 0.42122 -0.07749 -0.38807 / 1), color(--oklab 0.41858 -0.08135 -0.39463 / 1), color(--oklab 0.41595 -0.08521 -0.40119 / 1), color(--oklab 0.41331 -0.08907 -0.40775 / 1), color(--oklab 0.41067 -0.09293 -0.41431 / 1), color(--oklab 0.40803 -0.09679 -0.42087 / 1)]\n

Lastly, it is important to note that this affects stops as well, mainly stops applied to interpolation endpoints. When an endpoint is moved inwards via a color stop, the end range of the interpolation is clamped, extending the star and end color. But when extrapolation is enabled, a color stop on an endpoint essentially moves the start and end interpolation. And since there are no other colors on either end to interpolate with, extrapolation occurs.

>>> Color.interpolate([stop('red', 0.25), stop('blue', 0.75)])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5da6190>\n>>> Color.interpolate([stop('red', 0.25), stop('blue', 0.75)], extrapolate=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d33390>\n
"},{"location":"interpolation/#null-handling","title":"Undefined/NaN Handling","text":"

Color spaces that have hue coordinates often have rules about when the hue is considered relevant. For instance, in the HSL color space, if saturation is zero, the hue is essentially powerless. This is because the color is \"without color\" or achromatic; therefore, the hue can have no affect on the actual color.

ColorAide will generally respect the values a user provides, so if an achromatic HSL color is given a hue of 270 degrees, ColorAide will accept it, but the hue will not affect the color in any meaningful way.

During conversions, such context is lost, and if an achromatic color is converted to the color space like HSL, the resultant color will have a hue that is noted as undefined. This is simply because there is no good hue for achromatic colors as they play no part in the color. Any hue is actually incorrect as achromatic colors have no real hue. Instead, colors will be returned with a value that represents that the hue is missing or undefined, or maybe better worded, could not be defined.

Many libraries, like d3-color, chroma.js, and color.js, represent null hues with NaN (not a number). This is usually done to make color interpolation easier. Some, like d3-color, are a bit more liberal with NaN and will target special cases that are above and beyond the normal rules to help ensure good interpolation. For instance, they not only mark hue undefined on HSL colors when saturation is zero, but they'll mark saturation as NaN when lightness indicates \"black\" or \"white\".

ColorAide also uses NaN, or in Python float('nan'), to represent undefined channels. In certain situations, when a hue is deemed undefined, the hue value will be set to coloraide.NaN, which is just a constant containing float('nan').

When performing linear interpolation, where only two color's channels are ever being evaluated together at a given time, if one color's channel has a NaN, the other color's channel will be used as the result. If both colors have a NaN for the same channel, then NaN will be returned.

Continuous NaN Handling

NaN handling is a bit different for the Continuous and Cubic Spline interpolation approaches. Linear only evaluates colors at a given time, while the others will take into consideration more than two colors. Because the context is much wider and more complicated, NaN values will often get context from both sides.

Notice that in this example, because white's saturation is zero, the hue is undefined. Because the hue is undefined, when the color is mixed with a second color (green), the hue of the second color is used.

>>> color = Color('white').convert('hsl')\n>>> color[:-1]\n[nan, 0.0, 1.0]\n>>> color2 = Color('green').convert('hsl')\n>>> color2[:-1]\n[120.0, 1.0, 0.25098039215686274]\n>>> color.mix(color2, space=\"hsl\")\ncolor(--hsl 120 0.5 0.62549 / 1)\n

But if we manually set the hue to 0 instead of NaN, we can see that the mixing goes quite differently.

>>> color = Color('white').convert('hsl').set('hue', 0)\n>>> color[:-1]\n[0.0, 0.0, 1.0]\n>>> color2 = Color('green').convert('hsl')\n>>> color2[:-1]\n[120.0, 1.0, 0.25098039215686274]\n>>> color.mix(color2, space=\"hsl\")\ncolor(--hsl 60 0.5 0.62549 / 1)\n

Technically, any channel can be set to NaN. And there are various ways to do this. The Color Manipulation documentation goes into the details of how these Nan values naturally occur and the various ways a user and manipulate them.

"},{"location":"interpolation/#carrying-forward","title":"Carrying-Forward","text":"

Experimental

This feature is provided to give parity with CSS behavior. As the spec is still in flux, behavior is subject to change or feature could be removed entirely. Use at your own risk.

CSS introduces the concept of carrying-forward undefined channels of like color spaces during conversion to the interpolating color space. The idea is to provide a sane handling to users who specified undefined channels for interpolation, but did not account for the conversion to the interpolating color space.

If a color has undefined channels, and is converting to a like color space, after conversion the new color will have the same undefined channels, assuming the channels support carrying-forward. The example below demonstrates the concept.

>>> rgb = Color('srgb', [0.5, NaN, 0.8])\n>>> p3 = rgb.convert('display-p3').set('green', NaN)\n>>> rgb, p3\n(color(srgb 0.5 none 0.8 / 1), color(display-p3 0.45659 none 0.76952 / 1))\n

ColorAide, by default, expects the user to be aware that undefined values are lost if conversion is required for interpolation. This is mainly because the intent of the color can be changed during this process, but some users may find the automatic carrying-forward more convenient. For this reason, ColorAide has implemented carrying-forward as an optional feature via the carryforward option.

In this example, interpolating without carrying-forward results in an interpolation between a purplish color and white. Using carrying-forward, we get a purplish color with an undefined green channel. The green channel takes on the white's green channel giving us an interpolation between a more greenish color and white.

>>> Color.interpolate(['color(srgb 0.5 none 0.8)', 'white'], space='display-p3')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d867d0>\n>>> Color.interpolate(['color(srgb 0.5 none 0.8)', 'white'], space='display-p3', carryforward=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d720d0>\n

Depending on the color space, carrying-forward may have better or worse results.

The following table shows channel components supported for carryforward. Spaces may use different names for their channels, but if they are derived from the related space classes, their channels are supported. For instance, xyz is derived from RGBish, so x, y, and z is treated like super saturated r, g, and b.

Space\u00a0Type Channel\u00a0Equivalents RGBish r, g, b LABish l LCHish l, c, h HSLish h, s, l HSVish h, s, v Cylindrical h

Carrying-forward is applied within categories.

Category Components Reds r Greens g Blues b Lightness l Colorfulness c, s Hue h Opponent a a Opponent b b Value v"},{"location":"interpolation/#powerless-hues","title":"Powerless Hues","text":"

Experimental

This feature is provided to give parity with CSS behavior. As the spec is still in flux, behavior is subject to change or feature could be removed entirely. Use at your own risk.

Normally, ColorAide respects the user's explicitly defined hues. This gives the user power to do things like masking off all channels but the hue to interpolate only the hue.

>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ee0090>\n

But when doing this, a user must explicitly define the hue as achromatic if they want the hue to be ignored. Conversions of achromatic colors to a cylindrical space will, in most cases, have the hue automatically set to undefined.

>>> Color.interpolate(['oklch(1 0 0)', 'oklch(0.75 0.2 180)'], space='oklch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ebac90>\n>>> Color.interpolate(['oklch(1 0 None)', 'oklch(0.75 0.2 180)'], space='oklch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dd3190>\n

CSS has the concept of powerless hues which causes explicitly defined hues to be powerless (or act as undefined) when a color is considered achromatic. This means a user never has to think about achromatic hues, so even if the erroneously define a hue, they will automatically be treated as undefined when interpolating. ColorAide implements this behavior via the powerless option.

>>> Color.interpolate(['oklch(1 0 0)', 'oklch(0.75 0.2 180)'], space='oklch', powerless=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dd3350>\n>>> Color.interpolate(['oklch(1 0 None)', 'oklch(0.75 0.2 180)'], space='oklch', powerless=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5dd1810>\n

The one downside is that control over the hue will be diminished to some degree as ColorAide will no longer respect a user's explicit hue if the color is determined to be achromatic.

>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ed3310>\n>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified', powerless=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5e450>\n
"},{"location":"manipulation/","title":"Manipulating Colors","text":"

Once a Color object is created, you have access to all the color channels. Color channels can be read individually or extracted all at once. Getting and setting color channels is flexible and easy, allowing for intuitive access.

"},{"location":"manipulation/#accessing-coordinates","title":"Accessing Coordinates","text":"

There are various ways to get and set the current values of color coordinates. Colors can be accessed by channel name or numerical index directly. We can also manipulate colors within different color spaces.

"},{"location":"manipulation/#access-by-channel-name","title":"Access By Channel Name","text":"

One of the more intuitive ways to access color values is by channel name. Each color space defines the name of each of the available channels. alpha is the one channel name that is always constant no matter the color space.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color['r']\n1.0\n>>> color['g']\n0.6470588235294118\n>>> color['b']\n0.0\n>>> color['alpha']\n1.0\n

Some channels may be also be recognized using an alias. Check the color space's documentation to learn the recognized channel names and aliases.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color['red'] = 0\n>>> color['green'] = 0\n>>> color['blue'] = 1\n>>> color\ncolor(srgb 0 0 1 / 1)\n
"},{"location":"manipulation/#access-by-index","title":"Access By Index","text":"

Color channels can also be read or set by index. Channels are always in logical order. This means, for instance, an RGB color space will have its channel in the order of r, g, b, and alpha. Thealpha channel always being the last channel in any color space. Check out the color space's documentation to learn more about available channels and the order in which they are stored.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color[0]\n1.0\n>>> color[1]\n0.6470588235294118\n>>> color[2]\n0.0\n>>> color[3]\n1.0\n

Because a Color object essentially operates similar to a list, negative values are also allowed.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color[-1] = 0.5\n>>> color\ncolor(srgb 1 0.64706 0 / 0.5)\n
"},{"location":"manipulation/#access-by-iteration","title":"Access By Iteration","text":"

Color objects can also be treated as an iterable object. This allows us to simply loop through the values.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> [c for c in color]\n[1.0, 0.6470588235294118, 0.0, 1.0]\n
"},{"location":"manipulation/#access-by-slicing","title":"Access By Slicing","text":"

As previously mentioned, Color objects operate very similar to lists, and as such, can also be read or set via slicing.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color[:-1]\n[1.0, 0.6470588235294118, 0.0]\n>>> color[:-1] = [0, 0, 1]\n>>> color\ncolor(srgb 0 0 1 / 1)\n
"},{"location":"manipulation/#access-by-type","title":"Access by Type","text":"

New 2.0

When dealing with colors, you have two types of channels: color channels and an alpha channel. These values can be accessed and separated by slicing as mentioned earlier, but some convenience functions have been added to make this easier. coords() and alpha() will retrieve the color channels and the alpha channel respectively.

>>> color = Color(\"srgb\", [1, 0, 1], 0.5)\n>>> color\ncolor(srgb 1 0 1 / 0.5)\n>>> color.alpha()\n0.5\n>>> color.coords()\n[1.0, 0.0, 1.0]\n

In addition, both of these functions offer a special parameter nans that controls whether undefined values are returned as specified or whether they are resolved to defined values.

>>> color = Color(\"hsl\", [NaN, 0, 0.75], 0.5)\n>>> color\ncolor(--hsl none 0 0.75 / 0.5)\n>>> color.coords()\n[nan, 0.0, 0.75]\n>>> color.coords(nans=False)\n[0.0, 0.0, 0.75]\n
"},{"location":"manipulation/#access-by-functions","title":"Access By Functions","text":"

Colors can also be accessed and modified in more advanced ways with special access functions get() and set().

get() provides access to any channel via the channel name for a given color space, but what sets it apart from other channel access methods is that it can indirectly access channels in other color spaces as well.

>>> color = Color(\"pink\")\n>>> color\ncolor(srgb 1 0.75294 0.79608 / 1)\n>>> color.get('red')\n1.0\n>>> color.get('oklch.hue')\n7.085489349755127\n

Like get(), set() is a method that allows for the setting of any color channel via the color channel names. The value can be set via numerical values or functions with more complex logic.

>>> color = Color(\"pink\")\n>>> color\ncolor(srgb 1 0.75294 0.79608 / 1)\n>>> color.set('blue', 0.5)\ncolor(srgb 1 0.75294 0.5 / 1)\n>>> color.set('green', lambda g: g * 1.3)\ncolor(srgb 1 0.97882 0.5 / 1)\n

Since set() returns a reference to the current color object, we can also chain multiple set() operations.

>>> color = Color('black')\n>>> color\ncolor(srgb 0 0 0 / 1)\n>>> color.set('red', 1).set('green', 1)\ncolor(srgb 1 1 0 / 1)\n

Even more interesting is that, like get(), you can modify a channel in another color space indirectly.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color.set('oklab.lightness', 0.50)\ncolor(srgb 0.61518 0.28886 -0.22143 / 1)\n

When getting/setting a color channel in a different color space than the current color space, the underlying color must be converted to the target color space in order to access the channel. When doing this to get/set multiple channels, this can be a bit inefficient. In order to make such operations more efficient, both get() and set() allow for bulk operations. When performing bulk channel operations, the channels operations are performed in the order they are specified; therefore, it is important to group together channels of the same color space to ensure they are accessed with a single conversion.

To get multiple channels, simply provide a list of channels.

>>> color = Color('orange')\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color.get(['oklch.lightness', 'oklch.hue', 'alpha'])\n[0.7926884361521512, 70.66991620195026, 1.0]\n

To set multiple channels, pass a single dictionary containing the channel names and values.

>>> color = Color('orange')\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color.set(\n...     {\n...         'oklch.lightness': lambda l: l - l * 0.25,\n...         'oklch.hue': 270\n...     }\n... )\ncolor(srgb 0.34573 0.45438 0.89059 / 1)\n

Indirect Channel Modifications

Indirect channel modification is very useful, but keep in mind that it may give you access to color spaces that are incompatible due to gamut size. Additionally, the feature converts the color to the target color space, modifies it, and then converts it back making it susceptible to any possible round trip errors.

New in 1.5: Getting/Setting Multiple Channels

"},{"location":"manipulation/#undefined-values","title":"Undefined Values","text":"

Colors can sometimes have undefined channels. This can actually happen in a number of ways. In almost all cases, undefined values are generated or manually inserted in order to help out with interpolation.

  1. Hues can naturally become undefined if the color is achromatic.

    >>> color = Color('white').convert('hsl')\n>>> color[:]\n[nan, 0.0, 1.0, 1.0]\n
  2. When specifying raw data, channels can be explicitly set to undefined, and when an insufficient amount of channel data is provided, the missing channels will be assumed as undefined, the exception is the alpha channel which is assumed to be 1 unless explicitly defined or explicitly set as undefined.

    >>> Color('srgb', [1])[:]\n[1.0, nan, nan, 1.0]\n>>> Color('srgb', [1, 0, 0], NaN)[:]\n[1.0, 0.0, 0.0, nan]\n
  3. Undefined values can also occur when a user specifies a channel with the none keyword in CSS syntax.

    >>> from coloraide import NaN\n>>> color = Color(\"srgb\", [0.3, NaN, 0.4])\n>>> color[:]\n[0.3, nan, 0.4, 1.0]\n>>> color = Color('rgb(30% none 40%)')\n>>> color[:]\n[0.3, nan, 0.4, 1.0]\n
  4. Lastly, a user can use the mask method which is a quick way to set one or multiple channels as undefined. Additionally, it returns a clone leaving the original untouched by default.

    >>> Color('white')[:]\n[1.0, 1.0, 1.0, 1.0]\n>>> Color('white').mask(['red', 'green'])[:]\n[nan, nan, 1.0, 1.0]\n

    The alpha channel can also be masked:

    >>> Color('white').mask('alpha')[-1]\nnan\n

    You can also do inverse masks, or masks that apply to every channel not specified.

    >>> c = Color('white').mask('blue', invert=True)\n>>> c[:]\n[nan, nan, 1.0, nan]\n
"},{"location":"manipulation/#checking-for-undefined-values","title":"Checking for Undefined Values","text":"

As previously mentioned, a color channel can be undefined for a number of reasons. And in cases such as interpolation, undefined values can even be useful. On the other hand, sometimes knowing there is an undefined value or being able to ignore it can be useful.

Undefined values are represented as the float value NaN. And since NaN values are not numbers \u2013 hence the name \"not a number\" \u2013 they don't quite work the same as normal numbers. They don't contribute to math operations like add, multiply, and divide. Any math operation performed with a NaN will simply yield NaN. NaN values are essentially infectious.

At first glance, the behavior of NaN values can seem confusing, but it is actually pretty intuitive. If we define a color with an undefined channel, and try to add to that value, what should we get? In reality, if the value is undefined, how could we possibly add to it? The only sane answer is to return NaN again.

>>> color = Color('color(srgb 1 none 1)')\n>>> color['green'] + 0.5\nnan\n

Because a NaN (or undefined value) may cause surprising results, it can be useful to check if a hue (or any channel) is undefined before applying certain operations where such a value may be undesirable, especially if the color potentially came from an unknown source. To make checking for undefined values easy, the convenience function is_nan has been made available. You can simply give is_nan the property you wish to check, and it will return either True or False.

>>> Color('hsl(none 0% 100%)').is_nan('hue')\nTrue\n

This is equivalent to using the math library and comparing the value directly:

>>> import math\n>>> math.isnan(Color('hsl(none 0% 100%)')['hue'])\nTrue\n
"},{"location":"manipulation/#forcing-defined-values","title":"Forcing Defined Values","text":"

Another way to deal with NaN values is to just ignore them. get(), set(), coords(), and alpha() all can use the nans option to ensure read operations return a defined value.

>>> c = Color('srgb', [])\n>>> c\ncolor(srgb none none none / 1)\n>>> c.get('red', nans=False)\n0.0\n>>> c.set('green', lambda x: x + 3, nans=False)\ncolor(srgb none 3 none / 1)\n

set()

In the context of set(), nans specifically ensures that when a callback function is provided that the input value is transformed into a real value opposed to an undefined value.

We can also use normalize() to just set all channels to defined values, but keep mind, when an achromatic color has a real hue, they will then influence interpolation results if interpolating in that same color space.

>>> c = Color('srgb', [])\n>>> c.normalize(nans=False)\ncolor(srgb 0 0 0 / 1)\n
"},{"location":"manipulation/#how-are-undefined-values-resolved","title":"How are Undefined Values Resolved?","text":"

ColorAide will resolve undefined values when necessary. Resolving undefined values may be needed to compute color distance, convert a color, serialize a color, or various other reasons.

Normally, an undefined value defaults to 0 when forced to be defined, but there are a few cases where this may not always be true.

  1. When a color is achromatic the hue becomes meaningless in most cylindrical color spaces. This makes sense as achromatic colors have no hues, but this is also because the algorithms usually work out this way. When chroma is small enough, it usually makes the hue mathematically insignificant. In these cases, when a hue must be defined, we will generally assume 0 as an arbitrary default, but there are some color spaces who have algorithms where the hue actually becomes more important for precise conversions.

    The color spaces CAM16 JMh and HCT are color models that allow you to set the viewing environment. One of the options determines whether the eye is adapted to the illuminant or not. If not adapted, which is our default for both CAM16 JMh and HCT, you can get an achromatic response where grayscale colors lean heavily into one specific hue. Additionally, achromatic chroma may grow to a value much greater than 0 as lightness increases. If we were to use 0 as a default for chroma and/or hue, we'd actually not convert back to a real achromatic color.

    We can see in the example below that using 0 for an undefined hue in CAM16 JMh will not convert gray back to sRGB properly, but using the one calculated for the color space gets us much closer.

    >>> srgb = Color('gray')\n>>> srgb\ncolor(srgb 0.50196 0.50196 0.50196 / 1)\n>>> jmh = srgb.convert('cam16-jmh')\n>>> jmh.coords(nans=False)\n[43.042092459543426, 1.4670107518796203, 209.53509867238978]\n>>> jmh.convert('srgb')\ncolor(srgb 0.50196 0.50196 0.50196 / 1)\n>>> jmh.set('hue', 0).convert('srgb')\ncolor(srgb 0.51857 0.49597 0.49766 / 1)\n

    For color spaces with more dynamic achromatic response, ColorAide will resolve undefined hues and chroma with real values that are neutral for that color's given lightness. This doesn't just apply to cylindrical spaces either. This behavior can be seen in non cylindrical spaces as well, like the Lab form of CAM16.

    >>> Color('gray').convert('cam16')\ncolor(--cam16 43.042 -1.2764 -0.72317 / 1)\n>>> Color('cam16', [43.042, NaN, NaN]).normalize(nans=False)\ncolor(--cam16 43.042 -1.2763 -0.72315 / 1)\n

    The selected values may not always perfectly precise, but they are much better than blindly assuming zero.

    There are a number of spaces that benefit from this approach: Jzazbz/JzCzhz, IPT, IgPgTg.

  2. Most of the time, if you set all color channels to undefined, when resolved, the color will be black (or white in the case of CMYK). Unfortunately, using 0 for undefined channels in some color spaces can create colors outside the viewable gamut. One such example is ACEScct (a logarithmic encoding of ACES) which has a greater value than zero for black. In this case, setting undefined channels to zero will cause nonsense colors. In this specific case, we use ACEScct's value for black instead of 0 for more a more practical default.

    >>> aces = Color('black').convert('acescct')\n>>> aces\ncolor(--acescct 0.07291 0.07291 0.07291 / 1)\n>>> aces.mask(['alpha'], invert=True, in_place=True)\ncolor(--acescct none none none / 1)\n>>> aces.coords(nans=False)\n[0.0729055341958355, 0.0729055341958355, 0.0729055341958355]\n>>> aces.in_gamut()\nTrue\n>>> aces[:] = [0] * 3\n>>> aces.in_gamut()\nFalse\n

    New 2.3

    ACEScc, another color space that performs a logarithmic encoding of ACES, will also resolve undefined channels to a non-zero value, not because zero is out of gamut, but to ensure consistency across all ACES color spaces which resolve to zero or non-zero equivalent values.

"},{"location":"manipulation/#achromatic-colors","title":"Achromatic Colors","text":"

An achromatic color is a color without any real hue. Essentially, it is devoid of color leaving only shades of gray. Different color spaces represent achromatic colors in different ways.

ColorAide has some special handling of achromatic colors and a few ways to test if a color is achromatic.

"},{"location":"manipulation/#checking-for-achromatic-colors","title":"Checking For Achromatic Colors","text":"

New 2.0

ColorAide generally respects an input color's defined channels, but during conversion, or if normalize() is called, cylindrical color spaces will have their hue set to undefined if the color is achromatic (or very close to achromatic). One easy way to check for achromatic colors is simply to check if the hue is undefined with is_nan().

>>> c = Color('gray').convert('hsl')\n>>> c.is_nan('hue')\nTrue\n

Unfortunately, this assumes that the hue has not been manually altered and this doesn't work with non cylindrical colors. Luckily, ColorAide has a universal way to check if any color is achromatic by using is_achromatic().

>>> color1 = Color('orange')\n>>> color1\ncolor(srgb 1 0.64706 0 / 1)\n>>> color1.is_achromatic()\nFalse\n>>> color2 = Color('gray').convert('lab')\n>>> color2\ncolor(--lab 53.585 0 0 / 1)\n>>> color2.is_achromatic()\nTrue\n>>> color3 = Color('darkgray').convert('hsl').set('hue', 270)\n>>> color3\ncolor(--hsl 270 0 0.66275 / 1)\n>>> color3.is_achromatic()\nTrue\n

is_achromatic() tries to use a reasonable threshold to determine achromatic colors. The method used is usually specific to the color space as it is fastest to test in the color space being evaluated.

"},{"location":"manipulation/#normalizing-achromatic-colors","title":"Normalizing Achromatic Colors","text":"

When ColorAide converts to a cylindrical color, if the color is achromatic, the hue will get set as undefined. This is mainly because when a color gets very close to achromatic, the hues can become nonsensical. Many cylindrical spaces, as chroma (or saturation) approaches zero, the color approaches being achromatic. And as chroma gets smaller, the impact of the hue becomes smaller and smaller. In these cases, when we get very close to achromatic, we don't care what the hue is, so it gets set as undefined. Additionally, having hue set to undefined allows us to interpolate achromatic colors in a sane way, see Interpolation for more info.

There are times that a color can be defined such that it is not in this normalized achromatic state. We can manually define a color not in this state, and we can also force a color out of this state.

Here we can disable the normalization when converting. We can do this with convert() and update()

>>> Color('white').convert('lch')\ncolor(--lch 100 0 none / 1)\n>>> Color('white').convert('lch', norm=False)\ncolor(--lch 100 0 0 / 1)\n

We can also remove the normalization by setting nans to False when using normalize().

>>> Color('white').convert('lch').normalize(nans=False)\ncolor(--lch 100 0 0 / 1)\n

If we want to force a color back into this normalized state, we can just call normalize() without any parameters. Normalize will remove any existing undefined channels and set achromatic hues to undefined.

>>> c = Color('lch', [1, 0, 0])\n>>> c\ncolor(--lch 1 0 0 / 1)\n>>> c.normalize()\ncolor(--lch 1 0 none / 1)\n
"},{"location":"playground/","title":"Playground","text":"Notebook"},{"location":"strings/","title":"String Output","text":"

ColorAide supports serializing colors in the same formats that it accepts as inputs. This includes all CSS formats for the associated color spaces, and if a color space is not supported in CSS, the color(space ...) format. ColorAide exposes various options to allow users to serialize in the form they most prefer.

"},{"location":"strings/#convert-to-strings","title":"Convert to Strings","text":"

Colors can be serialized to strings by using the to_string method. The color class will convert the current color into one of the many of CSS formats supported for the given color space.

>>> Color(\"srgb\", [0.5, 0, 1], 0.3).to_string()\n'rgb(127.5 0 255 / 0.3)'\n

There are a number of options that are common among all color spaces, but there are also some color space specific options. We will only cover the color spaces shipped with ColorAide. It is possible to write a color space plugin that uses very different options.

"},{"location":"strings/#common-options","title":"Common Options","text":"

All color spaces support the following parameters.

"},{"location":"strings/#alpha","title":"Alpha","text":"

alpha is set to None by default and controls whether the alpha channel is shown in the serialized output. When in the default state, alpha will only be shown if the alpha channel has a value less than 100%, but if set to True, alpha will always be shown. Setting to False will cause alpha to be ignored in the output.

"},{"location":"strings/#precision","title":"Precision","text":"

precision controls the precision of the output values. The name is a little misleading as it will actually adjust the precision and scale of the values. The default is 5. In some cases, like the sRGB hex output, precision may not really come into play as hex values are rounded to the nearest whole number.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=5, percent=True)\n'rgb(30.346% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=4, percent=True)\n'rgb(30.35% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=3, percent=True)\n'rgb(30.3% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=2, percent=True)\n'rgb(30% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=1, percent=True)\n'rgb(30% 80% 100%)'\n

Providing a precision of 0 will simply enable simple rounding to the nearest whole number.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=0, percent=True)\n'rgb(30% 75% 100%)'\n

Providing a precision of -1 is a special input that will give the highest, useful precision that can be given. Precision will be given out to double precision. Higher can be used, but will most likely be unhelpful.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=-1, percent=True)\n'rgb(30.345600000000001% 75% 100%)'\n

One note though, format of the value matters. Here we output in the range of 0 - 255. We can see that a precision of 1, in this case, can throw the color out of gamut. So remember to use a sufficient precision for what you are doing and the values you are working in.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=1)\n'rgb(80 200 300)'\n
"},{"location":"strings/#fit","title":"Fit","text":"

fit is set to True by default and controls whether colors are fit to their gamut or not. Some color spaces are technically unbounded, so no fitting may occur in those color spaces. Additionally, some color formats, like sRGB hex, are always fitted (regardless of this setting) as they must fit into the gamut or they cannot be translated.

>>> Color(\"rgb(30% 105% 0%)\").to_string()\n'rgb(109.21 255 60.975)'\n>>> Color(\"rgb(30% 105% 0%)\").to_string(fit=False)\n'rgb(76.5 267.75 0)'\n

Additionally, we can choose a different fitting method by passing fit the name of the method we would like.

>>> Color(\"rgb(30% 105% 0%)\").to_string()\n'rgb(109.21 255 60.975)'\n>>> Color(\"rgb(30% 105% 0%)\").to_string(fit='clip')\n'rgb(76.5 255 0)'\n
"},{"location":"strings/#color","title":"Color","text":"

color, for some color spaces, is the default output, but for others this format can be explicitly requested by setting color to True. If set to True, this will take priority over other format options.

>>> Color(\"rebeccapurple\").to_string(color=True)\n'color(srgb 0.4 0.2 0.6)'\n
"},{"location":"strings/#none","title":"None","text":"

Colors that have undefined channels are internally represented with NaN. On output, these can be displayed as none per the most recent CSS spec. These are very new, so most browsers do not support them. This is disabled by default until a time when this behavior is common enough. NaN values will not survive fitting unless a color channel is naturally undefined. An example would be a hue when the color has saturation or chroma set to zero.

>>> Color('hsl(none 0% 30%)').to_string(none=True)\n'hsl(none 0% 30%)'\n

The one exception is that legacy rgb(), rgba(), hsl(), and hsla() forms (comma separated) do not support none per the CSS spec.

"},{"location":"strings/#format-specific-options","title":"Format Specific Options","text":"

These options may occur in various color spaces depending on the CSS output format.

"},{"location":"strings/#comma","title":"Comma","text":"

In CSS, there are a few color spaces that allow a comma format: srgb and hsl. ColorAide allows these to be read in and to be output in their legacy comma format. These are the only formats that ship with comma support.

If we want commas, we can force the comma syntax by setting comma to True. This can alter some color space output in other subtle ways. As the comma format is the old legacy approach, when sRGB has commas enabled, it will use rgba instead of rgb. If using the non-comma syntax, rgb is always used, even when the color has transparency.

>>> Color(\"rgb(30 75 100 / 20%)\").to_string(comma=True)\n'rgba(30, 75, 100, 0.2)'\n
"},{"location":"strings/#percent","title":"Percent","text":"

RGB, HSL, HWB, CIELab, CIELCh, Oklab, and OkLCh can receive and output colors with optional percents for certain channels. By default, only HSL and HWB output channels with percents by default, and percentages for these color spaces can only be turned off when serializing to the modern syntax (space delimited). When percentage is enabled, ranges will be output in the range of [0%,100%] instead of their usual numeric value.

>>> Color(\"rebeccapurple\").to_string(percent=True)\n'rgb(40% 20% 60%)'\n>>> Color(\"rebeccapurple\").convert('lab').to_string(percent=True)\n'lab(32.393% 30.738% -38.153%)'\n>>> Color(\"rebeccapurple\").convert('hsl').to_string(percent=False)\n'hsl(270 50 40)'\n
"},{"location":"strings/#srgb-specific-options","title":"sRGB Specific Options","text":"

These options are currently specific to the sRGB color space.

"},{"location":"strings/#hex","title":"Hex","text":"

sRGB can output colors to a hex format which is unique compared to HSL and others. Simply enable hex.

>>> Color(\"rebeccapurple\").to_string(hex=True)\n'#663399'\n
"},{"location":"strings/#upper","title":"Upper","text":"

You can force hex to output in uppercase.

>>> Color(\"red\").to_string(hex=True)\n'#ff0000'\n>>> Color(\"red\").to_string(hex=True, upper=True)\n'#FF0000'\n
"},{"location":"strings/#compress","title":"Compress","text":"

When converting to the hex color format, a color can be compressed in certain cases. Enabling compress will compress a hex color if possible.

>>> Color(\"#11223388\").to_string(hex=True)\n'#11223388'\n>>> Color(\"#11223388\").to_string(hex=True, compress=True)\n'#1238'\n
"},{"location":"strings/#names","title":"Names","text":"

sRGB can also output color names. If a color evaluates to a hex code which also evaluates to a color name in the internal CSS color name mapping, then a color name will be returned. If the color does not match a color name, it will fallback to whatever the other options dictate.

>>> Color(\"#663399\").to_string(names=True)\n'rebeccapurple'\n
"},{"location":"temperature/","title":"Correlated Color Temperature","text":"

New 2.4

Correlated color temperature (CCT) is a measurement of the average hue of light as it appears to the eye. It is expressed as the temperature (in Kelvins) something would need to be heated to glow at approximately the same color.

This response can be modeled with a Planckian or black body locus/curve and is often shown in chromaticity diagrams.

1960 Chromaticity Diagram with black body curve in the range of 1,000K - 100,000K

In order to calculate the Planckian locus, color matching functions (CMFs) are needed. The CMFs are obtained from a series of experiments in which subjects set the intensities of three colors required to match a series of monochromatic (single wavelength) lights of equal energy that traverse the visible spectrum. CMFs contain the required data from such experiments and can be used to calculate a number of things, including the Planckian locus.

CMFs

It should be noted that there isn't one set set of CMFs. Over the years there have been multiple attempts to come up with the best CMFs and often done at both 2\u02da and 10\u02da viewing angles. ColorAide only provides CMFS provided by the CIE via coloraide.cmfs, the CIE 1931 2 Degree Standard Observer being the default as it is still the common approach even though better CMFs have been provided.

CMFS coloraide.cmfs.CIE_1931_2DEG coloraide.cmfs.CIE_1964_10DEG coloraide.cmfs.CIE_2015_2DEG coloraide.cmfs.CIE_2015_10DEG

External CMFs could be used as long as they are in the appropriate format and not at increments less than 1nm.

"},{"location":"temperature/#cct","title":"CCT","text":"

When anything gets warm enough it will start to give off light, and the hotter it gets, the more energetic the light is. As an object increases in temperature, it will shift from the red end of the spectrum to the blue end.

ColorAide provides the blackbody() method to generate colors along the black body curve. Simply give it a color space in which the color should be generated and a temperature in Kelvin and ColorAide will return an approximate color along the black body locus.

>>> Steps([Color.blackbody('srgb', t) for t in range(1000, 15000, 50)])\n[color(srgb 1 0.18142 0 / 1), color(srgb 1 0.22157 0 / 1), color(srgb 1 0.25489 0 / 1), color(srgb 1 0.28375 0 / 1), color(srgb 1 0.3093 0 / 1), color(srgb 1 0.33241 0 / 1), color(srgb 1 0.35347 0 / 1), color(srgb 1 0.3729 0 / 1), color(srgb 1 0.39084 0 / 1), color(srgb 1 0.40753 0 / 1), color(srgb 1 0.42314 0 / 1), color(srgb 1 0.43781 0 / 1), color(srgb 1 0.45159 0 / 1), color(srgb 1 0.46445 0 / 1), color(srgb 1 0.47663 0 / 1), color(srgb 1 0.48821 0 / 1), color(srgb 1 0.49913 0 / 1), color(srgb 1 0.50946 0 / 1), color(srgb 1 0.5194 0.00025 / 1), color(srgb 1 0.53171 0.05099 / 1), color(srgb 1 0.54362 0.08665 / 1), color(srgb 1 0.55508 0.11675 / 1), color(srgb 1 0.56621 0.14078 / 1), color(srgb 1 0.57692 0.16381 / 1), color(srgb 1 0.58734 0.18409 / 1), color(srgb 1 0.59742 0.20351 / 1), color(srgb 1 0.60722 0.22203 / 1), color(srgb 1 0.6168 0.23892 / 1), color(srgb 1 0.62597 0.25645 / 1), color(srgb 1 0.63495 0.27277 / 1), color(srgb 1 0.64376 0.28801 / 1), color(srgb 1 0.65219 0.30409 / 1), color(srgb 1 0.66048 0.3192 / 1), color(srgb 1 0.66862 0.33348 / 1), color(srgb 1 0.67644 0.34807 / 1), color(srgb 1 0.68406 0.36243 / 1), color(srgb 1 0.69155 0.37611 / 1), color(srgb 1 0.69892 0.38918 / 1), color(srgb 1 0.70597 0.40288 / 1), color(srgb 1 0.71288 0.41619 / 1), color(srgb 1 0.71969 0.42897 / 1), color(srgb 1 0.7264 0.44127 / 1), color(srgb 1 0.73289 0.45367 / 1), color(srgb 1 0.73912 0.46623 / 1), color(srgb 1 0.74528 0.47835 / 1), color(srgb 1 0.75136 0.49007 / 1), color(srgb 1 0.75737 0.50143 / 1), color(srgb 1 0.7632 0.51278 / 1), color(srgb 1 0.76876 0.52444 / 1), color(srgb 1 0.77426 0.53576 / 1), color(srgb 1 0.7797 0.54675 / 1), color(srgb 1 0.78508 0.55745 / 1), color(srgb 1 0.79039 0.56787 / 1), color(srgb 1 0.79556 0.57824 / 1), color(srgb 1 0.80043 0.58891 / 1), color(srgb 1 0.80525 0.59931 / 1), color(srgb 1 0.81002 0.60946 / 1), color(srgb 1 0.81474 0.61937 / 1), color(srgb 1 0.81941 0.62905 / 1), color(srgb 1 0.82404 0.63852 / 1), color(srgb 1 0.82862 0.64778 / 1), color(srgb 1 0.83282 0.65746 / 1), color(srgb 1 0.83698 0.66693 / 1), color(srgb 1 0.8411 0.6762 / 1), color(srgb 1 0.84518 0.68528 / 1), color(srgb 1 0.84923 0.69418 / 1), color(srgb 1 0.85323 0.7029 / 1), color(srgb 1 0.8572 0.71145 / 1), color(srgb 1 0.86113 0.71985 / 1), color(srgb 1 0.86499 0.72814 / 1), color(srgb 1 0.86851 0.73667 / 1), color(srgb 1 0.87201 0.74505 / 1), color(srgb 1 0.87547 0.75327 / 1), color(srgb 1 0.8789 0.76134 / 1), color(srgb 1 0.88231 0.76927 / 1), color(srgb 1 0.88568 0.77707 / 1), color(srgb 1 0.88902 0.78474 / 1), color(srgb 1 0.89233 0.79228 / 1), color(srgb 1 0.89561 0.7997 / 1), color(srgb 1 0.89887 0.80701 / 1), color(srgb 1 0.9021 0.8142 / 1), color(srgb 1 0.90496 0.82155 / 1), color(srgb 1 0.9078 0.82879 / 1), color(srgb 1 0.91062 0.83591 / 1), color(srgb 1 0.91341 0.84293 / 1), color(srgb 1 0.91618 0.84983 / 1), color(srgb 1 0.91892 0.85664 / 1), color(srgb 1 0.92165 0.86334 / 1), color(srgb 1 0.92435 0.86995 / 1), color(srgb 1 0.92702 0.87646 / 1), color(srgb 1 0.92968 0.88288 / 1), color(srgb 1 0.93231 0.88921 / 1), color(srgb 1 0.93492 0.89545 / 1), color(srgb 1 0.93751 0.90161 / 1), color(srgb 1 0.94008 0.90769 / 1), color(srgb 1 0.94242 0.91377 / 1), color(srgb 1 0.94464 0.9198 / 1), color(srgb 1 0.94685 0.92575 / 1), color(srgb 1 0.94904 0.93162 / 1), color(srgb 1 0.95121 0.9374 / 1), color(srgb 1 0.95337 0.94312 / 1), color(srgb 1 0.9555 0.94876 / 1), color(srgb 1 0.95762 0.95432 / 1), color(srgb 1 0.95973 0.95982 / 1), color(srgb 1 0.96181 0.96525 / 1), color(srgb 1 0.96388 0.97061 / 1), color(srgb 1 0.96594 0.97591 / 1), color(srgb 1 0.96798 0.98114 / 1), color(srgb 1 0.97 0.98631 / 1), color(srgb 1 0.972 0.99142 / 1), color(srgb 1 0.974 0.99646 / 1), color(srgb 0.99855 0.97455 1 / 1), color(srgb 0.99365 0.97172 1 / 1), color(srgb 0.98885 0.96895 1 / 1), color(srgb 0.98416 0.96606 1 / 1), color(srgb 0.97956 0.96315 1 / 1), color(srgb 0.97506 0.96031 1 / 1), color(srgb 0.97065 0.95752 1 / 1), color(srgb 0.96632 0.9548 1 / 1), color(srgb 0.96208 0.95214 1 / 1), color(srgb 0.95793 0.94954 1 / 1), color(srgb 0.95385 0.94699 1 / 1), color(srgb 0.94985 0.94449 1 / 1), color(srgb 0.94593 0.94205 1 / 1), color(srgb 0.94207 0.93966 1 / 1), color(srgb 0.93829 0.93731 1 / 1), color(srgb 0.93458 0.93502 1 / 1), color(srgb 0.93094 0.93277 1 / 1), color(srgb 0.92735 0.93056 1 / 1), color(srgb 0.92384 0.9284 1 / 1), color(srgb 0.92038 0.92627 1 / 1), color(srgb 0.91698 0.92419 1 / 1), color(srgb 0.91364 0.92215 1 / 1), color(srgb 0.91036 0.92015 1 / 1), color(srgb 0.90713 0.91818 1 / 1), color(srgb 0.90395 0.91625 1 / 1), color(srgb 0.90083 0.91435 1 / 1), color(srgb 0.89776 0.91249 1 / 1), color(srgb 0.89474 0.91066 1 / 1), color(srgb 0.89176 0.90887 1 / 1), color(srgb 0.88883 0.9071 1 / 1), color(srgb 0.88604 0.90525 1 / 1), color(srgb 0.88329 0.90342 1 / 1), color(srgb 0.88058 0.90163 1 / 1), color(srgb 0.87791 0.89987 1 / 1), color(srgb 0.87529 0.89814 1 / 1), color(srgb 0.8727 0.89644 1 / 1), color(srgb 0.87016 0.89476 1 / 1), color(srgb 0.86765 0.89312 1 / 1), color(srgb 0.86518 0.8915 1 / 1), color(srgb 0.86274 0.8899 1 / 1), color(srgb 0.86035 0.88833 1 / 1), color(srgb 0.85798 0.88679 1 / 1), color(srgb 0.85565 0.88527 1 / 1), color(srgb 0.85336 0.88377 1 / 1), color(srgb 0.8511 0.8823 1 / 1), color(srgb 0.84886 0.88085 1 / 1), color(srgb 0.84666 0.87942 1 / 1), color(srgb 0.84449 0.87802 1 / 1), color(srgb 0.84235 0.87663 1 / 1), color(srgb 0.84024 0.87527 1 / 1), color(srgb 0.83816 0.87392 1 / 1), color(srgb 0.83611 0.8726 1 / 1), color(srgb 0.83408 0.87129 1 / 1), color(srgb 0.83208 0.87 1 / 1), color(srgb 0.8301 0.86873 1 / 1), color(srgb 0.82816 0.86748 1 / 1), color(srgb 0.82623 0.86625 1 / 1), color(srgb 0.82433 0.86504 1 / 1), color(srgb 0.82246 0.86384 1 / 1), color(srgb 0.82061 0.86265 1 / 1), color(srgb 0.81878 0.86149 1 / 1), color(srgb 0.81698 0.86034 1 / 1), color(srgb 0.8152 0.8592 1 / 1), color(srgb 0.81343 0.85808 1 / 1), color(srgb 0.8117 0.85698 1 / 1), color(srgb 0.80998 0.85589 1 / 1), color(srgb 0.80828 0.85481 1 / 1), color(srgb 0.8066 0.85375 1 / 1), color(srgb 0.80495 0.8527 1 / 1), color(srgb 0.80331 0.85167 1 / 1), color(srgb 0.80176 0.85062 1 / 1), color(srgb 0.80022 0.84959 1 / 1), color(srgb 0.79871 0.84857 1 / 1), color(srgb 0.79721 0.84756 1 / 1), color(srgb 0.79573 0.84656 1 / 1), color(srgb 0.79426 0.84558 1 / 1), color(srgb 0.79282 0.84461 1 / 1), color(srgb 0.79139 0.84365 1 / 1), color(srgb 0.78997 0.8427 1 / 1), color(srgb 0.78857 0.84177 1 / 1), color(srgb 0.78719 0.84084 1 / 1), color(srgb 0.78582 0.83993 1 / 1), color(srgb 0.78447 0.83902 1 / 1), color(srgb 0.78313 0.83813 1 / 1), color(srgb 0.7818 0.83725 1 / 1), color(srgb 0.78049 0.83638 1 / 1), color(srgb 0.7792 0.83552 1 / 1), color(srgb 0.77792 0.83466 1 / 1), color(srgb 0.77665 0.83382 1 / 1), color(srgb 0.77539 0.83299 1 / 1), color(srgb 0.77415 0.83217 1 / 1), color(srgb 0.77292 0.83135 1 / 1), color(srgb 0.77174 0.83054 1 / 1), color(srgb 0.77057 0.82974 1 / 1), color(srgb 0.76941 0.82894 1 / 1), color(srgb 0.76827 0.82816 1 / 1), color(srgb 0.76714 0.82738 1 / 1), color(srgb 0.76602 0.82661 1 / 1), color(srgb 0.76491 0.82585 1 / 1), color(srgb 0.76381 0.8251 1 / 1), color(srgb 0.76272 0.82435 1 / 1), color(srgb 0.76165 0.82362 1 / 1), color(srgb 0.76058 0.82289 1 / 1), color(srgb 0.75953 0.82217 1 / 1), color(srgb 0.75848 0.82145 1 / 1), color(srgb 0.75745 0.82075 1 / 1), color(srgb 0.75642 0.82005 1 / 1), color(srgb 0.75541 0.81935 1 / 1), color(srgb 0.7544 0.81867 1 / 1), color(srgb 0.75341 0.81799 1 / 1), color(srgb 0.75242 0.81732 1 / 1), color(srgb 0.75144 0.81666 1 / 1), color(srgb 0.75048 0.816 1 / 1), color(srgb 0.74952 0.81535 1 / 1), color(srgb 0.74857 0.8147 1 / 1), color(srgb 0.74763 0.81406 1 / 1), color(srgb 0.74669 0.81343 1 / 1), color(srgb 0.74577 0.81281 1 / 1), color(srgb 0.74485 0.81219 1 / 1), color(srgb 0.74395 0.81157 1 / 1), color(srgb 0.74308 0.81096 1 / 1), color(srgb 0.74222 0.81036 1 / 1), color(srgb 0.74137 0.80977 1 / 1), color(srgb 0.74052 0.80918 1 / 1), color(srgb 0.73968 0.80859 1 / 1), color(srgb 0.73886 0.80801 1 / 1), color(srgb 0.73803 0.80744 1 / 1), color(srgb 0.73722 0.80687 1 / 1), color(srgb 0.73641 0.80631 1 / 1), color(srgb 0.73561 0.80575 1 / 1), color(srgb 0.73481 0.80519 1 / 1), color(srgb 0.73403 0.80465 1 / 1), color(srgb 0.73325 0.8041 1 / 1), color(srgb 0.73247 0.80357 1 / 1), color(srgb 0.7317 0.80303 1 / 1), color(srgb 0.73094 0.8025 1 / 1), color(srgb 0.73019 0.80198 1 / 1), color(srgb 0.72944 0.80146 1 / 1), color(srgb 0.7287 0.80094 1 / 1), color(srgb 0.72796 0.80043 1 / 1), color(srgb 0.72723 0.79993 1 / 1), color(srgb 0.72651 0.79943 1 / 1), color(srgb 0.72579 0.79893 1 / 1), color(srgb 0.72508 0.79844 1 / 1), color(srgb 0.72437 0.79795 1 / 1), color(srgb 0.72367 0.79746 1 / 1), color(srgb 0.72297 0.79698 1 / 1), color(srgb 0.72228 0.79651 1 / 1), color(srgb 0.7216 0.79603 1 / 1), color(srgb 0.72092 0.79557 1 / 1), color(srgb 0.72025 0.7951 1 / 1), color(srgb 0.71958 0.79464 1 / 1), color(srgb 0.71892 0.79418 1 / 1), color(srgb 0.71826 0.79373 1 / 1), color(srgb 0.71761 0.79328 1 / 1), color(srgb 0.71697 0.79284 1 / 1), color(srgb 0.71635 0.7924 1 / 1), color(srgb 0.71574 0.79196 1 / 1), color(srgb 0.71514 0.79153 1 / 1), color(srgb 0.71454 0.7911 1 / 1), color(srgb 0.71394 0.79068 1 / 1), color(srgb 0.71335 0.79025 1 / 1), color(srgb 0.71276 0.78984 1 / 1), color(srgb 0.71218 0.78942 1 / 1), color(srgb 0.7116 0.78901 1 / 1), color(srgb 0.71103 0.7886 1 / 1), color(srgb 0.71046 0.78819 1 / 1), color(srgb 0.7099 0.78779 1 / 1), color(srgb 0.70933 0.78739 1 / 1)]\n

ColorAide also provides a method cct() which allows you to get an associated temperature and \u2206uv from a given color. \u2206uv will be discussed later.

>>> color = Color.blackbody('srgb', 2000)\n>>> color\ncolor(srgb 1 0.54362 0.08665 / 1)\n>>> color.cct()\n[1999.9999999999993, -5.5295962522242654e-17]\n
"},{"location":"temperature/#duv","title":"Duv","text":"

In addition to CCT, there is also the concept of Duv or \u2206uv. In the following image, we've now drawn perpendicular lines that intersect the black body curve. These lines are called isotherms. An isotherm is simply a line connecting points having the same temperature at a given time or on average over a given period. In the case of colors, they connect a number of colors that are close to the locus with the same temperature. \u2206uv describes the distance a given uv point is away from the associated uv point on the black body curve, positive being above the curve and negative being below the curve

1960 Chromaticity Diagram with black body curve and isotherms indicating \u00b1 0.03 \u2206uv.

We can calculate a color's associated temperature and its \u2206uv along the associated isotherm.

>>> 'CCT: {}K Duv: {}'.format(*Color('yellow').cct())\n'CCT: 3934.7189746569234K Duv: 0.04024818555895665'\n

CCT and \u2206uv can also be used together to get a specific color that satisfies those requirements.

>>> Color.blackbody('srgb-linear', *Color('yellow').cct())\ncolor(srgb-linear 1 1 0 / 1)\n
"},{"location":"temperature/#limitations","title":"Limitations","text":"

All algorithms to calculate to and from CCT have some limitations and are only approximations, some being more accurate than others. Many algorithms are only accurate up to a certain temperature range. Additionally, it is recommend that colors that exhibit a \u2206uv larger than |5e-2| should be considered inaccurate, and some algorithm's may not do as well out to even this limit.

"},{"location":"temperature/#out-of-gamut-temperatures","title":"Out of Gamut Temperatures","text":"

It should be noted that blackbody() normalizes/scales the returned colors by default as the colors are often much too bright initially, all having a max luminance. This scaling is usually done under a linear RGB color space, Linear Rec2020 being the default as it encompasses the entire curve.

Keep in mind that if the color is not in the display gamut it will need to be gamut mapped, and the gamut mapped value will not exhibit the same temperature. How far off it is will be depends on the disparity of the gamut sizes and how the color was gamut mapped.

CCT of 1200K in relation to the sRGB gamut.

Colors that are outside the traditional RGB gamut (0 - 1) will be scaled to be within that range. If the color is beyond the gamut of the scaling color space, it will not convert back to the same temperature.

>>> c = Color.blackbody('srgb', 1200)\n>>> c\ncolor(srgb 1 0.3093 0 / 1)\n>>> c.cct()\n[1305.4626114185926, -0.0053568532479511925]\n

Using a larger gamut, such as Linear Rec. 2020 that encompasses the entire black body curve, can allow for more accurate results throughout the entire range, but this may be impractical if you need to work in a smaller gamut.

The desired RGB color space to scale within can be specified via scale_space. And if no scaling is desired, it can be turned off by setting scale to False.

>>> c1 = Color.blackbody('display-p3', 1200, scale_space='display-p3-linear')\n>>> c1\ncolor(display-p3 1 0.36021 0.0172 / 1)\n>>> c1.cct()\n[1200.0000066930043, -1.093052476567441e-12]\n>>> c2 = Color.blackbody('display-p3', 1200, scale=False)\n>>> c2\ncolor(display-p3 1.6804 0.62798 0.05495 / 1)\n>>> c2.cct()\n[1200.000006693005, -1.0930524765675185e-12]\n
"},{"location":"temperature/#algorithms","title":"Algorithms","text":"

There are quite a few approaches to calculating to and from CCT, each with their strengths and weaknesses. ColorAide currently only supports a few approaches, specifically those that support the concept of CCT and \u2206uv. Each approach is implemented as a CCT plugin.

Algorithm Key Description Robertson\u00a01968 robertson-1968 Uses the CIE 2\u02da Standard Observer and can handle a range of 1000K - \u221e. Ohno\u00a02013 ohno-2013 Utilizes a combined approach of a triangular and parabolic solver. Current implementation allows for a range of 1000K - 100000K.

Robertson 1968 is the current default CCT approach, but any approach can be selected via the method parameter in blackbody() or cct().

>>> Color.blackbody('srgb-linear', 2500, method='ohno-2013').cct(method='ohno-2013')\n[2500.011703361377, 2.3130833455749082e-07]\n>>> Color.blackbody('srgb-linear', 2500, method='robertson-1968').cct(method='robertson-1968')\n[2499.999999999999, 0.0]\n
"},{"location":"temperature/#robertson-1968","title":"Robertson 1968","text":"

The Robertson 1968 CCT algorithm is registered in Color by default

The \"Robertson 1968\" approach was created by A. R. Robertson and is based on the CIE 2\u02da Standard Observer with a range of 1667K - \u221e. This approach uses a look up table containing 31 precalculated points along the black body curve and is used to approximate temperatures in between.

Robertson's approach is a reasonably fast approximation, but can exhibit moderate errors at times. The margin of error gets increasingly larger at very high temperatures approaching infinity.

ColorAide implements the Robertson 1968 approach by faithfully calculating the original 31 points (with later corrections), but it also uses the same approach to extend the lower range from 1667K to 1000K by calculating 16 additional points. There is no change in behavior from 1667K to \u221e, but it will now properly resolve values as low as 1000K as well.

Practical Range

While Robertson's technically supports a range out to infinity, it becomes increasingly less practical after 100000K due to increasingly less accurate results. Even some results below 100000K may already have fairly sizeable errors.

>>> color = Color.blackbody('srgb-linear', 5000, duv=0.02)\n>>> color\ncolor(srgb-linear 0.94158 1 0.49098 / 1)\n>>> color.cct()\n[5000.000000000003, 0.020000000000000025]\n

Because the calculation logic is built into the plugin, you can actually use the plugin to generate a higher resolution table or even generate one using a different set of CMFs. When registering the plugin, you can configure the CMFs and a few other options to customize the look up table.

Parameters Description cmfs Valid CMFs at a resolution greater than or equal to 1nm. white A white point as xy chromaticity coordinates. mired The mired value points to generate in the table (1e6 / Tkelvin = mired). Values should not be at a resolution lower than 1 mired as it can give the algorithm issues. 0 is acceptable though. sigfig Significant figures to round to. This is required to faithfully generate the values as documented in the papers and is set to 5 by default. If set to 0, no rounding will be done. planck_step This controls the resolution at which the wavelengths in the CMFs are used to calculate the points along the Planckian locus. The original values are calculated with a 1nm resolution, so the default is set to 1. If a given table has a lower resolution, such as 5nm, this value can be adjusted to properly work with that table.

To use a different set of CMFS, such as the CIE 1964 10\u02da Standard Observer, we could override the default plugin.

>>> from coloraide.temperature import robertson_1968\n>>> from coloraide import cmfs\n>>> from coloraide import cat\n>>> mired_points = tuple(range(0, 100, 10)) + tuple(range(100, 601, 25)) + tuple(range(625, 1001, 25))\n>>> class Custom(Color):\n...     ...\n... \n>>> Custom.register(\n...     robertson_1968.Robertson1968(cmfs.CIE_1964_10DEG, cat.WHITES['10deg']['D65'], mired_points, 0),\n...     overwrite=True\n... )\n>>> Steps([Color.blackbody('srgb', t) for t in range(1000, 15000, 50)])\n[color(srgb 1 0.18142 0 / 1), color(srgb 1 0.22157 0 / 1), color(srgb 1 0.25489 0 / 1), color(srgb 1 0.28375 0 / 1), color(srgb 1 0.3093 0 / 1), color(srgb 1 0.33241 0 / 1), color(srgb 1 0.35347 0 / 1), color(srgb 1 0.3729 0 / 1), color(srgb 1 0.39084 0 / 1), color(srgb 1 0.40753 0 / 1), color(srgb 1 0.42314 0 / 1), color(srgb 1 0.43781 0 / 1), color(srgb 1 0.45159 0 / 1), color(srgb 1 0.46445 0 / 1), color(srgb 1 0.47663 0 / 1), color(srgb 1 0.48821 0 / 1), color(srgb 1 0.49913 0 / 1), color(srgb 1 0.50946 0 / 1), color(srgb 1 0.5194 0.00025 / 1), color(srgb 1 0.53171 0.05099 / 1), color(srgb 1 0.54362 0.08665 / 1), color(srgb 1 0.55508 0.11675 / 1), color(srgb 1 0.56621 0.14078 / 1), color(srgb 1 0.57692 0.16381 / 1), color(srgb 1 0.58734 0.18409 / 1), color(srgb 1 0.59742 0.20351 / 1), color(srgb 1 0.60722 0.22203 / 1), color(srgb 1 0.6168 0.23892 / 1), color(srgb 1 0.62597 0.25645 / 1), color(srgb 1 0.63495 0.27277 / 1), color(srgb 1 0.64376 0.28801 / 1), color(srgb 1 0.65219 0.30409 / 1), color(srgb 1 0.66048 0.3192 / 1), color(srgb 1 0.66862 0.33348 / 1), color(srgb 1 0.67644 0.34807 / 1), color(srgb 1 0.68406 0.36243 / 1), color(srgb 1 0.69155 0.37611 / 1), color(srgb 1 0.69892 0.38918 / 1), color(srgb 1 0.70597 0.40288 / 1), color(srgb 1 0.71288 0.41619 / 1), color(srgb 1 0.71969 0.42897 / 1), color(srgb 1 0.7264 0.44127 / 1), color(srgb 1 0.73289 0.45367 / 1), color(srgb 1 0.73912 0.46623 / 1), color(srgb 1 0.74528 0.47835 / 1), color(srgb 1 0.75136 0.49007 / 1), color(srgb 1 0.75737 0.50143 / 1), color(srgb 1 0.7632 0.51278 / 1), color(srgb 1 0.76876 0.52444 / 1), color(srgb 1 0.77426 0.53576 / 1), color(srgb 1 0.7797 0.54675 / 1), color(srgb 1 0.78508 0.55745 / 1), color(srgb 1 0.79039 0.56787 / 1), color(srgb 1 0.79556 0.57824 / 1), color(srgb 1 0.80043 0.58891 / 1), color(srgb 1 0.80525 0.59931 / 1), color(srgb 1 0.81002 0.60946 / 1), color(srgb 1 0.81474 0.61937 / 1), color(srgb 1 0.81941 0.62905 / 1), color(srgb 1 0.82404 0.63852 / 1), color(srgb 1 0.82862 0.64778 / 1), color(srgb 1 0.83282 0.65746 / 1), color(srgb 1 0.83698 0.66693 / 1), color(srgb 1 0.8411 0.6762 / 1), color(srgb 1 0.84518 0.68528 / 1), color(srgb 1 0.84923 0.69418 / 1), color(srgb 1 0.85323 0.7029 / 1), color(srgb 1 0.8572 0.71145 / 1), color(srgb 1 0.86113 0.71985 / 1), color(srgb 1 0.86499 0.72814 / 1), color(srgb 1 0.86851 0.73667 / 1), color(srgb 1 0.87201 0.74505 / 1), color(srgb 1 0.87547 0.75327 / 1), color(srgb 1 0.8789 0.76134 / 1), color(srgb 1 0.88231 0.76927 / 1), color(srgb 1 0.88568 0.77707 / 1), color(srgb 1 0.88902 0.78474 / 1), color(srgb 1 0.89233 0.79228 / 1), color(srgb 1 0.89561 0.7997 / 1), color(srgb 1 0.89887 0.80701 / 1), color(srgb 1 0.9021 0.8142 / 1), color(srgb 1 0.90496 0.82155 / 1), color(srgb 1 0.9078 0.82879 / 1), color(srgb 1 0.91062 0.83591 / 1), color(srgb 1 0.91341 0.84293 / 1), color(srgb 1 0.91618 0.84983 / 1), color(srgb 1 0.91892 0.85664 / 1), color(srgb 1 0.92165 0.86334 / 1), color(srgb 1 0.92435 0.86995 / 1), color(srgb 1 0.92702 0.87646 / 1), color(srgb 1 0.92968 0.88288 / 1), color(srgb 1 0.93231 0.88921 / 1), color(srgb 1 0.93492 0.89545 / 1), color(srgb 1 0.93751 0.90161 / 1), color(srgb 1 0.94008 0.90769 / 1), color(srgb 1 0.94242 0.91377 / 1), color(srgb 1 0.94464 0.9198 / 1), color(srgb 1 0.94685 0.92575 / 1), color(srgb 1 0.94904 0.93162 / 1), color(srgb 1 0.95121 0.9374 / 1), color(srgb 1 0.95337 0.94312 / 1), color(srgb 1 0.9555 0.94876 / 1), color(srgb 1 0.95762 0.95432 / 1), color(srgb 1 0.95973 0.95982 / 1), color(srgb 1 0.96181 0.96525 / 1), color(srgb 1 0.96388 0.97061 / 1), color(srgb 1 0.96594 0.97591 / 1), color(srgb 1 0.96798 0.98114 / 1), color(srgb 1 0.97 0.98631 / 1), color(srgb 1 0.972 0.99142 / 1), color(srgb 1 0.974 0.99646 / 1), color(srgb 0.99855 0.97455 1 / 1), color(srgb 0.99365 0.97172 1 / 1), color(srgb 0.98885 0.96895 1 / 1), color(srgb 0.98416 0.96606 1 / 1), color(srgb 0.97956 0.96315 1 / 1), color(srgb 0.97506 0.96031 1 / 1), color(srgb 0.97065 0.95752 1 / 1), color(srgb 0.96632 0.9548 1 / 1), color(srgb 0.96208 0.95214 1 / 1), color(srgb 0.95793 0.94954 1 / 1), color(srgb 0.95385 0.94699 1 / 1), color(srgb 0.94985 0.94449 1 / 1), color(srgb 0.94593 0.94205 1 / 1), color(srgb 0.94207 0.93966 1 / 1), color(srgb 0.93829 0.93731 1 / 1), color(srgb 0.93458 0.93502 1 / 1), color(srgb 0.93094 0.93277 1 / 1), color(srgb 0.92735 0.93056 1 / 1), color(srgb 0.92384 0.9284 1 / 1), color(srgb 0.92038 0.92627 1 / 1), color(srgb 0.91698 0.92419 1 / 1), color(srgb 0.91364 0.92215 1 / 1), color(srgb 0.91036 0.92015 1 / 1), color(srgb 0.90713 0.91818 1 / 1), color(srgb 0.90395 0.91625 1 / 1), color(srgb 0.90083 0.91435 1 / 1), color(srgb 0.89776 0.91249 1 / 1), color(srgb 0.89474 0.91066 1 / 1), color(srgb 0.89176 0.90887 1 / 1), color(srgb 0.88883 0.9071 1 / 1), color(srgb 0.88604 0.90525 1 / 1), color(srgb 0.88329 0.90342 1 / 1), color(srgb 0.88058 0.90163 1 / 1), color(srgb 0.87791 0.89987 1 / 1), color(srgb 0.87529 0.89814 1 / 1), color(srgb 0.8727 0.89644 1 / 1), color(srgb 0.87016 0.89476 1 / 1), color(srgb 0.86765 0.89312 1 / 1), color(srgb 0.86518 0.8915 1 / 1), color(srgb 0.86274 0.8899 1 / 1), color(srgb 0.86035 0.88833 1 / 1), color(srgb 0.85798 0.88679 1 / 1), color(srgb 0.85565 0.88527 1 / 1), color(srgb 0.85336 0.88377 1 / 1), color(srgb 0.8511 0.8823 1 / 1), color(srgb 0.84886 0.88085 1 / 1), color(srgb 0.84666 0.87942 1 / 1), color(srgb 0.84449 0.87802 1 / 1), color(srgb 0.84235 0.87663 1 / 1), color(srgb 0.84024 0.87527 1 / 1), color(srgb 0.83816 0.87392 1 / 1), color(srgb 0.83611 0.8726 1 / 1), color(srgb 0.83408 0.87129 1 / 1), color(srgb 0.83208 0.87 1 / 1), color(srgb 0.8301 0.86873 1 / 1), color(srgb 0.82816 0.86748 1 / 1), color(srgb 0.82623 0.86625 1 / 1), color(srgb 0.82433 0.86504 1 / 1), color(srgb 0.82246 0.86384 1 / 1), color(srgb 0.82061 0.86265 1 / 1), color(srgb 0.81878 0.86149 1 / 1), color(srgb 0.81698 0.86034 1 / 1), color(srgb 0.8152 0.8592 1 / 1), color(srgb 0.81343 0.85808 1 / 1), color(srgb 0.8117 0.85698 1 / 1), color(srgb 0.80998 0.85589 1 / 1), color(srgb 0.80828 0.85481 1 / 1), color(srgb 0.8066 0.85375 1 / 1), color(srgb 0.80495 0.8527 1 / 1), color(srgb 0.80331 0.85167 1 / 1), color(srgb 0.80176 0.85062 1 / 1), color(srgb 0.80022 0.84959 1 / 1), color(srgb 0.79871 0.84857 1 / 1), color(srgb 0.79721 0.84756 1 / 1), color(srgb 0.79573 0.84656 1 / 1), color(srgb 0.79426 0.84558 1 / 1), color(srgb 0.79282 0.84461 1 / 1), color(srgb 0.79139 0.84365 1 / 1), color(srgb 0.78997 0.8427 1 / 1), color(srgb 0.78857 0.84177 1 / 1), color(srgb 0.78719 0.84084 1 / 1), color(srgb 0.78582 0.83993 1 / 1), color(srgb 0.78447 0.83902 1 / 1), color(srgb 0.78313 0.83813 1 / 1), color(srgb 0.7818 0.83725 1 / 1), color(srgb 0.78049 0.83638 1 / 1), color(srgb 0.7792 0.83552 1 / 1), color(srgb 0.77792 0.83466 1 / 1), color(srgb 0.77665 0.83382 1 / 1), color(srgb 0.77539 0.83299 1 / 1), color(srgb 0.77415 0.83217 1 / 1), color(srgb 0.77292 0.83135 1 / 1), color(srgb 0.77174 0.83054 1 / 1), color(srgb 0.77057 0.82974 1 / 1), color(srgb 0.76941 0.82894 1 / 1), color(srgb 0.76827 0.82816 1 / 1), color(srgb 0.76714 0.82738 1 / 1), color(srgb 0.76602 0.82661 1 / 1), color(srgb 0.76491 0.82585 1 / 1), color(srgb 0.76381 0.8251 1 / 1), color(srgb 0.76272 0.82435 1 / 1), color(srgb 0.76165 0.82362 1 / 1), color(srgb 0.76058 0.82289 1 / 1), color(srgb 0.75953 0.82217 1 / 1), color(srgb 0.75848 0.82145 1 / 1), color(srgb 0.75745 0.82075 1 / 1), color(srgb 0.75642 0.82005 1 / 1), color(srgb 0.75541 0.81935 1 / 1), color(srgb 0.7544 0.81867 1 / 1), color(srgb 0.75341 0.81799 1 / 1), color(srgb 0.75242 0.81732 1 / 1), color(srgb 0.75144 0.81666 1 / 1), color(srgb 0.75048 0.816 1 / 1), color(srgb 0.74952 0.81535 1 / 1), color(srgb 0.74857 0.8147 1 / 1), color(srgb 0.74763 0.81406 1 / 1), color(srgb 0.74669 0.81343 1 / 1), color(srgb 0.74577 0.81281 1 / 1), color(srgb 0.74485 0.81219 1 / 1), color(srgb 0.74395 0.81157 1 / 1), color(srgb 0.74308 0.81096 1 / 1), color(srgb 0.74222 0.81036 1 / 1), color(srgb 0.74137 0.80977 1 / 1), color(srgb 0.74052 0.80918 1 / 1), color(srgb 0.73968 0.80859 1 / 1), color(srgb 0.73886 0.80801 1 / 1), color(srgb 0.73803 0.80744 1 / 1), color(srgb 0.73722 0.80687 1 / 1), color(srgb 0.73641 0.80631 1 / 1), color(srgb 0.73561 0.80575 1 / 1), color(srgb 0.73481 0.80519 1 / 1), color(srgb 0.73403 0.80465 1 / 1), color(srgb 0.73325 0.8041 1 / 1), color(srgb 0.73247 0.80357 1 / 1), color(srgb 0.7317 0.80303 1 / 1), color(srgb 0.73094 0.8025 1 / 1), color(srgb 0.73019 0.80198 1 / 1), color(srgb 0.72944 0.80146 1 / 1), color(srgb 0.7287 0.80094 1 / 1), color(srgb 0.72796 0.80043 1 / 1), color(srgb 0.72723 0.79993 1 / 1), color(srgb 0.72651 0.79943 1 / 1), color(srgb 0.72579 0.79893 1 / 1), color(srgb 0.72508 0.79844 1 / 1), color(srgb 0.72437 0.79795 1 / 1), color(srgb 0.72367 0.79746 1 / 1), color(srgb 0.72297 0.79698 1 / 1), color(srgb 0.72228 0.79651 1 / 1), color(srgb 0.7216 0.79603 1 / 1), color(srgb 0.72092 0.79557 1 / 1), color(srgb 0.72025 0.7951 1 / 1), color(srgb 0.71958 0.79464 1 / 1), color(srgb 0.71892 0.79418 1 / 1), color(srgb 0.71826 0.79373 1 / 1), color(srgb 0.71761 0.79328 1 / 1), color(srgb 0.71697 0.79284 1 / 1), color(srgb 0.71635 0.7924 1 / 1), color(srgb 0.71574 0.79196 1 / 1), color(srgb 0.71514 0.79153 1 / 1), color(srgb 0.71454 0.7911 1 / 1), color(srgb 0.71394 0.79068 1 / 1), color(srgb 0.71335 0.79025 1 / 1), color(srgb 0.71276 0.78984 1 / 1), color(srgb 0.71218 0.78942 1 / 1), color(srgb 0.7116 0.78901 1 / 1), color(srgb 0.71103 0.7886 1 / 1), color(srgb 0.71046 0.78819 1 / 1), color(srgb 0.7099 0.78779 1 / 1), color(srgb 0.70933 0.78739 1 / 1)]\n
"},{"location":"temperature/#ohno-2013","title":"Ohno 2013","text":"

The Ohno 2013 CCT algorithm is registered in Color by default

This is an approach researched by Yoshi Ohno and aims to provide better accuracy. It uses a look up table similar to the Roberson method and employs a combined approach of a triangular solver and a parabolic solver. This can lead to high accuracy if the table is large enough.

Additionally, an \"automatic expansion\" technique can be used that starts with a small table and expands the table on smaller intervals based on the first solution. This can allow for a high accuracy without having to keep a large table in memory.

For good accuracy throughout the range of 1000K - 100000K, as ColorAide supports, a very large table would be needed. If the \"automatic expansion\" technique was used, without caching the data which would cause the table to balloon in memory, the process is much slower.

To mitigate the downside of storing a massive table in memory and to reduce the performance issues when using \"automatic expansion\", ColorAide uses a moderately sized table and creates a spline to interpolate points in between. The expansion technique is then used to get close to the target using the spline as the data table. Once points sufficiently close are found via the automatic expansion, more accurate values are calculated at those locations and are used in the triangular and parabolic solver. This allows us to use a smaller table while mitigating the performance issues associated with the expansion technique, all while maintaining good accuracy. The technique is still slower than the Roberson approach, but it is a more accurate approach.

>>> color = Color.blackbody('srgb-linear', 5000, duv=0.02, method='ohno-2013')\n>>> color\ncolor(srgb-linear 0.94166 1 0.49106 / 1)\n>>> color.cct()\n[5000.015474747112, 0.019993847505661237]\n

ColorAide exposes some of the knobs to control the automatic expansion.

Parameter Description start Used to control the starting range for the search. Default is 1000. For accuracy, the start should not be set lower than 1000. end Used to control the ending range for the search. Default is 100000. For accuracy, the end should not be set higher than 100000. samples Number of sample points to use on each iteration of \"automatic expansion\". The default is 10. iterations Number of iterations to perform when converging close to the temperature. The default is 6 as experimentation seemed indicate it yields the best results with a sample size of 10 and a range of 1000K - 100000K. exact Controls whether the spline approximation is used. When set to True, all values are directly calculated, bypassing the spline. Calculations will be slower when enabled. The default is False and will utilize the spline providing a performance boost.

By default, the entire range of 1000K to 100000K is explored when resolving CCT, but a smaller range can be used. This can be useful if you have an idea of the range, but not the specific value. Using a smaller range may allow for less iterations to achieve the same or better accuracy.

>>> Color('orange').cct(method='ohno-2013')\n[2424.1146637385255, 0.008069417642630583]\n>>> Color('orange').cct(start=2000, end=3000, iterations=3, method='ohno-2013')\n[2424.117026379762, 0.008069404687598869]\n

If a more pure approach is desired, exact can be used to directly use the \"automatic expansion\" without using the spline. The values for the table will be explicitly calculated on the fly. Performance will be affected with minimal to no increase in accuracy.

>>> Color.blackbody('srgb-linear', 5000, duv=0.02).cct(method='ohno-2013')\n[4999.987927661827, 0.020006162744118677]\n>>> Color.blackbody('srgb-linear', 5000, duv=0.02).cct(method='ohno-2013', exact=True)\n[4999.987927661827, 0.020006162744118677]\n

Lastly, the Ohno 2013 method can be used with other CMFs if desired. To do this, the plugin must be instantiated with different CMFs. The plugin supports a few initialization parameters to controls this.

Parameters Description cmfs Valid CMFs at a resolution greater than or equal to 1nm. white A white point as xy chromaticity coordinates. planck_step This controls the resolution at which the wavelengths in the CMFs are used to calculate the points along the Planckian locus. 5 (5nm) is used as the default as it provides decent performance vs accuracy.

To use an different CMFs, such as the CIE 10\u02da Standard Observer, we can add the plugin with our desired configuration, overwriting the defaults.

>>> from coloraide import cmfs\n>>> from coloraide import cat\n>>> from coloraide.temperature.ohno_2013 import Ohno2013\n>>> class Custom(Color):\n...     ...\n... \n>>> Custom.register(\n...     Ohno2013(cmfs.CIE_1964_10DEG, cat.WHITES['10deg']['D65']),\n...     overwrite=True\n... )\n>>> Steps([Custom.blackbody('srgb-linear', t, method='ohno-2013') for t in range(1000, 15000, 50)])\n[color(srgb-linear 1 0.04002 0 / 1), color(srgb-linear 1 0.05154 0 / 1), color(srgb-linear 1 0.06305 0 / 1), color(srgb-linear 1 0.07453 0 / 1), color(srgb-linear 1 0.08593 0 / 1), color(srgb-linear 1 0.09724 0 / 1), color(srgb-linear 1 0.10842 0 / 1), color(srgb-linear 1 0.11944 0 / 1), color(srgb-linear 1 0.1303 0 / 1), color(srgb-linear 1 0.14097 0 / 1), color(srgb-linear 1 0.15144 0 / 1), color(srgb-linear 1 0.16171 0 / 1), color(srgb-linear 1 0.17175 0 / 1), color(srgb-linear 1 0.18157 0 / 1), color(srgb-linear 1 0.19116 0 / 1), color(srgb-linear 1 0.20052 0 / 1), color(srgb-linear 1 0.20965 0 / 1), color(srgb-linear 1 0.21854 0 / 1), color(srgb-linear 1 0.22719 0 / 1), color(srgb-linear 1 0.2371 0.00195 / 1), color(srgb-linear 1 0.2484 0.00608 / 1), color(srgb-linear 1 0.25963 0.01052 / 1), color(srgb-linear 1 0.27081 0.01528 / 1), color(srgb-linear 1 0.28192 0.02035 / 1), color(srgb-linear 1 0.29296 0.02572 / 1), color(srgb-linear 1 0.30393 0.03139 / 1), color(srgb-linear 1 0.31483 0.03735 / 1), color(srgb-linear 1 0.32565 0.04361 / 1), color(srgb-linear 1 0.33639 0.05015 / 1), color(srgb-linear 1 0.34706 0.05696 / 1), color(srgb-linear 1 0.35764 0.06405 / 1), color(srgb-linear 1 0.36814 0.07141 / 1), color(srgb-linear 1 0.37856 0.07902 / 1), color(srgb-linear 1 0.3889 0.08688 / 1), color(srgb-linear 1 0.39915 0.09499 / 1), color(srgb-linear 1 0.40932 0.10333 / 1), color(srgb-linear 1 0.4194 0.1119 / 1), color(srgb-linear 1 0.4294 0.1207 / 1), color(srgb-linear 1 0.43931 0.12971 / 1), color(srgb-linear 1 0.44913 0.13893 / 1), color(srgb-linear 1 0.45886 0.14835 / 1), color(srgb-linear 1 0.46851 0.15796 / 1), color(srgb-linear 1 0.47807 0.16776 / 1), color(srgb-linear 1 0.48754 0.17774 / 1), color(srgb-linear 1 0.49693 0.1879 / 1), color(srgb-linear 1 0.50622 0.19821 / 1), color(srgb-linear 1 0.51543 0.20869 / 1), color(srgb-linear 1 0.52455 0.21932 / 1), color(srgb-linear 1 0.53358 0.23009 / 1), color(srgb-linear 1 0.54253 0.241 / 1), color(srgb-linear 1 0.55139 0.25204 / 1), color(srgb-linear 1 0.56016 0.26321 / 1), color(srgb-linear 1 0.56885 0.2745 / 1), color(srgb-linear 1 0.57745 0.2859 / 1), color(srgb-linear 1 0.58597 0.29741 / 1), color(srgb-linear 1 0.5944 0.30903 / 1), color(srgb-linear 1 0.60274 0.32073 / 1), color(srgb-linear 1 0.611 0.33253 / 1), color(srgb-linear 1 0.61918 0.34442 / 1), color(srgb-linear 1 0.62728 0.35638 / 1), color(srgb-linear 1 0.63529 0.36842 / 1), color(srgb-linear 1 0.64322 0.38053 / 1), color(srgb-linear 1 0.65107 0.3927 / 1), color(srgb-linear 1 0.65884 0.40493 / 1), color(srgb-linear 1 0.66652 0.41722 / 1), color(srgb-linear 1 0.67413 0.42956 / 1), color(srgb-linear 1 0.68166 0.44195 / 1), color(srgb-linear 1 0.68911 0.45438 / 1), color(srgb-linear 1 0.69648 0.46684 / 1), color(srgb-linear 1 0.70377 0.47934 / 1), color(srgb-linear 1 0.71099 0.49187 / 1), color(srgb-linear 1 0.71813 0.50443 / 1), color(srgb-linear 1 0.72519 0.51701 / 1), color(srgb-linear 1 0.73218 0.52961 / 1), color(srgb-linear 1 0.7391 0.54222 / 1), color(srgb-linear 1 0.74594 0.55485 / 1), color(srgb-linear 1 0.75271 0.56748 / 1), color(srgb-linear 1 0.75941 0.58012 / 1), color(srgb-linear 1 0.76604 0.59277 / 1), color(srgb-linear 1 0.77259 0.60541 / 1), color(srgb-linear 1 0.77908 0.61805 / 1), color(srgb-linear 1 0.78549 0.63068 / 1), color(srgb-linear 1 0.79184 0.64331 / 1), color(srgb-linear 1 0.79812 0.65592 / 1), color(srgb-linear 1 0.80433 0.66853 / 1), color(srgb-linear 1 0.81047 0.68111 / 1), color(srgb-linear 1 0.81655 0.69368 / 1), color(srgb-linear 1 0.82256 0.70623 / 1), color(srgb-linear 1 0.82851 0.71875 / 1), color(srgb-linear 1 0.8344 0.73125 / 1), color(srgb-linear 1 0.84022 0.74373 / 1), color(srgb-linear 1 0.84598 0.75617 / 1), color(srgb-linear 1 0.85167 0.76859 / 1), color(srgb-linear 1 0.85731 0.78097 / 1), color(srgb-linear 1 0.86288 0.79332 / 1), color(srgb-linear 1 0.8684 0.80564 / 1), color(srgb-linear 1 0.87385 0.81792 / 1), color(srgb-linear 1 0.87925 0.83016 / 1), color(srgb-linear 1 0.88459 0.84236 / 1), color(srgb-linear 1 0.88987 0.85452 / 1), color(srgb-linear 1 0.89509 0.86664 / 1), color(srgb-linear 1 0.90026 0.87872 / 1), color(srgb-linear 1 0.90538 0.89075 / 1), color(srgb-linear 1 0.91044 0.90273 / 1), color(srgb-linear 1 0.91544 0.91467 / 1), color(srgb-linear 1 0.92039 0.92656 / 1), color(srgb-linear 1 0.92529 0.93841 / 1), color(srgb-linear 1 0.93014 0.9502 / 1), color(srgb-linear 1 0.93493 0.96195 / 1), color(srgb-linear 1 0.93967 0.97364 / 1), color(srgb-linear 1 0.94437 0.98528 / 1), color(srgb-linear 1 0.94901 0.99687 / 1), color(srgb-linear 0.99167 0.94566 1 / 1), color(srgb-linear 0.9805 0.93947 1 / 1), color(srgb-linear 0.96964 0.93342 1 / 1), color(srgb-linear 0.95906 0.92751 1 / 1), color(srgb-linear 0.94877 0.92173 1 / 1), color(srgb-linear 0.93874 0.91607 1 / 1), color(srgb-linear 0.92897 0.91054 1 / 1), color(srgb-linear 0.91944 0.90513 1 / 1), color(srgb-linear 0.91016 0.89984 1 / 1), color(srgb-linear 0.90111 0.89465 1 / 1), color(srgb-linear 0.89229 0.88958 1 / 1), color(srgb-linear 0.88368 0.88461 1 / 1), color(srgb-linear 0.87528 0.87975 1 / 1), color(srgb-linear 0.86709 0.87499 1 / 1), color(srgb-linear 0.85909 0.87032 1 / 1), color(srgb-linear 0.85127 0.86574 1 / 1), color(srgb-linear 0.84364 0.86126 1 / 1), color(srgb-linear 0.83619 0.85687 1 / 1), color(srgb-linear 0.82891 0.85256 1 / 1), color(srgb-linear 0.8218 0.84833 1 / 1), color(srgb-linear 0.81484 0.84419 1 / 1), color(srgb-linear 0.80804 0.84013 1 / 1), color(srgb-linear 0.8014 0.83614 1 / 1), color(srgb-linear 0.7949 0.83223 1 / 1), color(srgb-linear 0.78854 0.82839 1 / 1), color(srgb-linear 0.78231 0.82463 1 / 1), color(srgb-linear 0.77622 0.82093 1 / 1), color(srgb-linear 0.77027 0.8173 1 / 1), color(srgb-linear 0.76443 0.81374 1 / 1), color(srgb-linear 0.75872 0.81024 1 / 1), color(srgb-linear 0.75313 0.8068 1 / 1), color(srgb-linear 0.74765 0.80342 1 / 1), color(srgb-linear 0.74228 0.80011 1 / 1), color(srgb-linear 0.73702 0.79685 1 / 1), color(srgb-linear 0.73187 0.79364 1 / 1), color(srgb-linear 0.72682 0.79049 1 / 1), color(srgb-linear 0.72187 0.7874 1 / 1), color(srgb-linear 0.71702 0.78436 1 / 1), color(srgb-linear 0.71226 0.78137 1 / 1), color(srgb-linear 0.70759 0.77843 1 / 1), color(srgb-linear 0.70301 0.77553 1 / 1), color(srgb-linear 0.69852 0.77269 1 / 1), color(srgb-linear 0.69412 0.76989 1 / 1), color(srgb-linear 0.68979 0.76714 1 / 1), color(srgb-linear 0.68555 0.76443 1 / 1), color(srgb-linear 0.68138 0.76176 1 / 1), color(srgb-linear 0.67729 0.75914 1 / 1), color(srgb-linear 0.67328 0.75656 1 / 1), color(srgb-linear 0.66934 0.75402 1 / 1), color(srgb-linear 0.66547 0.75152 1 / 1), color(srgb-linear 0.66166 0.74906 1 / 1), color(srgb-linear 0.65793 0.74663 1 / 1), color(srgb-linear 0.65426 0.74424 1 / 1), color(srgb-linear 0.65065 0.74189 1 / 1), color(srgb-linear 0.64711 0.73958 1 / 1), color(srgb-linear 0.64362 0.73729 1 / 1), color(srgb-linear 0.6402 0.73505 1 / 1), color(srgb-linear 0.63683 0.73283 1 / 1), color(srgb-linear 0.63352 0.73065 1 / 1), color(srgb-linear 0.63027 0.7285 1 / 1), color(srgb-linear 0.62707 0.72638 1 / 1), color(srgb-linear 0.62392 0.7243 1 / 1), color(srgb-linear 0.62082 0.72224 1 / 1), color(srgb-linear 0.61778 0.72021 1 / 1), color(srgb-linear 0.61478 0.71821 1 / 1), color(srgb-linear 0.61183 0.71624 1 / 1), color(srgb-linear 0.60893 0.71429 1 / 1), color(srgb-linear 0.60607 0.71238 1 / 1), color(srgb-linear 0.60326 0.71048 1 / 1), color(srgb-linear 0.60049 0.70862 1 / 1), color(srgb-linear 0.59777 0.70678 1 / 1), color(srgb-linear 0.59509 0.70497 1 / 1), color(srgb-linear 0.59245 0.70318 1 / 1), color(srgb-linear 0.58984 0.70141 1 / 1), color(srgb-linear 0.58728 0.69967 1 / 1), color(srgb-linear 0.58476 0.69795 1 / 1), color(srgb-linear 0.58227 0.69625 1 / 1), color(srgb-linear 0.57982 0.69458 1 / 1), color(srgb-linear 0.57741 0.69293 1 / 1), color(srgb-linear 0.57503 0.6913 1 / 1), color(srgb-linear 0.57269 0.68969 1 / 1), color(srgb-linear 0.57038 0.6881 1 / 1), color(srgb-linear 0.56811 0.68653 1 / 1), color(srgb-linear 0.56586 0.68498 1 / 1), color(srgb-linear 0.56365 0.68345 1 / 1), color(srgb-linear 0.56147 0.68194 1 / 1), color(srgb-linear 0.55933 0.68045 1 / 1), color(srgb-linear 0.55721 0.67898 1 / 1), color(srgb-linear 0.55512 0.67752 1 / 1), color(srgb-linear 0.55306 0.67609 1 / 1), color(srgb-linear 0.55102 0.67467 1 / 1), color(srgb-linear 0.54902 0.67327 1 / 1), color(srgb-linear 0.54704 0.67188 1 / 1), color(srgb-linear 0.54509 0.67051 1 / 1), color(srgb-linear 0.54317 0.66916 1 / 1), color(srgb-linear 0.54127 0.66782 1 / 1), color(srgb-linear 0.53939 0.6665 1 / 1), color(srgb-linear 0.53754 0.6652 1 / 1), color(srgb-linear 0.53572 0.66391 1 / 1), color(srgb-linear 0.53392 0.66264 1 / 1), color(srgb-linear 0.53214 0.66138 1 / 1), color(srgb-linear 0.53039 0.66013 1 / 1), color(srgb-linear 0.52866 0.6589 1 / 1), color(srgb-linear 0.52695 0.65769 1 / 1), color(srgb-linear 0.52526 0.65648 1 / 1), color(srgb-linear 0.52359 0.6553 1 / 1), color(srgb-linear 0.52195 0.65412 1 / 1), color(srgb-linear 0.52032 0.65296 1 / 1), color(srgb-linear 0.51871 0.65181 1 / 1), color(srgb-linear 0.51713 0.65067 1 / 1), color(srgb-linear 0.51556 0.64955 1 / 1), color(srgb-linear 0.51402 0.64844 1 / 1), color(srgb-linear 0.51249 0.64734 1 / 1), color(srgb-linear 0.51098 0.64625 1 / 1), color(srgb-linear 0.50949 0.64517 1 / 1), color(srgb-linear 0.50802 0.64411 1 / 1), color(srgb-linear 0.50656 0.64306 1 / 1), color(srgb-linear 0.50512 0.64202 1 / 1), color(srgb-linear 0.5037 0.64099 1 / 1), color(srgb-linear 0.5023 0.63997 1 / 1), color(srgb-linear 0.50091 0.63896 1 / 1), color(srgb-linear 0.49954 0.63796 1 / 1), color(srgb-linear 0.49818 0.63697 1 / 1), color(srgb-linear 0.49684 0.63599 1 / 1), color(srgb-linear 0.49552 0.63503 1 / 1), color(srgb-linear 0.4942 0.63407 1 / 1), color(srgb-linear 0.49291 0.63312 1 / 1), color(srgb-linear 0.49163 0.63219 1 / 1), color(srgb-linear 0.49036 0.63126 1 / 1), color(srgb-linear 0.48911 0.63034 1 / 1), color(srgb-linear 0.48787 0.62943 1 / 1), color(srgb-linear 0.48665 0.62853 1 / 1), color(srgb-linear 0.48544 0.62764 1 / 1), color(srgb-linear 0.48424 0.62675 1 / 1), color(srgb-linear 0.48306 0.62588 1 / 1), color(srgb-linear 0.48188 0.62502 1 / 1), color(srgb-linear 0.48072 0.62416 1 / 1), color(srgb-linear 0.47958 0.62331 1 / 1), color(srgb-linear 0.47844 0.62247 1 / 1), color(srgb-linear 0.47732 0.62164 1 / 1), color(srgb-linear 0.47621 0.62081 1 / 1), color(srgb-linear 0.47511 0.62 1 / 1), color(srgb-linear 0.47403 0.61919 1 / 1), color(srgb-linear 0.47295 0.61839 1 / 1), color(srgb-linear 0.47189 0.6176 1 / 1), color(srgb-linear 0.47083 0.61681 1 / 1), color(srgb-linear 0.46979 0.61603 1 / 1), color(srgb-linear 0.46876 0.61526 1 / 1), color(srgb-linear 0.46774 0.6145 1 / 1), color(srgb-linear 0.46673 0.61374 1 / 1), color(srgb-linear 0.46573 0.61299 1 / 1), color(srgb-linear 0.46474 0.61225 1 / 1), color(srgb-linear 0.46376 0.61151 1 / 1), color(srgb-linear 0.46279 0.61079 1 / 1), color(srgb-linear 0.46182 0.61006 1 / 1), color(srgb-linear 0.46087 0.60935 1 / 1), color(srgb-linear 0.45993 0.60864 1 / 1), color(srgb-linear 0.459 0.60793 1 / 1), color(srgb-linear 0.45808 0.60724 1 / 1), color(srgb-linear 0.45716 0.60655 1 / 1), color(srgb-linear 0.45626 0.60586 1 / 1), color(srgb-linear 0.45536 0.60519 1 / 1), color(srgb-linear 0.45447 0.60451 1 / 1), color(srgb-linear 0.45359 0.60385 1 / 1), color(srgb-linear 0.45272 0.60319 1 / 1), color(srgb-linear 0.45186 0.60253 1 / 1), color(srgb-linear 0.451 0.60188 1 / 1), color(srgb-linear 0.45015 0.60124 1 / 1)]\n
"},{"location":"about/acknowledgments/","title":"Acknowledgments","text":"

All projects gain help and inspiration from somewhere, and we wanted to document the places in which we we gathered knowledge, ideas, and help.

"},{"location":"about/acknowledgments/#projects","title":"Projects","text":""},{"location":"about/acknowledgments/#colorjs","title":"Color.js","text":"

When we began writing ColorAide, we wanted a simple interface to deal with different colors. We also wanted to support CSS colors which a lot of people are familiar with. We had a number of questions about the CSS spec and stumbled on Color.js, a JavaScript library developed and maintained by the co-authors of the CSS spec. We found Color.js and its authors helped clarify a number of confusing points. Additionally, their library did end up heavily inspiring many aspects of our own API as its approach very much aligned with the direction we had already started down.

"},{"location":"about/acknowledgments/#culori","title":"Culori","text":"

The Culori library helped inspire the use of cubic splines as interpolation methods.

"},{"location":"about/acknowledgments/#references","title":"References","text":"

When researching color vision deficiencies, aside from the usual scientific papers, a couple of sites were found to be quite helpful.

  • daltonlens.org was particularly helpful. As it comes from the perspective of an actual Protan, it provided reviews on various algorithm's and explained in great depth some approaches and why they were preferred over others.
  • ixora.io was another useful site that went into great details specifically about the Vi\u00e9not approach.
"},{"location":"about/changelog/","title":"Changelog","text":""},{"location":"about/changelog/#210","title":"2.10","text":"
  • NEW: Declare official support for Python 3.12.
  • NEW: Color.steps and Color.discrete now accept delta_e_args to allow configuring the underlying distance algorithm when using the delta_e option.
  • NEW: CIE Lab, both D50 and D65, are now derived from a CIELab class. CIE LCh, both D50 and D65, are also now derived from a CIELCh class. This makes it easy to determine a CIE Lab or CIE LCh space from other Lab-like spaces.
  • NEW: \u2206E*76, \u2206E*94, \u2206E*00, and \u2206E*cmc all accept a new parameter called space which allows the user to specify a registered Lab color space name (one that is derived from the CIELab class) to use as the distancing color space. This allows a user to use D50 Lab (or any other variant) for distancing if required.
  • FIX: For consistency, \u2206E*94 and \u2206E*cmc now use Lab D65 by default just like \u2206E*76 and \u2206E*00. This fixes an issue where the docs indicated that they use D65, but in actuality they were using D50.
"},{"location":"about/changelog/#291post1","title":"2.9.1.post1","text":"
  • FIX: Fix incorrect changelog mention of recent fix being for HSL instead of HWB.
"},{"location":"about/changelog/#291","title":"2.9.1","text":"
  • FIX: Average should allow controlling powerless be disabled by default for backwards compatibility.
  • FIX: HWB should use the algorithm defined in CSS that allows for round tripping even in the negative lightness direction. Previously we were converting directly from HSV.
"},{"location":"about/changelog/#29","title":"2.9","text":"
  • NEW: Add HWBish mixin class.
  • NEW: Deprecate algebra.no_nan(), algebra.no_nans(), and algebra.is_nan().
  • NEW: When averaging in a cylindrical space, always treat achromatic hues as powerless for better results.
  • NEW: Add experimental support for CSS \"powerless\" hue handling and carrying-forward in interpolation, both disabled by default.
  • FIX: Fix RLAB conversion.
  • FIX: Fix clipping of hues.
  • ENHANCE: Tweaks to some matrix calculations.
  • ENHANCE: Various performance related tweaks.
"},{"location":"about/changelog/#28","title":"2.8","text":"
  • NEW: Add Cubehelix color space.
  • NEW: When precision is set to -1 for string output, double precision (17) will be assumed.
  • ENHANCE: More robust and generally better matrix inverse. Related inverse matrices have been regenerated for consistency.
"},{"location":"about/changelog/#272","title":"2.7.2","text":"
  • FIX: More accurate easing logic.
"},{"location":"about/changelog/#271","title":"2.7.1","text":"
  • FIX: Fix issue where harmony would convert some colors to cylindrical spaces and not properly consider order of channels.
  • FIX: XYB, while Lab like in its default configuration, has such a large disparity in the non-lightness components that the ranges for them should not be the same when using percentages.
  • FIX: Lab like space mixins should not try and order a and b like coordinates when calling indexes(), but should return them in there current order with lightness first. The meaning of these components can be different enough for a given color space to make normalizing their ordered configuration meaningless and alter inherit hue direction when processing for harmony.
"},{"location":"about/changelog/#27","title":"2.7","text":"
  • NEW: Add new RYB color space.
  • NEW: Add Regular mixin class for normal, 3 channel color spaces (sRGB, CMY, RYB, etc.).
  • NEW: harmony() can now accept and transform Labish and Regular color spaces to cylindrical spaces.
"},{"location":"about/changelog/#26","title":"2.6","text":"
  • NEW: Add padding parameter to limit color scales when interpolating.
"},{"location":"about/changelog/#25","title":"2.5","text":"
  • NEW: Add new discrete() function that creates a discrete interpolation object.
  • NEW: Deprecate coloraide.algebra.apply function in favor of new vectorize functions.
  • FIX: Fix small typing issue.
  • FIX: Tweaks to Oklab 64 bit matrix precision.
  • FIX: Fix prismatic and cmyk achromatic check logic.
  • FIX: Ensure IPT uses the exact white point as documented in the paper.
  • FIX: Fix various corner cases of algebraic functions and implement some performance improvements.
"},{"location":"about/changelog/#24","title":"2.4","text":"
  • NEW: Add Rec. 709 RGB color space.
  • NEW: Add the 1960 UCS color space.
  • NEW: Add correlated color temperature support with new cct() and blackbody() API.
  • NEW: Add support for Robertson 1968 and Ohno 2013 CCT plugins.
  • NEW: Add support for determining if a color is in the Pointer Gamut and provide a way to clamp a color to the gamut.
  • NEW: Include CMFS: CIE 1931 2 Degree Standard Observer, CIE 1964 10 Degree Standard Observer, CIE 2015 2 Degree Standard Observer, and CIE 2015 10 Degree Standard Observer.
  • NEW: Add split_chromaticity() method which will split a color into its chromaticity and luminance parts.
  • NEW: Add chromaticity() which will create a new color from a given set of chromaticity coordinates.
  • NEW: Relax chromatic_adaptation() type requirement of white point chromaticity inputs.
  • NEW: luminance(), xy(), and uv() all now accept an optional white point via the white parameter to control the white point in which the returned values are relative to. luminance() still defaults to D65 but will use the current color's white point, like xy() and uv() if white is set to None.
  • NEW: white() now accepts a positional parameter allowing it to output the white point of the current color as various chromaticity coordinates in addition to the default XYZ coordinates.
  • FIX: Fix case where deregistering all plugins with * was not deregistering Filter plugins.
"},{"location":"about/changelog/#23","title":"2.3","text":"
  • NEW ACEScc will now resolve undefined color channels (non-alpha) with a non-zero default that represents black for consistency with other ACES color spaces.
  • ENHANCE: Streamline averaging algorithm to increase performance.
  • FIX: Ensure that HCT consistently clamps negative lightness and chroma to zero.
"},{"location":"about/changelog/#222","title":"2.2.2","text":"
  • FIX: Improve HCT round trip conversion speed and improve conversion in some weak areas.
"},{"location":"about/changelog/#221","title":"2.2.1","text":"
  • FIX: Averaging of a channel set with only undefined values should return an undefined value.
  • FIX: Averaging should be done in linear light by default for a sane default. Default is now srgb-linear.
"},{"location":"about/changelog/#22","title":"2.2","text":"
  • NEW: Add XYB color space.
  • ENHANCE: More efficient averaging.
  • FIX: Fix issue where if all colors have the same channel undefined that a divide by zero can occur.
"},{"location":"about/changelog/#21","title":"2.1","text":"
  • NEW: Add new color averaging method.
  • FIX: Interpolation should not modify any input colors.
"},{"location":"about/changelog/#202","title":"2.0.2","text":"
  • FIX: Consistent normalization of HWB hue.
  • FIX: Consistent normalization of color in harmony monochromatic.
"},{"location":"about/changelog/#201","title":"2.0.1","text":"
  • FIX: Incorrect result when interpolating from a cylindrical space to a rectangular space and using out_space.
"},{"location":"about/changelog/#20","title":"2.0","text":"
  • BREAK: interpolate, steps, mix, filter, compose, and harmony will no longer base the output color on the first input color. Colors will be evaluated in the specified color space and be output in that space unless out_space is used to specify a specific output color space. For migration, specify the desired out_space if the working space does not match the desired output.

  • BREAK: Achromatic and undefined color channel handling has been rewritten. Color space objects no longer utilize the normalize() or achromatic_hue() method and instead now use a new is_achromatic() and resolve_channel() methods.

  • NEW: Expose the new Color.is_achromatic() method to tell if colors, even non-cylindrical colors, are achromatic or reasonably close to achromatic.

  • NEW: Color channel definitions can specify a non-zero default for an undefined channel. Use resolve_channel() for more advanced handling.

  • NEW: CAM16, CAM16 UCS, CAM16 SCD, CAM15 LCD, CAM16 JMh, HCT, Jzazbz, JzCzhz, and IPT all currently require a dynamic approach to detect achromatic colors. Undefined LCh chroma and hue channels and Lab a and b channels can now resolve to non-zero values when undefined for better achromatic interpolation.

  • NEW: ACEScct will now resolve undefined color channels (non-alpha) with a non-zero default that represents black as zero is actually out of gamut for that space.

  • NEW: filter, compose, and harmony all now support the out_space parameter.

  • NEW: All <space>ish mixin classes now give access to normalized names and indexes as names() and indexes() opposed to <space>ish_names() etc. Old methods are still available but are deprecated.

  • NEW: All RGB, HSL, and HSV color spaces are now created with a respective RGBish, HSLish, and HSVish mixin class.

  • NEW: Separable blend modes will now be evaluated in whatever RGB-ish color space is provided.

  • NEW: compose will throw an error if a non-RGB-ish color space is provided.

  • NEW: Color.normalize() added a new nans parameter that when set to False will prevent achromatic hue normalization and will just force all channels to be defined.

  • NEW: Color.coords() and Color.alpha(), which used to be available during the alpha/beta period have been re-added. coords() accesses just the color channels (no alpha channel) while alpha() gets the alpha channel.

  • NEW: Coordinate access functions: get(), set(), coords(), and alpha() functions now have a nans parameter that when set to False will ensure the component(s) is returned as a real number instead of NaN. Set operations only apply this when passing the current value to a callback for relative modification.

  • NEW: A norm parameter is now added to convert and update. When set to False, it will prevent achromatic normalization of hues during conversion. If no conversion is needed, the color is returned as is.

  • NEW: ColorAide used to gamut map colors such as HSL, HSV, and HWB when interpolating into those spaces. This is no longer done. It is possible to gamut map wider gamuts with these color spaces, so it will be up to the user to apply gamut mapping when it is determined they need it.

  • NEW: EXTENDED_RANGE is no longer needed and is removed from current color space classes.

  • NEW: Improved accuracy for Oklab, OkLCh, Okhsl, and Okhsv.

  • NEW: New \"continuous\" interpolation method.

  • FIX: Fix aliases in IPT and IgPgTg.

  • FIX: Fix some conversion issues with CAM16 based color spaces that was caused due to bad achromatic handling.

"},{"location":"about/changelog/#182","title":"1.8.2","text":"
  • FIX: Fix some exception messages.
"},{"location":"about/changelog/#181","title":"1.8.1","text":"
  • FIX: Ensure Judd-Vos correction is applied to linear RGB to LMS conversion for CVD.
  • FIX: Fix outdated API information in docs.
"},{"location":"about/changelog/#180","title":"1.8.0","text":"
  • NEW: Modern sRGB, HSL, and HWB should allow mixed percentage and numbers. HSL and HWB percentages in the hsl() and hwb() formats respectively will resolve to numbers in the range [0, 100]. These changes reflect the latest changes in the CSS Level 4 Color spec.
  • NEW: HSL and HWB can serialize to a modern syntax that does not use percentages, but the default still uses percentages.
  • NEW: Rework CSS parsing for better performance.
  • FIX: Handle some parsing corner cases that are handled by browsers, but not by ColorAide. For example, color(srgb 1-0.5.4) should parse as color(srgb 1 -0.5 0.4).
  • FIX: Ensure that COLOR_FORMAT is respected.
"},{"location":"about/changelog/#171","title":"1.7.1","text":"
  • FIX: Ensure CAM16 spaces mirrors positive and negative percentages for a and b components.
  • FIX: Since the CAM16 JMh model can not predict achromatic colors with negative lightness and, more importantly, negative lightness is not useful, limit the lower end of lightness in CAM16 spaces to zero.
  • FIX: When a CAM16 JMh (or HCT) color's chroma, when not discounting illuminance, has chroma drop below the actual ideal achromatic chroma threshold, just use the ideal chroma to ensure better conversion back to XYZ.
  • FIX: Jzazbz and JzCzhz model can never translate a color with a negative lightness, so just clamp negative lightness while in Jzazbz and JzCzhz.
  • FIX: Fix a math error in CAM16.
  • FIX: Fix CAM16 JMh M limit which was too low.
  • FIX: IPT was set to \"bound\" when it should have an unbounded gamut.
  • FIX: When both comma and none are enabled it could make undefined alpha values show up as none in legacy CSS format.
  • FIX: Sane handling of inverse lightness in DIN99o.
"},{"location":"about/changelog/#17","title":"1.7","text":"
  • NEW: Add support for CAM16 Jab and JMh: cam16 and cam16-jmh respectively.
  • NEW: Add support for CAM16 UCS (Jab forms): cam16-ucs, cam16-scd, and cam16-lcd.
  • NEW: Add support for the HCT color space (hct) which combines the colorfulness and hue from CAM16 JMh and the lightness from CIELab.
  • NEW: Gamut mapping classes derived from fit_lch_chroma can set DE_OPTIONS to pass \u2206E parameters.
  • NEW: While rare, some cylindrical color spaces have an algorithm such that achromatic colors convert best with a very specific hue. Internally, this is now handled during conversions, but there can be reasons where knowing the hue can be useful such as plotting. Cylindrical spaces now expose a method called achromatic_hue() which will return this specific hue if needed.
  • FIX: Fix rec2100-hlg transform.
  • FIX: Some color transformation improvements.
  • FIX: Relax some achromatic detection logic for sRGB cylindrical models. Improves achromatic hue detection results when converting to and from various non-sRGB color spaces.
"},{"location":"about/changelog/#16","title":"1.6","text":"
  • NEW: Add rec2100-hlg color space.
  • BREAKING: rec2100pq should have been named rec2100-pq for consistency. It has been renamed to rec2100-pq and serializes with the CSS ID of --rec2100-pq. This is likely to have little impact on most users.
"},{"location":"about/changelog/#15","title":"1.5","text":"
  • NEW: Formally add support for Python 3.11.
  • NEW: Add support for custom domains when interpolating.
  • NEW: set() can now take a dictionary of channels and values and set multiple channels at once.
  • NEW: get() can now take a list of channels and will return a list of those channel values.
  • ENHANCE: Simplify some type annotation syntax.
  • ENHANCE: Some minor performance enhancements.
  • FIX: Fix OkLCh CSS parsing.
"},{"location":"about/changelog/#14","title":"1.4","text":"
  • NEW: A color space can now declare its dynamic range. By default, spaces are assumed to be SDR, but can declare themselves as HDR, or something else. This allows ColorAide to make decisions based on a color's dynamic range.
  • NEW: Add channel aliases for IPT and IPT-like color spaces (IgPgTg and ICtCp): intensity, protan, and tritan.
  • FIX: The ICtCp and oRGB space would return the Lab-ish equivalents for a and b in reverse order if calling Labish.labish_names. This was not actually called anywhere in the code, but is now fixed for any future cases that may require calling it.
  • FIX: Undefined channels should be ignored when clipping a color.
  • FIX: Do not apply SDR shortcuts in gamut mapping when fitting in a non-SDR color gamut, such as HDR.
"},{"location":"about/changelog/#13","title":"1.3","text":"
  • ENHANCE: Color vision deficiency filters can now be instantiated with different default methods for severe and anomalous cases.
  • FIX: Fix premultiplication handling when using compose.
"},{"location":"about/changelog/#12","title":"1.2","text":"
  • NEW: Add new monotone interpolation method.
  • ENHANCE: Better extrapolation past end of spline.
  • FIX: Small speed up in natural spline calculation.
  • FIX: Fix import that should have been relative, not absolute.
"},{"location":"about/changelog/#11","title":"1.1","text":"
  • NEW: Slight refactor of interpolation plugin so that common code does not need to be duplicated, and the interpolate method no longer needs to accept an easing parameter as the plugin class exposes a new ease method to automatically acquire the proper, specified easing function and apply it.
  • NEW: Functions built upon interpolation can now use a new extrapolate parameter to enable extrapolation if interpolation inputs exceed 0 - 1. point will be passed to Interpolator.interpolate un-clamped if extrapolate is enabled. If a particular interpolation plugin needs to do additional work to handle extrapolation, they can check self.extrapolate to know whether extrapolation is enabled.
  • NEW: Implement and provide the following easing functions as described in the CSS Easing Level 1 spec: cubic_bezier, ease, ease_in, ease_out, and ease_in_out. Also provide a simple linear easing function.
  • New: Add natural and catrom cubic spline options for interpolation. The catrom (Catmull-Rom) spline requires the plugin to be registered in order to use it.
  • FIX: Due to floating point math, B-spline could sometimes return an interpolation of fully opaque colors with an imperceptible amount of transparency. If alpha is very close (1e-6) to being opaque, just round it to opaque.
  • FIX: An easing function's output should not be clamped, only the input, and that only needs to occur on the the outer range of an entire interpolation.
"},{"location":"about/changelog/#10","title":"1.0","text":"

Stable Release!

Checkout migration guide if you were an early adopter.

  • NEW: Bezier interpolation dropped for B-spline which provides much better interpolation.
  • NEW: All new interpolation methods now supports hue fix-ups: shorter, longer, increasing, decreasing, and specified.
  • NEW: Interpolation is now exposed as a plugin to allow for expansion.
  • FIX: Fixed an issue related to premultiplication and undefined alpha channels.
"},{"location":"about/changelog/#10rc1","title":"1.0rc1","text":"

Plugin Refactor

For more flexibility there was one final rework of plugins. Registering requires all plugins to be instantiated before being passed into Color.register, but this allows a user redefine some defaults of certain plugins.

coloraide.ColorAll was moved to coloraide.everythng.ColorAll to avoid allocating plugins when they are not desired.

In the process, we also renamed a number of plugin classes for consistency and predictability, details found below.

  • NEW: Updated some class names for consistency and predictability. XyY \u2192 xyY, Din99o \u2192 DIN99o, SRGB \u2192 sRGB, and ORGB \u2192 oRGB.

    Lastly, LCh should be the default casing convention. This convention will be followed unless a spec mentions otherwise. Changes: Lch \u2192 LCh, LchD65 \u2192 LChD65, Oklch \u2192 OkLCh, Lchuv \u2192 LChuv, Lch99o \u2192 LCh99o, LchChroma \u2192 LChChroma, OklchChroma \u2192 OkLChChroma, and Lchish \u2192 LChish.

  • NEW: Updated migration guide with recent plugin changes.

  • NEW: coloraide.ColorAll renamed and moved to coloraide.everything.ColorAll. This prevents unnecessary inclusion and allocation of objects that are not desired.
  • NEW: Default Color object now only registers bradford CAT by default, all others must be registered separately, or coloraide.everything.Color could be used.
  • NEW: All plugin classes must be instantiated when being registered. This allows some plugins to be instantiated with different defaults. This allows some plugins to be configured with different defaults.

    # Before change:\nColor.register([Plugin1, Plugin2])\n\n# After change:\nColor.register([Plugin1(), Plugin2(optional_parm=True)])\n
  • FIX: Negative luminance is now clamped during contrast calculations.

"},{"location":"about/changelog/#10b3","title":"1.0b3","text":"
  • FIX: Fixed the bad CAT16 matrix for chromatic adaptation.
  • FIX: Small fix related to how CAT plugin classes are defined for better abstraction.
  • FIX: Restrict optional keywords in Color.register() and Color.deregister() to keyword only parameters.
"},{"location":"about/changelog/#10b2","title":"1.0b2","text":"

Breaking Changes

1.0b2 only introduces one more last breaking change that was forgotten in 1.0b1.

  • BREAK: Remove filters parameter on new class instantiation.
  • NEW: Added new migration guide to the documentation to help early adopters move to the 1.0 release.
  • NEW: Added HPLuv space described in the HSLuv spec.
  • NEW: Added new color spaces: ACES 2065-1, ACEScg, ACEScc, and ACEScct.
  • NEW: Contrast is now exposed as a plugin to allow for future expansion of approaches. While there is currently only one approach, methods can be selected via the method attribute.
  • NEW: Add new random method for generating a random color for a given color space.
"},{"location":"about/changelog/#10b1","title":"1.0b1","text":"

Breaking Changes

1.0b1 introduces a number of breaking changes. As we are very close to releasing the first stable release, we've taken opportunity to address any issues related to speed and usability. While this is unfortunate for early adopters, we feel that in the long run that these changes will make ColorAide a better library. We've also added new a new Bezier interpolation method and added many more color spaces!

  • BREAK: The coloraide.Color object now only registers a subset of the available color spaces and \u2206E algorithms in order to create a lighter default color object. coloraide.ColorAll has been provided for a quick way to get access to all available color spaces and plugins. Generally, it is recommend to subclass Color and register just what is desired.

  • BREAK: Reworked interpolation:

    • interpolate and steps functions are now @classmethods. 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):

    {\"space\": \"name\", \"coords\": [0, 0, 0], \"alpha\": 1}\n

    This allows for quicker processing and less complexity dealing with channel names and aliases.

  • BREAK: The CSS Level 4 Color spec has accepted our proposed changes to the gamut mapping algorithm. With this change, the oklch-chroma gamut mapping algorithm is now compliant with the CSS spec, and css-color-4 is no longer needed. If you were experimenting with css-color-4, please use oklch-chroma instead. The algorithm is faster and does not have the color banding issue that css-color-4 had, and it is now exactly the same as the CSS spec.

  • BREAK: New breaking change. Refactor of Space plugins. Space plugins are no longer instantiated which cuts down on overhead lending to better performance. BOUNDS and CHANNEL_NAMES attributes were combined into one attribute called CHANNELS which serves the same purpose as the former attributes. Space plugins also no longer need to define channel property accessors as those are handled through CHANNELS in a more generic way. This is a breaking change for any custom plugins.

    Additionally, the Space plugin's null_adjust method has been renamed as normalize matching its functionality and usage in regards to the Color object. It no longer accepts color coordinates and alpha channel coordinates separately, but will receive them as a single list and return them as such.

  • BREAK: Color's fit and clip methods now perform the operation in place, modifying the current color directly. The in_place parameter has been removed. To create a new color when performing these actions, simply clone the color first: color.clone().clip().

  • BREAK: Remove deprecated dynamic properties which helps to increase speed by removing overhead on class property access.

  • BREAK: Remove deprecated dynamic properties which helps to increase speed by removing overhead on class property access. Use indexing instead: color['red'] or color[0].

  • BREAK: Remove deprecated coords() method. Use indexing and slices instead: color[:-1].

  • 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 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.

    Non-CSS color spaces will also respect their defined ranges when using percentages in the color() form.

  • NEW: Add silent option to deregister so that if a proper category is specified, and the plugin does not exit, the operation will not throw an error.

  • NEW: Add new color spaces: display-p3-linear, a98-rgb-linear, rec2020-linear, prophoto-rgb-linear, and rec2100pq, hsi, rlab, hunter-lab, xyy, prismatic, orgb, cmy, cmyk, ipt, and igpgtg.

  • NEW: Monochromatic color harmony must also be performed in a cylindrical color space to make achromatic detection easier. This means all color harmonies now must be performed under a cylindrical color space.

  • NEW: Use Lab D65 for \u2206E 2000, \u2206E 76, \u2206E HyAB, Euclidean distance, and LCh D65 for LCh Chroma gamut mapping. Lab D65 is far more commonly used for the aforementioned \u2206E methods. LCh Chroma gamut mapping, which uses \u2206E 2000 needs to use the same D65 white point to avoid wasting conversion time.

  • FIX: Better handling of monochromatic harmonies that are near white or black.

  • FIX: Small fix to steps \u2206E logic.

"},{"location":"about/changelog/#0181","title":"0.18.1","text":"
  • FIX: Fix issue where when generating steps with a max_delta_e, the \u2206E was reduced too much causing additional, unnecessary steps along with longer processing time.
"},{"location":"about/changelog/#0180","title":"0.18.0","text":"
  • NEW: Allow dictionary input to use aliases in the dictionary.
  • FIX: If too many channels are given to a color space via raw data, ensure the operation fails.
  • FIX: Sync up achromatic logic of the Okhsl and Okhsv normalize function with the actual conversion algorithm.
  • FIX: Regression that caused cat16 not to work due to a misnamed variable.
"},{"location":"about/changelog/#0170","title":"0.17.0","text":"

Interpolations Are Now Premultiplied

ColorAide has moved to make premultiplication the default for interpolation methods such as mix, steps, and interpolate. The aim is to provide more accurate interpolation when using transparent colors. In cases where premultiplication is not desired, it can be disabled by setting it to False. There are real reasons to do so as it may be desirous to mimic an old implementation that has always used naive interpolation of transparent colors.

Additionally, in the past, premultiplication was not really documented as it had not been fully tested. Premultiplication is now covered in the documentation.

  • NEW: All mixing/interpolation methods will use premultiply=True by default.
  • NEW: Allow aliases in interpolation's progress mappings.
  • FIX: Fix premultiplication when alpha is undefined.
  • FIX: Fix some potential issues in some matrix math logic.
  • FIX: Piecewise() object didn't default all the non-required parameters to None as documented.
"},{"location":"about/changelog/#0160","title":"0.16.0","text":"

Deprecations

In interest of speed, and due to the overhead inflicted on every class attribute access, we've decided to deprecate dynamic properties. This includes dynamic color properties (e.g. Color.red) and dynamic \u2206E methods (e.g. Color.delta_e_2000()). As far as color channel coordinate access is concerned, we've reworked a faster more useful approach. \u2206E already has a suitable replacement and will be the only approach moving forward.

  1. Use of delta_e_<method> is deprecated. Users should use the already available delta_e(color, method=name) approach when using non-default \u2206E methods.

  2. Color channel access has changed. Dynamic channel properties have been deprecated. Usage of Color.coords() has also been deprecated. All channels can now easily be accessed with indexing. Color.get() and Color.set() have not changed.

    • You can index with numbers: Color[0].
    • You can index with channel names: Color['red'].
    • You can slice to get specific color coordinates: Color[:-1].
    • You can get all coordinates: Color[:] or list(Color).
    • You can even iterate coordinates: [c for c in Color].
    • Indexing also supports assignment: Color[0] = 1 or Color[:3] = [1, 1, 1].

Please consider updating usage to utilize the suggested approaches. The aforementioned methods will be removed sometime before the 1.0 release.

  • NEW: Color objects are now indexable and channels can be retrieved using either numbers or strings, e.g., Color[0] or Color['red']. Slicing and assignments via slicing are also supported: Color1[:] = Color2[:].
  • NEW: Color.coords(), dynamic color properties, and dynamic \u2206E methods are all deprecated.
  • NEW: Input method names for distancing, gamut mapping, compositing, and space methods are now case sensitive. There were inconsistencies in some places, so it was opted to make all case sensitive.
  • NEW: The ability to create color harmonies has been added via the new harmony() method. Also, the default color space used to calculate color harmonies can be overridden by the class property HARMONY.
  • NEW: Add new support for filters added via the filter() method. Filters include the W3C Filter Effects Level 1 and color vision deficiency simulation.
  • NEW: Some performance enhancements in conversions.
  • NEW: Chromatic adaptation is now exposed as a plugin. New CAT plugins can be created externally and registered.
  • FIX: Okhsl and Okhsv handling of achromatic values during conversion.
"},{"location":"about/changelog/#0151","title":"0.15.1","text":"
  • FIX: Fix an issue related to matching colors in a buffer at a given offset.
"},{"location":"about/changelog/#0150","title":"0.15.0","text":"

Warning

No changes in the public API have changed, but type annotations have. If you were importing type annotations, you will have to update them.

Also, if any undocumented math related methods were accessed (for plugins or otherwise) they've been moved to coloraide.algebra

  • NEW: A number of performance improvements.
  • NEW: Regenerate all matrices with our own matrix tools so that there is consistency between precision of pre-generated matrices and on-the-fly matrix generation. Reduces some noise in a few color space transforms.
  • NEW: Changes to type annotations. Mutable<type>, where type is either Matrix, Vector, or Array, are simply known as <type>. Types previously specified as <type>, where type is either Matrix, Vector, or Array, are now known as <type>Like. The types are expected to be mutable lists, anything else is noted as \"like\".
  • NEW: All matrix and math utilities have been moved to coloraide.algebra.
  • FIX: Fix rare issue where precision adjustment could fail.
  • FIX: Fix matrix divide logic when dividing a number or vector by a matrix. There are no actual usage of these cases in the code but they were fixed in case they are used in the future.
"},{"location":"about/changelog/#0141","title":"0.14.1","text":"
  • FIX: Fix bug related to parsing strings without full matching.
"},{"location":"about/changelog/#0140","title":"0.14.0","text":"

Note

No changes should break existing color space plugins. Moved objects and references are still also available in old locations, and new functionality is implemented in such a way as to not break existing plugins, but plugins should be updated as sometime before the 1.0 release, such legacy access will be removed.

  • NEW: Faster parsing. Instead of parsing color(space ...) each time it is evaluated for a different color space, parse it generically and then associate it with a given registered color space. If a color spaces wishes to opt out of the color(space ...) input format, the space should set COLOR_FORMAT to False. This means there is no need to call super.match() when overriding Color.match() to ensure support for the color(space ...) format as it will be handled unless COLOR_FORMAT is turned off. DEFAULT_MATCH usage should also be discontinued as it now does nothing.
  • NEW: Other speed optimizations.
  • NEW: All CSS parsing and serialization is now contained in a single module at coloraide.css. This simplifies the current color space classes greatly when it comes to supporting CSS specific formats.
  • NEW: Move our white space mapping to the cat module as it makes more sense there.
  • NEW: GamutBound, GamutUnbound, and associated flags are now contained under coloraide.gamut.bounds.
  • NEW: normalize will also remove masked values to properly adjust the color.
  • FIX: Compositing and blending should not \"fit\" colors before applying, it is only specified that the range should be clamped at the end of blending.
  • FIX: Fix issue where a subclassed Color() object could not recognize the base class or other subclasses.
"},{"location":"about/changelog/#0130","title":"0.13.0","text":"
  • NEW: Add new closest method that takes a list of colors and returns the one that is closet to the calling color object.
  • NEW: CSS color syntax no longer allows for forgiving channels in color(). This means that when a channel other than alpha is omitted, we will no longer treat them as undefined. Instead, the color will simply fail to parse. Raw data channels also must specify all channels.
  • NEW: Clamp lower bounds of chroma at the channel level.
  • NEW: coloraide.spaces.WHITES is now a 2 deep dictionary containing both 2\u02da and 10\u02da observer variants of white points.
  • NEW: Color space plugins now specify WHITE as a tuple with the x and y chromaticity coordinates. This allows a space to specify unknown white points if desired.
  • FIX: Fix longer hue interpolation when \u03b81 - \u03b82 = 0. The spec is wrong in this case, and interpolation should still occur the long way around instead of keeping hue constant.
  • FIX: Reduce redundancy in some CSS parsing patterns.
  • FIX: Minor performance improvements.
  • FIX: Legacy rgb(), rgba(), hsl(), and hsla() comma separated forms in CSS do not support none, only the new space separated forms do.
  • FIX: Ensure py.typed is installed with package so that type annotations work properly.
"},{"location":"about/changelog/#0120","title":"0.12.0","text":"
  • NEW: Add a gamut mapping variant that matches the CSS Color Level 4 spec.
  • FIX: Fix precision rounding issue.
"},{"location":"about/changelog/#0110","title":"0.11.0","text":"

Breaking Changes

  1. Prior to 0.11.0, if you specified a cylindrical space directly, ColorAide would normalize undefined hues the same way that the conversion algorithm did. In the below case, saturation is zero, so the hue was declared undefined.

    >>> Color('hsl(270 0% 50%)')\ncolor(--hsl none 0 0.5 / 1)\n

    We should not have been doing this, and it made some cases of interpolation a bit confusing. It is no longer done as the hues are in fact specified by the user, even if they are powerless in relation to contributing to the rendered color. When a cylindrical color is converted or if a user declares the channel as undefined with none or some other way, then the channel will be declared undefined, because in these cases, they truly are.

    >>> Color('white').convert('hsl')\ncolor(--hsl none 0 1 / 1)\n>>> Color('color(--hsl none 0 0.5)')\ncolor(--hsl none 0 0.5)\n

    If you are working directly in a cylindrical color space and ever wish to force the normalization of color hues as undefined when the color meets the usual requirements as specified by the color space's current rules, just call normalize on the color and it will apply the same logic that occurs during the conversion process.

    >>> Color('hsl(270 0% 50%)').normalize()\ncolor(--hsl none 0 0.5 / 1)\n
    2. If you relied on commas in CSS forms that did not support them, this behavior is no longer allowed. It was thought that CSS may consider allowing comma formats in formats like hwb(), etc., and it was considered, but ultimately the decision was to avoid adding such support. We've updated our input and output support to reflect this. Color spaces can always be subclassed and have this support added back, if desired, but will not be shipped as the default anymore. 3. The D65 form of Luv and LChuv is now the only supported Luv based color spaces by default now. D50 Luv and LChuv have been dropped and luv and lchuv now refers to the D65 version. In most places, the D65 is the most common used white space as most monitors are calibrated for this white point. The only reason CIELab and CIELCh are D50 by default is that CSS requires it. Anyone interested in using Luv with a different white point can easily subclass the current Luv and create a new plugin color space that uses the new white point. 4. Renamed DIN99o LCh identifier to the short name of lch99o.

  • NEW: ColorAide now only ships with the D65 version Luv and LChuv as D65, in most places is the expected white space. Now, the identifier luv and lchuv will refer to the D65 version of the respective color spaces. D50 variants are no longer available by default.
  • NEW: Add the HSLuv color space.
  • NEW: DIN99o LCh identifier was renamed from din99o-lch to lch99o. To use in CSS color() form, use --lch99o.
  • NEW: Refactor chroma reduction/MINDE logic to cut processing time in half. Gamut mapping results remain very similar.
  • NEW: Be more strict with CSS inputs and outputs. hwb(), lab(), lch(), oklab(), and oklch() no longer support comma string formats.
  • NEW: Officially drop Python 3.6 support.
  • FIX: Do not assume user defined, powerless hues as undefined. If they are defined by the user, they should be respected, even if they have no effect on the current color. This helps to ensure interpolations acts in an unsurprising way. If a user manually specifies the channel with none, then it will be considered undefined, or if the color goes through a conversion to a space that cannot pick an appropriate hue, they will also be undefined.
"},{"location":"about/changelog/#0100","title":"0.10.0","text":"
  • NEW: Switch back to using CIELCh for gamut mapping (lch-chroma). There are still some edge cases that make oklch-chroma less desirable.
  • FIX: Fix an issue where when attempting to generate steps some \u2206E distance apart, the maximum step range was not respected and could result in large hangs.
"},{"location":"about/changelog/#090","title":"0.9.0","text":"

Breaking Changes

Custom gamut mapping plugins no longer return coordinates and require the method to update the passed in color.

  • NEW: Improved, faster gamut mapping algorithm.
  • NEW: FIT plugins (gamut mapping) no longer return coordinates but should modify the color passed in.
  • NEW: Expose default interpolation space as a class variable that can be controlled when creating a custom class via class inheritance.
  • NEW: Colors can now directly specify the \u2206E method that is used when interpolating color steps and using max_delta_e via the new delta_e argument. If the delta_e parameter is omitted, the color object's default \u2206E method will be used.
  • NEW: Oklab is now the default interpolation color space.
  • NEW: Interpolation will now avoid fitting colors that are out of gamut unless the color space cannot represent out of gamut colors. Currently, all of the RGB colors (srgb, display-p3, etc.) all support extended ranges, but the HSL, HWB, and HSV color models for srgb (including spaces such as okhsl and okhsv) do not support extended ranges and will still be gamut mapped.
  • FIX: Remove some incorrect code from the gamut mapping algorithm that would shortcut the mapping to reduce chroma to zero.
"},{"location":"about/changelog/#080","title":"0.8.0","text":"

Breaking Changes

The use of xyz as the color space name has been changed in favor of xyz-d65. This better matches the CSS specification. As we are still in a prerelease state, we have not provided any backwards compatibility.

CSS color input strings in the form color(xyz x y z) will continue to be accepted as CSS will allow both the xyz and the xyz-d65 identifier, but output serialization will prefer the color(xyz-d65 x y z) form as using xyz is an alias for xyz-d65.

Again, this breaking change only affects operations where the color space \"name\" is used in the API to specify usage of a specific color space in order to create a color, convert, mutate, interpolate, etc.

Color('red').convert('xyz')      # Bad\nColor('red').convert('xyz-d65')  # Okay\n\nColor('xyz' [0, 0, 0])      # Bad\nColor('xyz-d65' [0, 0, 0])  # Okay\n\nColor('red').interpolate('green', space='xyz')      # Bad\nColor('red').interpolate('green', space='xyz-d65')  # Okay\n\n# No changes to CSS inputs\nColor('color(xyz 0 0 0)')      # Okay\nColor('color(xyz-d65 0 0 0)')  # Okay\n
  • NEW: Add the official CSS syntax oklab() and oklch() for the Oklab and OkLCh color spaces respectively.
  • NEW: Custom fit plugin's fit method now allows additional kwargs in its signature. The API will accept kwargs allowing a custom fit plugin to have configurable parameters. None of the current built-in plugins provide additional parameters, but this is provided in case it is found useful in the future.
  • NEW: XYZ D65 space will now be known as xyz-d65, not xyz. Per the CSS specification, we also ensure XYZ D65 color space serializes as xyz-d65 instead of the alias xyz. CSS input string format will still accept the xyz identifier as this is defined in the CSS specification as an alias for xyz-d65, but when serializing a color to a string, the xyz-d65 will be used as the preferred form.
  • NEW: By default, gamut mapping is done with oklch-chroma which matches the current CSS specification. If desired, the old way (lch-chroma) can manually be specified or set as the default by subclassing Color and setting FIT to lch-chroma.
  • FIX: Ensure the convert method's fit parameter is typed appropriately and is documented correctly.
"},{"location":"about/changelog/#070","title":"0.7.0","text":"
  • NEW: Formally expose srgb-linear as a valid color space.
  • NEW: Distance plugins and gamut mapping plugins now use classmethod instead of staticmethod. This allows for inheritance from other classes and the overriding of plugin options included as class members.
  • NEW: Tweak LCh chroma gamut mapping threshold.
  • FIX: Issue where it is possible, when generating steps, to cause a shift in midpoint of colors if exceeding the maximum steps. Ensure that no stops are injected if injecting a stop between every color would exceed the max steps.
"},{"location":"about/changelog/#060","title":"0.6.0","text":"
  • NEW: Update spaces such that they provide a single conversion point which simplifies color space API and centralizes all conversion logic allowing us to pull chromatic adaptation out of spaces.
  • NEW: color() output format never uses percent when serializing, but will optionally accept percent as input.
  • NEW: Slight refactor of color space, delta E, and gamut mapping plugins. All now specify there name via the property NAME instead of methods space() for color spaces and name() for other plugins.
  • NEW: Restructure source structure by flattening out some directories and better organizing source files. This changes some import paths.
  • NEW: Color spaces do not specify alpha in CHANNEL_NAMES as the alpha name cannot be changed.
  • NEW: Color space objects do not need a constant to track number of color channels.
"},{"location":"about/changelog/#050","title":"0.5.0","text":"
  • NEW: Add type annotations and refactor code to better accommodate the type annotations. Public API not really affected, but a bit of the internals have changed.
  • FIX: Fix issue where compose, if backdrop list is empty, would not respect in_place option.
"},{"location":"about/changelog/#040","title":"0.4.0","text":"
  • NEW: Officially support Python 3.10.
  • NEW: Slightly more accurate Oklab matrix calculation.
  • NEW: Exported dictionary form can now be used as a normal color input in functions like contrast, interpolate, etc.
  • NEW: Color objects will accept a dictionary mapping when alpha is not specified. When this occurs, alpha is assumed to be 1.
  • FIX: Fix an object compare issue.
"},{"location":"about/changelog/#030","title":"0.3.0","text":"

Breaking Changes

XYZ changes below will cause breakage as xyz now refers to XYZ with D65 instead of D50. Also, CSS identifiers changed per the recent specification change.

  • NEW: When calling dir() on Color(), ensure dynamic methods are in the list.
  • NEW: xyz now refers to XYZ D65. CSS color() function now specifies D65 color as either color(xyz x y z) or color(xyz-d65 x y z). XYZ D50 is now specified as color(xyz-D50 x y z).
  • NEW: Add CIELuv and CIELChuv D65 variants.
"},{"location":"about/changelog/#020","title":"0.2.0","text":"
  • NEW: Provide dedicated clip method. clip is still a specifiable method under the fit function. It is also a reserved name under fit and cannot be overridden via plugins or be removed.
  • NEW: Add more conversion shortcuts to OK family of color spaces.
  • FIX: Fix an issue where the shorter conversion path wasn't always taken as convert couldn't find to/from methods if the color space name had - in it.
"},{"location":"about/changelog/#010","title":"0.1.0","text":"

First non-alpha prerelease. Notable changes from the last alpha listed below.

Breaking Changes

There are some breaking changes if coming from the previous alpha releases. All sRGB cylindrical spaces' non-hue data ranges are no longer scaled to 0 - 100, but use 0 - 1. Hue ranges have not changed.

  • NEW: By accepting HSL, HSV, and HWB as non-hue channels as 0-100, we do lose a little precision, so for 1.0, we are switching to accepting and returning raw data values between 0 - 1. We've kept hue between 0 - 360 as it is easier for users to deal with hues between 0 - 360. Doing this will also match the new color spaces Okhsl and Okhsv that need to be kept at 0 - 1 to get better rounding.
  • NEW: We do not currently restrict percentages anymore in color() functions. There is no hard rules that we need to at this time and no currently specified spaces that do this in the CSS specification. This is relaxed for now until some future time when it becomes clear we must.
  • NEW: New okhsl and okhsv color space.
  • NEW: All color channels now accept the none keyword to specify an undefined channel. They can also optionally output CSS strings with the keyword.
  • NEW: Interpolation will return an undefined channel if both colors have that channel set to undefined.
  • NEW: Provide a way to dump a color object to a simple dictionary and have the Color() object accept that dictionary to recreate the color object.
  • NEW: Provide cat16 chromatic adaptation.
  • NEW: Add normalize method to force channel normalization (evaluation of channels and setting undefined as appropriate).
  • NEW: Interpolated and composited colors will normalize undefined channels when returning a color.
  • NEW: Jzazbz now also has an alias for az and bz channels as a and b respectively.
  • FIX: Fix an attribute \"get\" issue where attributes that were not present on the Color() object appeared to be present when using hasattr().
  • FIX: More accurate Oklab matrix.
"},{"location":"about/contributing/","title":"Contributing & Support","text":"

There are many ways to help support this project, regardless of skills and abilities. If you enjoy this project and want to get involved, consider checking out one of the various ways below. Feel free to get creative, there may be other ways to contribute in which we have not thought of!

"},{"location":"about/contributing/#become-a-sponsor","title":"Become a Sponsor","text":"

Open source projects take time and money. Help support the project by becoming a sponsor. You can add your support at any tier you feel comfortable with. No amount is too little. We also accept one time contributions via PayPal.

GitHub Sponsors PayPal

"},{"location":"about/contributing/#bug-reports","title":"Bug Reports","text":"
  1. Please read the documentation and search the issue tracker to try and find the answer to your question before posting an issue.

  2. When creating an issue on the repository, please provide as much info as possible:

    • Version being used.
    • Operating system.
    • Version of Python.
    • Errors in console.
    • Detailed description of the problem.
    • Examples for reproducing the error. You can post pictures, but if specific text or code is required to reproduce the issue, please provide the text in a plain text format for easy copy/paste.

    The more info provided, the greater the chance someone will take the time to answer, implement, or fix the issue.

  3. Be prepared to answer questions and provide additional information if required. Issues in which the creator refuses to respond to follow up questions will be marked as stale and closed.

"},{"location":"about/contributing/#reviewing-code","title":"Reviewing Code","text":"

Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss solutions to overcome weakness in the algorithm.

"},{"location":"about/contributing/#answer-questions-in-issues","title":"Answer Questions in Issues","text":"

Take time and answer questions and offer suggestions to people who've created issues in the issue tracker. Often people will have questions that you might have an answer for. Or maybe you know how to help them accomplish a specific task they are asking about. Feel free to share your experience to help others out.

"},{"location":"about/contributing/#pull-requests","title":"Pull Requests","text":"

Pull requests are welcome, and a great way to help fix bugs and add new features.

"},{"location":"about/contributing/#documentation-improvements","title":"Documentation Improvements","text":"

A ton of time has been spent not only creating and supporting this tool and related extensions, but also spent making this documentation. If you feel it is still lacking, show your appreciation for the tool and/or extensions by helping to improve the documentation.

"},{"location":"about/license/","title":"License","text":"

MIT License

Copyright \u00a9 2020 - 2023 Isaac Muse

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"},{"location":"about/releases/1.0/","title":"1.0 Migration Notes","text":"

ColorAide has been a constantly evolving project. As we've pushed for a stable release, the 1.0 milestone was no exception. For any of the early adopters, there are a number of things to be aware of when migrating to 1.0. In this guide, we'll cover the changes most likely to impact users.

"},{"location":"about/releases/1.0/#plugins","title":"Plugins","text":"

Plugins have gone through a number of reworks. In 1.0, we now require all plugins to be registered as instances. Prior to 1.0 plugins were just passed in un-instantiated. Some plugins were used as static classes almost, and some (color spaces) were instantiated on the creation of every color.

In 1.0, all plugins are required to be instantiated prior to registration. This gives the user the opportunity to specify any alternative defaults if desired.

>>> Color('red').delta_e('blue', method='cmc')\n108.56925233888809\n>>> from coloraide.distance.delta_e_cmc import DECMC\n>>> class Custom(Color):\n...     DELTA_E = \"cmc\"\n... \n>>> Custom.register(DECMC(l=1, c=1), overwrite=True)\n>>> Custom('red').delta_e('blue')\n109.7597278634398\n
"},{"location":"about/releases/1.0/#plugin-renames","title":"Plugin Renames","text":"

ColorAide had some inconsistencies when it came to some plugin names. For instance, we would use variations of Lch, LCH, etc. This made it more difficult to predict how a plugin was named, and remember it. In this case appropriate casing is usually LCh. All plugins referring to LCh were renamed to be more consistent.

Outside of LCh related classes, there were a few additional renames to better match the color space of interest's real name.

Old\u00a0Name New\u00a0Name Lch LCh LchD65 LChD65 Oklch OkLCh Lchuv LChuv Lch99o LCh99o LchChroma LChChroma OklchChroma OkLChchroma Lchish LChish XyY xyY Din99o DIN99o SRGB sRGB SRGBLinear sRGBLinear ORGB oRGB"},{"location":"about/releases/1.0/#default-plugins","title":"Default Plugins","text":"

Over the course of development, we've added a good number of color spaces. With the 1.0 release, it was decided to have the default Color object not register all available color spaces out of the box as the amount of color spaces has grown quite substantially.

As not all color spaces are registered by default, \u2206E plugins tied to color spaces no longer registered by default will also not be registered by default.

Lastly, since the bradford CAT is the default, it was deemed unnecessary to register all the other CAT plugins by default.

With all of that said, all of the exiting plugins are still available and can be included if/when needed. If any of the plugins that are no longer registered by default are needed, there are a couple of options:

  1. The recommended way is to just subclass the Color object and cherry pick the plugins that are needed. When classing, all the plugins registered in the base will be copied over to the derived class. Then we can just pass in the instantiated plugins.

    >>> from coloraide import Color as Base\n>>> from coloraide.spaces.jzazbz import Jzazbz\n>>> from coloraide.distance.delta_e_z import DEZ\n>>> class Color(Base): ...\n... \n>>> Color.register([Jzazbz(), DEZ()])\n>>> Color('red').convert('jzazbz')\ncolor(--jzazbz 0.13438 0.11789 0.11188 / 1)\n>>> Color('red').delta_e('blue', method='jz')\n0.33960388420164006\n
  2. We also provide a new color object derived from Color that includes all color spaces called ColorAll. This object won't be as light, but will provide quick and easy access to everything ColorAide offers. By default, it registers every plugin. It can be found under coloraide.everything.

    >>> from coloraide.everything import ColorAll as Color\n>>> Color('purple').convert('hunter-lab')\ncolor(--hunter-lab 24.796 50.842 -35.444 / 1)\n
"},{"location":"about/releases/1.0/#dynamic-propertiesfunctions-and-coordinate-access","title":"Dynamic Properties/Functions and Coordinate Access","text":"

Prior to 1.0, ColorAide's Color object had color channel properties that would magically mutate based on what the current color space was that the object currently held. While cool, this added overhead to every class attribute access. In an effort to dramatically reduce unnecessary overhead, this feature had to be rethought.

Additionally, \u2206E methods were also added dynamically. For instance, if we had the \u2206E 2000 distancing plugin registered, we'd have access to \u2206E via Color.delta_e_2000 or Color.delta_e(color, method='2000'). Again, the overhead to magically provide these properties the way we were posed the same problem as what was seen with dynamic color channel properties

Additionally, the Color object used to have a coords() function to get all the non-alpha color channels. This function was not really problematic, but as we decided a solution for the dynamic properties, it became apparent that we would no longer need such a function.

1.0 removed the overhead of magic dynamic properties and functions. In particular, we decided to approach color channel access in a new and different way.

Moving forward, the Color object is now iterable and indexable. Channels can be directly indexed via channel names or numerical indexes. You can even use slices:

>>> color = Color('orange')\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color['blue']\n0.0\n>>> color[0]\n1.0\n>>> color[:-1]\n[1.0, 0.6470588235294118, 0.0]\n

Color objects are also iterable, so you can just loop them as well, or cast the object as a list.

>>> for channel in Color('orange'):\n...     print(channel)\n... \n1.0\n0.6470588235294118\n0.0\n1.0\n>>> list(Color('green'))\n[0.0, 0.5019607843137255, 0.0, 1.0]\n

Setting channels is just as easy and can be done by indexing channels with names, numerical indexes, or even slices.

>>> color = Color('transparent')\n>>> color[:] = [1, 0, 0, 0.5]\n>>> color\ncolor(srgb 1 0 0 / 0.5)\n

As far as \u2206E methods are concerned, we already had two different ways to approach this, so we simply removed the dynamic functions. To access any of the different \u2206E methods, simply call the generic delta_e function and provide the method.

>>> Color('red').delta_e('green', method='2000')\n72.18053591241998\n
"},{"location":"about/releases/1.0/#gamut-mapping-and-clipping","title":"Gamut Mapping and Clipping","text":"

During our path to 1.0, we noticed that when performing gamut mapping and clipping, in most cases, we were performing them \"in place\" instead of the default which generated new Color instances. There are times when we occasionally wanted a new instance of the color when fitting a color to its gamut, but that turned out to not be the norm.

Generating new instances obviously will create more overhead, and in some cases, such as color mixing, returning a new color opposed to mutating the existing one makes a lot more sense, but with gamut mapping and clipping, for efficiency, we were often forcing \"in place\" operations.

1.0 now does gamut mapping and clipping in place by default. With this change, the in_place parameter is not longer available for fit() and clip().

So, if migrating to 1.0, if you were calling fit() and clip() directly, a few changes will need to be made. If you'd like to do an in place gamut correction, simply call the function. If you'd like to generate a new instance, clone the color first.

>>> color1 = Color('display-p3', [1, 1, 0])\n>>> color1.fit('srgb')\ncolor(display-p3 0.99859 0.9923 0.32854 / 1)\n>>> color1\ncolor(display-p3 0.99859 0.9923 0.32854 / 1)\n>>> color2 = Color('display-p3', [0, 1, 0])\n>>> color3 = color2.clone().fit('srgb')\n>>> color2, color3\n(color(display-p3 0 1 0 / 1), color(display-p3 0.45742 0.98328 0.29762 / 1))\n
"},{"location":"about/releases/1.0/#dictionary-output","title":"Dictionary Output","text":"

The dictionary format for input and output as been simplified for the 1.0 release. Prior to 1.0, the Color object used to export color dictionaries with the space name and each channel under an individually named key:

{'space': 'srgb', 'r': 1, 'g': 0, 'b': 0, 'alpha': 1}\n

This required more overhead, particularly when parsing to handle channel alias and the like. For 1.0, we've streamlined the format to export the data with all color coordinates under coords and the alpha channel still under alpha. This makes streamlines the process of handling dictionaries as inputs and outputting them when requested, in turn, improving performance.

>>> d = Color('rebeccapurple').to_dict()\n>>> d\n{'space': 'srgb', 'coords': [0.4, 0.2, 0.6], 'alpha': 1.0}\n>>> Color(d)\ncolor(srgb 0.4 0.2 0.6 / 1)\n
"},{"location":"about/releases/1.0/#interpolation","title":"Interpolation","text":"

Interpolation was an area we were generally unhappy with, so it was majorly overhauled.

Prior to 1.0, interpolation could be a bit awkward. Interpolation used to require the first color in the interpolation to be the calling object, and all the rest had to be fed in.

Color('red').interpolate(['blue', 'green', 'orange'])\n

When performing a simple mix, this felt natural and made sense:

Color('red').mix('blue', 0.25)\n

But with long chains of colors, this just felt cumbersome. To remedy this, we changed the interpolate and steps methods to @classmethods. We left mix as is since with two colors it feels natural.

So moving forward, interpolate and steps will execute interpolations from class methods.

>>> Color.interpolate(['red', 'blue', 'green', 'orange'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5c39c50>\n>>> Color.steps(['red', 'blue', 'green', 'orange'], steps=10)\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.56931 0.13909 -0.01995 / 1), color(--oklab 0.51066 0.05332 -0.16574 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.47459 -0.06841 -0.17179 / 1), color(--oklab 0.49717 -0.10435 -0.03206 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1), color(--oklab 0.61073 -0.07466 0.12558 / 1), color(--oklab 0.70171 -0.00903 0.14348 / 1), color(--oklab 0.79269 0.05661 0.16138 / 1)]\n

This means that you do not have to call the function from an instantiated object, and if you do, the instantiated color that is making the call will not be included in the interpolation. Only the colors in the list are considered during the interpolation.

>>> Color('white').interpolate(['red', 'blue', 'green', 'orange'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5cbba90>\n

This will make even more sense as we highlight the other changes.

Another problem we faced was the awkwardness of color stops and easing functions. Before we used to have a Piecewise object that you'd wrap a channel in to create color stops or inject easing functions and other various behaviors between colors, but it had to be applied on the second color in the chain, and this didn't quite work for the first color. If you wanted to add a stop to the first color, you then had to use a special stop parameter\u2026it was unintuitive.

from coloraide import Piecewise\nColor('red').interpolate(['blue', Piecewise('orange', 0.75, progress=lambda t: t * 3), 'purple'], stop=0.25)\n

In 1.0, we simplified things greatly. Since interpolate and steps now require that all colors must be in the input list if they are to be considered for interpolation, we can process them all in a consistent and more intuitive manner.

As before, steps and interpolate allow you to set function parameters to generally control the behavior for the entire interpolation across all colors. You can also still add easing functions via progress which will also affect the entire interpolation by default, but now you can inject easing functions directly between colors which will only be applied between those two colors.

>>> Color.interpolate(['red', lambda t: t * 3, 'orange', 'purple'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab6312cd0>\n

You can also directly wrap any color in the list with stop to change the color stop position. Since the first color is now treated like all the other colors, there is no need for the stop function parameter either.

>>> from coloraide import stop\n>>> Color.interpolate([stop('red', 0.25), stop('orange', 0.75), 'purple'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5da52d0>\n

And if you are familiar with CSS color hinting, which essentially alters the midpoint between two color stops, we've added a hint function which takes a new relative midpoint and returns a midpoint easing function which essentially acts the same as CSS interpolation hints.

>>> from coloraide import hint\n>>> Color.interpolate(['yellow', 'pink'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d17290>\n>>> Color.interpolate(['yellow', hint(0.25), 'pink'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d14510>\n

All of this makes for a less confusing experience when using interpolation. Additionally, all of the changes simplified the logic allowing us to even add a new interpolation method!

>>> Color.interpolate(['red', 'blue', 'green', 'orange'], method='bspline')\n<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x7fbab6d22e10>\n
"},{"location":"about/releases/1.0/#color-space-filters","title":"Color Space Filters","text":"

In the beginning, the Color space object was created with a naive filtering system. It added a little overhead, but the real issue was the fact that it only filtered inputs through new, match, and through normal instantiation. It did not filter through almost any other method that accepted inputs. It was decided to leave color filtering up to the user.

>>> c = Color('display-p3', [1, 1, 0])\n>>> try:\n...     if c.space() not in ['srgb', 'hsl', 'hwb']:\n...         raise ValueError('Invalid Color Space')\n... except ValueError as e:\n...     print(e)\n... \nInvalid Color Space\n
"},{"location":"api/","title":"Color API","text":""},{"location":"api/#nan","title":"coloraide.NaN","text":"Description NaN is a convenience constant for float('nan'). Import path

NaN is imported from the coloraide library:

from coloraide import NaN\n
"},{"location":"api/#stop","title":"coloraide.stop","text":"
class stop:\n    def __init__(\n        self,\n        color: ColorInput,\n        value: float\n    ) -> None:\n        ...\n
Description stop objects are used in interpolate methods. They allow a user to specify a color stop for a given color during the interpolation process. Import Path

stop is imported from coloraide library:

from coloraide import stop\n
Parameters 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."},{"location":"api/#hint","title":"coloraide.hint","text":"
def hint(\n    mid: float,\n) -> Callable[..., float]:\n
Description hint returns an easing function that adjust the midpoint between two color stops. Import Path

hint is imported from coloraide library:

from coloraide import hint\n
Parameters Parameters Defaults Description mid A numerical value, relative to the two color stops it occurs between. The value will be used as the new midpoint."},{"location":"api/#color","title":"coloraide.Color","text":"
class Color:\n    def __init__(\n        self,\n        color: ColorInput,\n        data: VectorLike | None = None,\n        alpha: float = util.DEF_ALPHA,\n        **kwargs: Any\n    ) -> None:\n        ...\n
Description The Color class object is a wrapper around the internal color space objects. Color is the base Color object and only registers a select number of color spaces by default. It provides an API interface to allow users to specify and manipulate colors. Import path

Color is imported from the coloraide library:

from coloraide import Color\n
Parameters Parameters Defaults Description color A color string, a dictionary describing the color, or another Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored."},{"location":"api/#colorall","title":"coloraide.everything.ColorAll","text":"
class ColorAll(Color):\n    def __init__(\n        self,\n        color: ColorInput,\n        data: VectorLike | None = None,\n        alpha: float = util.DEF_ALPHA,\n        **kwargs: Any\n    ) -> None:\n        ...\n
Description The ColorAll class object is derived from Color and extends the registered color spaces to include all offered by ColorAide. Import path

ColorAll is imported from the coloraide library:

from coloraide.everything import ColorAll\n
Parameters Parameters Defaults Description color A color string, a dictionary describing the color, or another ColorAll class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored."},{"location":"api/#register","title":"Color.register","text":"
def register(\n    cls,\n    plugin: Plugin | Sequence[Plugin],\n    *,\n    overwrite: bool = False,\n    silent: bool = False\n) -> None:\n    ...\n
Description Register a plugin(s). Parameters Parameters Defaults Description plugin A plugin instance or list of plugin instances to register. overwrite False overwrite will allow an already registered plugin to be overwritten if the plugin to register specifies a name that is already used for registration. silent False silent will avoid throwing an error if the name is already found and overwrite is set to False in the specified category."},{"location":"api/#deregister","title":"Color.deregister","text":"
@classmethod\ndef deregister(\n    cls,\n    plugin: str | Sequence[str], *,\n    silent: bool = False\n) -> None:\n    ...\n
Description Remove an already registered plugin(s). Parameters Parameters Defaults Description plugin A string or list of strings that describe the plugin(s) to be removed. Strings should be in the format category:name where category is either space, delta-e, cat, filter, contrast, interpolate, or fit and name is the name the plugin was registered under. * will remove all plugins and category:* will remove all within a specific category. silent False silent will avoid throwing an error if the name can not be found in the specified category."},{"location":"api/#match","title":"Color.match","text":"
@classmethod\ndef match(\n    cls,\n    string: str,\n    start: int = 0,\n    fullmatch: bool = False\n) -> ColorMatch | None:\n    ...\n
Description

The match class method provides access to the color matching interface and allows a user to provide a color string and get back a ColorMatch object. ColorMatch objects contain three properties:

class ColorMatch:\n    def __init__(\n        self,\n        color: Color,\n        start: int,\n        end: int\n    ) -> None:\n        ...\n
Parameter Description color The Color object. start The starting point within the string buffer where the color was found. end The ending point within the string buffer where the color was found.

Match does not search the entire buffer, but simply matches at the location specified by start.

Parameters Parameters Defaults Description string A string representing the color. start 0 Accepts an integer offset into the provided string buffer to start the match. fullmatch False A boolean which defines whether match must match to the end of the string buffer. Return Returns a ColorMatch object."},{"location":"api/#new","title":"Color.new","text":"
def new(\n    self,\n    color: ColorInput,\n    data: VectorLike | None = None,\n    alpha: float = util.DEF_ALPHA,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description The new class method exposes the interface of creating new color objects. Using new is the same as using Color(). Parameters Parameters Defaults Description color A color string, or other Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored. Return Returns a Color object."},{"location":"api/#random","title":"Color.random","text":"
@classmethod\ndef random(\n    cls,\n    space: str,\n    *,\n    limits: Sequence[Sequence[float] | None] | None = None\n) -> Color:\n    ...\n
Description Generate a random color in the provided space. The color space's channel range will be used as a limit for the channel. For color spaces with no clearly defined gamut, these values can be arbitrary. In such cases, it may be advisable to fit the returned color space to a displayable gamut. Parameters Parameters Defaults Description space The color space name in which to generate a random color in. limits None An optional list of constraints for various color channels. Each entry should either be a sequence contain a minimum and maximum value, or should be None. None values will be ignored and the color space's specified channel range will be used instead. Any missing entries will be treated as None. Return Returns a Color object."},{"location":"api/#clone","title":"Color.clone","text":"
def clone(\n    self\n):\n
Description The clone method provides a way to create a duplicate of the current Color instance. Return Returns a Color object."},{"location":"api/#update","title":"Color.update","text":"
def update(\n    self,\n    color: ColorInput,\n    data: VectorLike | None = None,\n    alpha: float = util.DEF_ALPHA,\n    *,\n    norm: bool = True,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description The update method provides a way to update the underlying color space with coordinates from any color space. The method's signature is the same as new except that it adds an additional norm parameter used to skip achromatic hue normalization when converting to the current color space. The object itself will assume the equivalent color in the current color space that matches the input color's value (assuming no algorithmic limitations preventing an equivalent color). Parameters Parameters Defaults Description color A color string, or other Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored. norm True When set to False, this prevents achromatic normalization when updating from a different color space. If no update occurs, nothing is done. Return Returns a reference to the current Color object."},{"location":"api/#mutate","title":"Color.mutate","text":"
def mutate(\n    self,\n    color: ColorInput,\n    data: VectorLike | None = None,\n    alpha: float = util.DEF_ALPHA,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description The mutate method is similar to update except that it does not convert the input color to the current color space, but instead replaces the current color space and values with the input color's color space and values. Parameters Parameters Defaults Description color A color string, or other Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored. Return Returns a reference to the current Color object."},{"location":"api/#convert","title":"Color.convert","text":"
def convert(\n    self,\n    space: str,\n    *,\n    fit: bool | str = False,\n    in_place: bool = False,\n    norm: bool = True\n) -> Color:\n    ...\n
Description Converts a Color object from one color space to another. If the current color space matches the specified color space, a clone of the current color will be returned with no changes to the channel values. If in_place is True, the current object will be modified in place. Parameters Parameters Defaults Description space A string representing the desired final color space. fit False Parameter specifying whether the current color should be gamut mapped into the final, desired color space. If set to True, the color will be gamut mapped using the default gamut mapping method. If set to a string, the string will be interpreted as the name of the gamut mapping method to be used. in_place False Boolean specifying whether the convert should alter the current Color object or return a new one. norm True When set to False, this prevents achromatic normalization when converting from a different color space. If no update occurs, nothing is done. Return Returns a reference to the converted Color object. If in_place is True, the return will be a reference to the current Color object."},{"location":"api/#space","title":"Color.space","text":"
def space(\n    self\n) -> str:\n    ...\n
Description Retrieves the current color space name as specified by the underlying color space object. Return Returns a string with the name of the current color space."},{"location":"api/#normalize","title":"Color.normalize","text":"
def normalize(\n    self,\n    *,\n    nans: bool = True\n) -> Color:\n    ...\n
Description Force normalization of a color's channels by cleaning up channels that setting hue to undefined if the color is achromatic. if nans is set to False, the hue normalization step (setting hue to undefined) will be skipped. Normalize modifies the current color in place. Parameters Parameters Defaults Description nans True Perform hue normalization (setting hue to undefined if the color is achromatic). Return Returns a reference to the current Color object after normalizing the channels for undefined hues."},{"location":"api/#to_dict","title":"Color.to_dict","text":"
def to_dict(\n    self,\n    *,\n    nans: bool = True\n) -> Mapping[str, Any]:\n    ...\n
Description

Dump the color object to a simple dictionary.

{\n    'space': 'srgb',            # Color space name\n    'coords': [1.0, 0.0, 0.0],  # Color channel values\n    'alpha': 1.0                # Alpha channel value\n}\n
Parameters Parameters Defaults Description nans True Return channel values having undefined values resolved as defined values. Return A dictionary containing the color space name and channel values."},{"location":"api/#to_string","title":"Color.to_string","text":"
def to_string(\n    self,\n    **kwargs: Any\n) -> str:\n    ...\n
Description Method that converts the current color to an output format supported by the color space. While a number of the parameters are common, some may be specific to the color space. The usage guide covers color space specific options in more details. Parameters

Common parameters:

Parameters Defaults Description alpha None Boolean or None value which determines whether the output includes alpha. If None, the default alpha will only be shown if less than 1. If True, alpha will always be shown. If False, alpha will be omitted. precision 5 Integer value that sets precision and scale. Precision and scale will match the value if greater than zero. If 0, values will be rounded to the nearest integer. If -1, number will be output at the highest precision. fit True A boolean that controls whether gamut mapping is performed on string creation. By default, colors will be fit to their own color space. This can be disabled by setting to False. color False A boolean that will determine if the color(space coord+ / alpha) format is used for string output. Has highest precedence. percent Varies A boolean that will output color channels as percents. Not all color spaces support percents, or may support percents only in certain scenarios. Default value may be determined by the color space.

sRGB specific parameters:

Parameters Defaults Description hex. False String output will be in #RRGGBBAA format. names False Boolean indicating a preference for CSS color names. When translating a color to it's closest hex form, if that hex value matches a CSS color name, that color name will be returned as the output. hex does not have to be True for this to apply. compress False If hex is True and compress is True, hex values will be compressed if possible: #RRGGBBAA \u2192 #RGBA.

Space dependent parameters:

Parameters Defaults Description comma False If supported by the color space and the current output format, commas will be used instead of space format: rgba(0, 0, 0, 1) \u2192 rgb(0 0 0 /1). Return Returns a string representation of the current color."},{"location":"api/#luminance","title":"Color.luminance","text":"
def luminance(\n    self,\n    *,\n    white: VectorLike | None = cat.WHITES['2deg']['D65']\n\n) -> float:\n    ...\n
Description Get the relative luminance. Relative luminance is obtained from the Y coordinate in the XYZ color space. XYZ, in this case, has a D65 white point. Parameters Parameters Defaults Description white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns an float indicating the relative luminance."},{"location":"api/#colorcontrast","title":"Color.contrast","text":"
def contrast(\n    self,\n    color: ColorInput,\n    method: str | None = None\n) -> float:\n    ...\n
Description Get the contrast ratio based on the relative luminance between two colors. Parameters Parameters Defaults Description color A color string or Color object representing a color. method None Specify the method used to obtain the contrast value. If None, the default specified by the class will be used. Return Returns a float indicating the contrast ratio between two colors."},{"location":"api/#distance","title":"Color.distance","text":"
def distance(\n    self,\n    color: ColorInput,\n    *,\n    space: str = \"lab\"\n) -> float:\n    ...\n
Description Performs a euclidean distance algorithm on two colors. Parameters Parameters Defaults Description color A color string or Color object representing a color. space \"lab\" Color space to perform distancing algorithm in. Return Returns a float indicating euclidean distance between the two colors."},{"location":"api/#delta_e","title":"Color.delta_e","text":"
def delta_e(\n    self,\n    color: ColorInput,\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> float:\n    ...\n
Description

Performs a delta E distance algorithm on two colors. Default algorithm that is used is Delta E 1976 (76). Some methods have additional weighting that can be configured through method specific options which are represented by **kwargs.

Available methods:

Name Input Parameters \u2206E*ab\u00a0(CIE76) 76 \u2206E*cmc\u00a0(CMC\u00a0l:c\u00a0(1984)) cmc l=2, c=1 \u2206E*94\u00a0(CIE94) 94 kl=1, k1=0.045, k2=0.015 \u2206E*00 \u00a0(CIEDE2000) 2000 kl=1, kc=1, kh=1 \u2206EHyAB\u00a0(HyAB) hyab space=\"lab\" \u2206Eok ok scalar=1 \u2206Eitp\u00a0(ICtCp) itp scalar=720 \u2206Ez\u00a0(Jzazbz) jz \u2206E99o\u00a0(DIN99o) 99o \u2206Ecam16 cam16 model=ucs \u2206EHCT hct Parameters Parameters Defaults Description color A color string or Color object representing a color. method None String that specifies the method to use. If None, the default will be used. **kwargs Any distancing specific parameters to pass to \u2206E method. Return Returns a float indicating the delta E distance between the two colors."},{"location":"api/#closest","title":"Color.closest","text":"
def closest(\n    self,\n    colors: Sequence[ColorInput],\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description Given a list of colors, calculates the closest color to the calling color object. Parameters Parameters Defaults Description colors A list of color strings, Color object, or dictionary representing a color. method None String that specifies the method of color distancing to use. **kwargs Any distancing specific parameters to pass to \u2206E method. Return The Color that is closest to the calling color object. In the off chance that an empty list is passed in None will be returned."},{"location":"api/#mask","title":"Color.mask","text":"
def mask(\n    self,\n    channel: str | Sequence[str],\n    *,\n    invert: bool = False,\n    in_place: bool = False\n) -> Color:\n    ...\n
Description The mask method will set any and all specified channels to NaN. If invert is set to True, mask will set any and all channels not specified to NaN. Parameters Parameters Defaults Description channel A string specifying a channel, or a list of strings specifying multiple channels. Specified channels will be masked (or the only channels not masked if invert is True). invert False Use inverse masking logic and mask all channels that are not specified. in_place False Boolean used to determine if the current color should be modified \"in place\" or a new Color object should be returned. Return Returns a reference to the masked Color object. If in_place is True, the return will be a reference to the current Color object."},{"location":"api/#interpolate","title":"Color.interpolate","text":"
@classmethod\ndef interpolate(\n    cls,\n    colors: Sequence[ColorInput | interpolate.stop | Callable[..., float]],\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    progress: Mapping[str, Callable[..., float]] | Callable[..., float] | None = None,\n    hue: str = util.DEF_HUE_ADJ,\n    premultiplied: bool = True,\n    extrapolate: bool = False,\n    domain: list[float] | None = None,\n    method: str = \"linear\",\n    padding: float | tuple[float, float] | None = None,\n    carryforward: bool | None = False,\n    powerless: bool | None = False,\n    **kwargs: Any\n) -> Interpolator:\n    ...\n
Description

The interpolate method creates a function that takes a value between 0 - 1 and interpolates a new color based on the input value.

If more than one color is provided, the returned function will span the interpolations between all the provided colors with the same range of 0 - 1.

Interpolation can be customized by limiting the interpolation to specific color channels, providing custom interpolation functions, and even adjusting the hue logic used.

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\u00a0Evaluation Description shorter Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 [-180, 180]. longer Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 {[-360, -180], [180, 360]}. increasing Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 [0, 360]. decreasing Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 [-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. bspline An interpolation method that employs cubic B-spline curves to calculate an interpolation path through multiple colors. natural A natural interpolation spline based on the cubic B-spline curve. monotone An interpolation method that utilizes a monotonic cubic spline based on the Hermite spline. catrom Interpolation based on the Catmull-Rom cubic spline. Parameters Parameters Defaults Description colors A list of color strings, Color objects, dictionaries representing a color, stop objects, or easing functions. space \"lab\" Color space to interpolate in. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space. progress None An optional function that that allows for custom logic to perform non-linear interpolation. hue \"shorter\" Define how color spaces which have hue angles are interpolated. Default evaluates between the shortest angle. premultiplied True Use premultiplied alpha when interpolating. extrapolate False Interpolations should extrapolate when values exceed the domain range ([0, 1] by default). domain None A list of numbers defining the domain range of the interpolation. method \"linear\" The interpolation method to use. padding None Adjust the padding of the interpolation range. carryforward False Carry forward undefined channels when converting to the interpolation space. If None, will use the class default which is False by default. powerless None Treat explicitly defined hues as powerless when the color is considered achromatic. If None, will use the class default which is False by default. Return Returns a function that takes a range within the specified domain, the default being [0..1]. The function returns a new, interpolated Color object."},{"location":"api/#steps","title":"Color.steps","text":"
@classmethod\ndef steps(\n    cls,\n    colors: Sequence[ColorInput | interpolate.stop | Callable[..., float]],\n    *,\n    steps: int = 2,\n    max_steps: int = 1000,\n    max_delta_e: float = 0,\n    delta_e: str | None = None,\n    delta_e_args: dict[str, Any] | None = None,\n    **interpolate_args: Any\n) -> list[Color]:\n    ...\n
Description

Creates an interpolate function and iterates through it with user defined step parameters to produce discrete color steps. Will attempt to provide the minimum number of steps without exceeding max_steps. If max_delta_e is provided, the distance between each stop will be cut in half until there are no colors with a distance greater than the specified max_delta_e. The default \u2206E method is used by default, but it can be changed with the delta_e parameter.

If more than one color is provided, the steps will be returned from the interpolations between all the provided colors.

Like interpolate, the default interpolation space is lab.

Parameters Parameters Defaults Description color A list of color strings, Color objects, dictionaries representing a color, stop objects, or easing functions. steps 2 Minimum number of steps. max_steps 1000 Maximum number of steps. max_delta_e 0 Maximum delta E distance between the color stops. A value of 0 or less will be ignored. delta_e None A string indicating which \u2206E method to use. If nothing is supplied, the class object's current default \u2206E method will be used. delta_e_args None A dictionary containing keyword arguments to be passed to the delta_e method. **interpolate_args See\u00a0interpolate Keyword arguments defined in interpolate. Return List of Color objects."},{"location":"api/#discrete","title":"Color.discrete","text":"
@classmethod\ndef discrete(\n    cls,\n    colors: Sequence[ColorInput | interpolate.stop | Callable[..., float]],\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    steps: int | None = None,\n    max_steps: int = 1000,\n    max_delta_e: float = 0,\n    delta_e: str | None = None,\n    delta_e_args: dict[str, Any] | None = None,\n    domain: list[float] | None = None,\n    **interpolate_args: Any\n) -> Interpolator:\n    ...\n
Description

Generates an interpolate function with discrete color scale. By default it assumes as many discrete colors as the user inputs, but steps can be used to generate more or less using the input colors. As discrete is built on steps, it takes all the same arguments.

Like interpolate, the default interpolation space is lab.

Parameters Parameters Defaults Description color A list of color strings, Color objects, dictionaries representing a color, stop objects, or easing functions. space \"lab\" Color space to interpolate in. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space. steps None Minimum number of steps. If None, steps will be set to the number of input colors. max_steps 1000 Maximum number of steps. max_delta_e 0 Maximum delta E distance between the color stops. A value of 0 or less will be ignored. delta_e None A string indicating which \u2206E method to use. If nothing is supplied, the class object's current default \u2206E method will be used. delta_e_args None A dictionary containing keyword arguments to be passed to the delta_e method. domain None A list of numbers defining the domain range of the interpolation. **interpolate_args See\u00a0interpolate Keyword arguments defined in interpolate. Return Returns a function that takes a range within the specified domain, the default being [0..1]. The function returns a new, interpolated Color object."},{"location":"api/#mix","title":"Color.mix","text":"
def mix(\n    self,\n    color: ColorInput,\n    percent: float = util.DEF_MIX,\n    *,\n    in_place: bool = False,\n    **interpolate_args: Any\n) -> Color:\n    ...\n
Description Interpolates between two colors returning a color that represents the mixing of the base color and the provided color mixed at the provided percent, where percent applies to how much the provided color contributes to the the final result. Parameters Parameters Defaults Description color A color string, Color object, and/or dictionary representing a color. percent 0.5 A numerical value between 0 - 1 representing the percentage at which the parameter color will be mixed. in_place False Boolean used to determine if the the current color should be modified \"in place\" or a new Color object should be returned. **interpolate_args See\u00a0interpolate Keyword arguments defined in interpolate. Return Returns a reference to the new Color object or a reference to the current Color if in_place is True."},{"location":"api/#average","title":"Color.average","text":"
@classmethod\ndef average(\n    cls,\n    colors: Iterable[ColorInput],\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    premultiplied: bool = True,\n    powerelss: bool | None = None\n    **kwargs: Any\n) -> Color:\n
Description Get the average mean of all channels given a particular set of input colors. Parameters Parameters Defaults Description colors An iterable of color strings, Color objects, and/or dictionaries representing a color. space None An optional string to specify what color space the colors should be averaged in. If none is provided, Oklab is assumed. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified via space. premultiplied True Specify whether colors should be premultiplied during the averaging process. powerless None Treat explicitly defined hues as powerless when the color is considered achromatic. If None, will use the class default which is False by default. Return Returns a reference to the new Color object representing the average of the input colors."},{"location":"api/#cvd","title":"Color.filter","text":"
def filter(\n    self,\n    name: str,\n    amount: float | None = None,\n    *,\n    space: str | None = None,\n    in_place: bool = False,\n    out_space: str | None = None,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description

Apply a color filter to alter a given color. The non-CVD filters are based on the W3C Filter Effects and behave in the same manner. Colors are evaluated in the sRGB Linear color space unless otherwise specified via the space parameter. No other color space will be accepted except sRGB and sRGB Linear.

An amount can be provided to adjust how much the color is filtered. Any clamping that occurs with the amount parameter, and related ways in which amount are applied, follow the W3C Filter Effects spec.

Some filters, such as CVDs, may take additional arguments via kwargs.

Filters Name Default Brightness brightness 1 Saturation saturate 1 Contrast contrast 1 Opacity opacity 1 Invert invert 1 Hue\u00a0rotation hue-rotate 0 Sepia sepia 1 Grayscale grayscale 1 CVD\u00a0Filters Name Default Protanopia\u00a0CVD protan 1 Deuteranopia\u00a0CVD deutan 1 Tritanopia\u00a0CVD tritan 1 Parameters Parameters Defaults Description name The name of the filter that should be applied. amount See\u00a0above A numerical value adjusting to what degree the filter is applied. Input range can vary depending on the filter being used. Default can also dependent on the filter being used. space None Controls the algorithm used for simulating the given CVD. in_place False Boolean used to determine if the the current color should be modified \"in place\" or a new Color object should be returned. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified via space. **kwargs Additional filter specific parameters.

CVDs also take an optional method parameter that allows for specifying the CVD algorithm to use.

Simulation\u00a0Approach Name Brettel 1997 brettel Vi\u00e9not, Brettel, and Mollon 1999 vienot Machado 2009 machado Return Returns a reference to the new Color object or a reference to the current Color if in_place is True."},{"location":"api/#harmony","title":"Color.harmony","text":"
def harmony(\n    self,\n    name: str,\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    **kwargs: Any\n) -> list[Color]:\n    ...\n
Description

The harmony method uses the current color and returns a set of harmonious colors (including the current color). The color harmonies are based on the classical color harmonies of color theory. By default, harmonious colors are selected under the perceptually uniform OkLCh color space, but other cylindrical color spaces can be used.

Harmony Name Monochromatic mono Complementary complement Split\u00a0Complement split Analogous analogous Triadic triad Tetradic\u00a0Square square Tetradic\u00a0Rectangle rectangle Wheel wheel Parameters Parameters Defaults Description name Name of the color harmony to use. space 'oklch' Color space under which the harmonies will be calculated. Must be a cylindrical space. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified via space. **kwargs Any harmony specific parameters to pass to the called harmony. Return Returns a list of Color objects."},{"location":"api/#compose","title":"Color.compose","text":"
def compose(\n    self,\n    backdrop: ColorInput | Sequence[ColorInput],\n    *,\n    blend: str | bool = True,\n    operator: str | bool = True,\n    space: str | None = None,\n    out_space: str | None = None,\n    in_place: bool = False\n) -> Color:\n    ...\n
Description

Apply compositing which consists of a blend mode and a Porter Duff operator for alpha compositing. The current color is treated as the source (top layer) and the provided color as the backdrop (bottom layer). Colors will be composited in the srgb color space unless otherwise specified.

Colors should generally be RGB-ish colors (sRGB, Display P3, A98 RGB, etc.). The algorithm is designed only for RGB-ish colors. Non-RGB-ish colors are likely to provide nonsense results.

Supported blend modes are:

Blend Modes normal multiply darken lighten burn dodge screen overlay hard-light exclusion difference soft-light hue saturation luminosity color color hue saturation luminosity

Supported Port Duff operators are:

Operators clear copy destination source-over destination-over source-in destination-in source-out destination-out source-atop destination-atop xor lighter Parameters Parameters Defaults Description backdrop A background color represented with either a string or Color object. blend None A blend mode to use to use when compositing. Values should be a string specifying the name of the blend mode to use. If None, normal will be used. If False, blending will be skipped. operator None A Porter Duff operator to use for alpha compositing. Values should be a string specifying the name of the operator to use. If None, source-over will be used. If False, alpha compositing will be skipped. space None A color space to perform the overlay in. If None, the base color's space will be used. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space. in_place False Boolean used to determine if the the current color should be modified \"in place\" or a new Color object should be returned. Return Returns a reference to the new Color object or a reference to the current Color if in_place is True."},{"location":"api/#clip","title":"Color.clip","text":"
def clip(\n    self,\n    space: str | None = None\n) -> Color:\n
Description Performs simple clipping on color channels that are out of gamut. Parameters Parameters Defaults Description space None The color space that the color must be mapped to. If space is None, then the current color space will be used. Return Returns a reference to the current Color after fitting its coordinates to the specified gamut."},{"location":"api/#fit","title":"Color.fit","text":"
def fit(\n    self,\n    space: str | None = None,\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description

Fits color to the current or specified color gamut.

By default, lch-chroma gamut mapping is used. This is essentially an approach that holds lightness and hue constant in the CIELCh color space while reducing chroma until the color is in gamut. Clipping is done at each step of the way and the color distance measured to see how close our color is to the intended color.

The supported gamut mapping methods are:

Name Input Clipping clip OkLCh Chroma oklch-chroma LCh Chroma lch-chroma Parameters

Some methods could have additional parameters to configure the behavior, these would be done through **kwargs. None of built-in gamut mapping methods currently have additional parameters.

Parameters Defaults Description space None The color space that the color must be mapped to. If space is None, then the current color space will be used. method None String that specifies which gamut mapping method to use. If None, lch-chroma will be used. Return Returns a reference to the current Color after fitting its coordinates to the specified gamut."},{"location":"api/#in_gamut","title":"Color.in_gamut","text":"
def in_gamut(\n    self, space: str | None = None,\n    *,\n    tolerance: float = util.DEF_FIT_TOLERANCE\n) -> bool:\n
Description Checks if the current color is in the current or specified gamut. Parameters Parameters Defaults Description space None The color space that the color must be fit within. If space is None, then the current color space will be used. tolerance 0.000075 Tolerance allowed when checking bounds of color. Return Returns a boolean indicating whether the color is in the specified gamut."},{"location":"api/#get","title":"Color.get","text":"
def get(\n    self,\n    name: str | list[str] | tuple[str, ...],\n    *,\n    nans: bool = True\n) -> float | list[float]:\n
Description Retrieves the coordinate value from the specified channel or values from a sequence of specified channels. Channels must be a channel name in the current color space or a channel name in the specified color space using the syntax: space.channel. Parameters Parameters Defaults Description name Channel name or sequence of channel names. Channel names can define the color space and channel name to retrieve value from a different color space. nans True Determines whether an undefined value is allowed to be returned. If disabled, undefined values will be resolved before returning. Return

Returns a numerical value that is stored internally for the specified channel, or a calculated value in the case that a channel in a different color space is requested. If more than one value is requested, the a list of numerical values will be returned.

"},{"location":"api/#set","title":"Color.set","text":"
def set(\n    self,\n    name: str | dict[str, float | Callable[..., float]],\n    value: float | Callable[..., float] | None = None,\n    *,\n    nans: bool = True\n) -> Color:\n
Description

Sets the given value to the specified channel. If the name is provided in the form space.channel, the value will be applied to the channel of the specified color space while keeping current color space the same.

The value can be a numerical value or a function that accepts a numerical channel value and returns a numerical channel value.

name can also be a dictionary of channels, each with a value. In this case, the value parameter of the function can be ignored.

This function returns the current colors reference so that multiple sets can be chained together.

Parameters Parameters Defaults Description name A string containing a channel name or color space and channel separated by a . specifying the what channel to set. If value is omitted, name can also be a dictionary containing multiple channels, each specifying their own value to set. value A numerical value, a string value accepted by the specified color space, or a function. nans True When doing relative sets via a callback input, ensure the channel value passed to the callback is a real number, not an undefined value. Return Returns a reference to the current Color object."},{"location":"api/#coords","title":"Color.coords","text":"
def coords(\n    self,\n    *,\n    nans: bool = True\n) -> Vector:\n    ...\n
Description Get the color channels (no alpha). If nans is set to False, all undefined values will be returned as defined. Parameters Parameters Defaults Description nans True If nans is set to False, all undefined values will be returned as defined. Return Returns a list of float values, one for each color channel."},{"location":"api/#coords","title":"Color.alpha","text":"
def alpha(\n    self,\n    *,\n    nans: bool = True\n) -> float:\n    ...\n
Description Get the alpha channel's value. If nans is set to False, an undefined value will be returned as defined. Parameters Parameters Defaults Description nans True If nans is set to False, an undefined value will be returned as defined. Return Returns a float."},{"location":"api/#is_achromatic","title":"Color.is_achromatic","text":"
def is_achromatic(\n    self\n) -> bool:\n    ...\n
Description Can be called on any color to determine if the color is achromatic. If a color is achromatic, or very close to achromatic, it will return True. Return Returns a boolean indicating whether the color is achromatic."},{"location":"api/#is_nan","title":"Color.is_nan","text":"
def is_nan(\n    self,\n    name: str\n) -> bool:\n    ...\n
Description Retrieves the coordinate value from the specified channel and checks whether the value is undefined (set to NaN). Channel must be a channel name in the current color space or a channel name in the specified color space using the syntax: space.channel. Parameters Parameters Defaults Description name A string indicating what channel property to check. Return Returns a boolean indicating whether the specified color space's channel is NaN."},{"location":"api/#white","title":"Color.white","text":"
def white(\n    self\n) -> Vector:\n    ...\n
Description Retrieves the white point for the current color's color space. Return Returns a set of XYZ coordinates that align with the white point for the given color space."},{"location":"api/#blackbocy","title":"Color.blackbody","text":"
@classmethod\ndef blackbody(\n    cls,\n    temp: float,\n    duv: float = 0.0,\n    *,\n    scale: bool = True,\n    scale_space: str | None = None,\n    out_space: str | None = None,\n    method: str | None = None,\n    **kwargs: Any\n) -> Color:\n
Description Creates a color from a temperature in Kelvin and an optional \u2206uv. The color will be scaled within the linear RGB space specified by scale_space and can be disabled by setting scale to False. By default, the Ohno 2013 algorithm is used and can be configured via method. Parameters Parameters Defaults Description temp A positive temperature in Kelvin. Accepted range of temperature is based on the algorithm. duv 0.0 An optional \u2206uv specifying the distance from the black body curve. scale True Scale the color with a linear RGB color space as defined by scale_space. scale_space 'srgb-linear' If scale is enabled, scale_space defines the RGB color space in which the returned color should be scaled within. The color space should be a linear space for best results. If undefined, srgb-linear will be used. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space or xyz-d65 if space is None. method None A string specifying the algorithm to use. By default robertson-1968 is used. **kwargs Any plugin specific parameters to pass to the blackbody method. Return Returns a reference to the current Color."},{"location":"api/#cct","title":"Color.cct","text":"
def cct(\n    self,\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> Vector:\n
Description Returns the associated CCT and \u2206uv for a given color. If the color is beyond an acceptable range for the algorithm or the color is very far from the locus, the result may be surprising. Parameters Parameters Defaults Description method None A string specifying the algorithm to use. By default robertson-1968 is used. **kwargs Any plugin specific parameters to pass to the blackbody method. Return Returns a list containing the correlated color temperature in Kelvin and the \u2206uv."},{"location":"api/#colorchromatic_adaptation","title":"Color.chromatic_adaptation","text":"
@classmethod\ndef chromatic_adaptation(\n    cls,\n    w1: tuple[float, float],\n    w2: tuple[float, float],\n    xyz: VectorLike,\n    *,\n    method: str | None = None\n) -> Vector:\n    ...\n
Description A class method that converts an XYZ set of coordinates between two given white points. The first white point must match the white point of the current coordinates and the second white point must be the desired white point to use. method dictates the method of chromatic adaptation to use. Parameters Parameters Defaults Description w1 Current white point of the XYZ coordinates. w2 Desired white point of the XYZ coordinates. xyz The XYZ coordinates to adapt. method None The method of chromatic adaptation to use. If not specified, the current class's default method will be used. Return Returns a set of XYZ coordinates that have been chromatically adapted to the desired white point."},{"location":"api/#xy","title":"Color.xy","text":"
def xy(\n    self,\n    *,\n    white: VectorLike | None = None\n) -> Vector:\n
Description Retrieves the CIE 1931 (x, y) chromaticity coordinates for a given color. Parameters Parameters Defaults Description white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns a tuple of CIE 1931 (x, y) chromaticity points for the given color. The XYZ translation to xy will use the current color's white point to ensure the values are relative to the proper white point."},{"location":"api/#xy","title":"Color.uv","text":"
def uv(\n    self,\n    mode: str = '1976',\n    *,\n    white: VectorLike | None = None\n) -> Vector:\n    ...\n
Description Retrieves the UCS 1960 (u, v) chromaticity coordinates for a given color or the CIE 1976 UCS (u', v') chromaticity coordinates, the latter being the default. Parameters Parameters Defaults Description mode '1976' A string indicating what mode to use. 1976 refers to the (u', v') points as described by CIE 1976 UCS and 1960 describes the (u, v) points as documented by CIE 1960 UCS. white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns a tuple of (u, v) \u2013 either 1976 (u', v') or 1960 (u, v) \u2013 chromaticity points for the given color. The XYZ translation to uv will use the current color's white point to ensure the values are relative to the proper white point."},{"location":"api/#get_chromaticity","title":"Color.get_chromaticity","text":"
def get_chromaticity(\n    self,\n    cspace: str = 'uv-1976',\n    *,\n    white: VectorLike | None = None\n) -> Vector:\n    ...\n
Description Retrieves the 1931 xy, 1960 uv, or 1976 u'v' chromaticity coordinates with the luminance (Y). Coordinates are returned in the format specified by cspace and will use the white point of the current color. Parameters Parameters Defaults Description cspace 'uv-1976' A string indicating what chromaticity space to use. uv-1976 being the default. white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns a list of chromaticity coordinates. Results will either be in [x, y, Y] for 1931 xy, [u, v, Y] for 1960 uv, or [u', v', Y] for 1976 u'v'."},{"location":"api/#get_chromaticity","title":"Color.chromaticity","text":"
@classmethod\ndef chromaticity(\n    cls,\n    space: str,\n    coords: VectorLike,\n    cspace: str = 'uv-1976',\n    *,\n    scale: bool = False,\n    scale_space: str | None = None,\n    white: VectorLike | None = None\n) -> Color:\n    ...\n
Description Returns a color that satisfies the provided chromaticity coordinates. Coordinates can be in the form 1931 xyY, 1960 uvY, or 1976 u'v'Y and can be configured via cspace. The target space to convert to should be specified via space. Chromaticity coordinates should math the white space of the targeted space, but if they are not the white point of the chromaticity coordinates can be specified with white. Parameters Parameters Defaults Description space Color space to chromaticities to. coords The chromaticity coordinates. Values can be in either 3D form (with luminance Y). cspace 'uv-1976' A string indicating what chromaticity space to use. uv-1976 being the default. white None Specify the white in which to chromatically adapt the points from, if none is specified, the targeted color's white point is assumed. scale True Scale the color with a linear RGB color space as defined by scale_space. scale_space None If scale is enabled, scale_space defines the RGB color space in which the returned color should be scaled within. The color space should be a linear space for best results. If undefined, srgb-linear will be used. Return Returns a reference to a new Color object that satisfies the chromaticity coordinates."},{"location":"api/#convert_chromaticity","title":"Color.convert_chromaticity","text":"
@classmethod\ndef convert_chromaticity(\n    cls,\n    cspace1: str,\n    cspace2: str,\n    coords: VectorLike\n) -> Vector:\n    ...\n

Description

  • Converts a 2D chromaticity pair from one chromaticity space to another. Supported spaces are xy-1931, uv-1960, and uv-1976.

Parameters

  • Parameters Defaults Description cspace1 Initial chromaticity space for the given coordinates. cspace2 Target chromaticity space for the given coordinates. coords The 2D chromaticity coordinates to convert.

Return

  • Returns the converted 2D chromaticity coordinates.
"},{"location":"colors/","title":"Color Spaces","text":"

ColorAide aims to support all the color spaces and models currently offered in modern CSS, such as sRGB, Display P3, CIELab, Oklab, etc. We also include a number of color spaces that are not available in CSS.

ColorAide registers a subset of the offered color spaces by default. But additional color spaces can be registered by subclassing the Color object and then registering any additional required plugins, such as color spaces.

Everything but the Kitchen Sink

It is not generally recommended to register all possible color spaces (and plugins in general). The suggested approach is to cherry pick additional color spaces as needed by simply subclassing Color and then registering the desired plugins, but if desired coloraide.everything.ColorAll already includes all plugins and can be imported to get access to every supported plugin.

"},{"location":"colors/#default-color-spaces","title":"Default Color Spaces","text":"

While ColorAide supports a lot of color spaces, it is rare that a user would ever need every color space implemented by ColorAide available at all times, so to keep the Color object lighter, and color matching logic quicker, the coloraide.Color object does not register all color spaces by default.

Default Color\u00a0Spaces XYZ\u00a0D65 XYZ\u00a0D50 Linear sRGB Linear Display\u00a0P3 Linear A98\u00a0RGB Linear Rec.\u00a02020 Linear ProPhoto\u00a0RGB sRGB Display\u00a0P3 A98\u00a0RGB Rec.\u00a02020 ProPhoto\u00a0RGB HSL HSV HWB Lab LCh Lab\u00a0D65 LCh\u00a0D65 Oklab OkLCh"},{"location":"colors/#color-space-map","title":"Color Space Map","text":"

When registering a plugin, it is important that all required plugins in the conversion path are registered as well. Below we've provided a diagram of all available color spaces and how they translate to one another.

Click any of the color spaces to jump to the related documentation.

flowchart TB\n\n    acescc --- acescg ---- xyz-d65\n        acescct --- acescg\n\n    aces2065-1 --- xyz-d65\n\n    oklch --- oklab ----- xyz-d65\n        okhsl --- oklab\n        okhsv --- oklab\n\n    display-p3 --- display-p3-linear --- xyz-d65\n\n    a98-rgb --- a98-rgb-linear --- xyz-d65\n\n    srgb-linear --- xyz-d65\n        rec709 --- srgb-linear\n        srgb --- srgb-linear\n            cubehelix --- srgb\n            ryb --- srgb\n            orgb --- srgb\n            prismatic --- srgb\n            hsi --- srgb\n            cmy --- srgb\n            cmyk --- srgb\n            xyb --- srgb\n            hsl --- srgb\n                hsv --- hsl\n            hwb --- srgb\n\n    rec2020-linear --- xyz-d65\n        rec2020 --- rec2020-linear\n        rec2100-pq --- rec2020-linear\n        rec2100-hlg --- rec2020-linear\n\n    prophoto-rgb --- prophoto-rgb-linear --- xyz-d50 ----- xyz-d65\n\n    lch --- lab --- xyz-d50\n\n    xyz-d65 --- lab-d65 --- lch-d65\n\n    xyz-d65 --- cam16-jmh --- cam16\n        cam16 --- cam16-ucs\n        cam16 --- cam16-scd\n        cam16 --- cam16-lcd\n\n    xyz-d65 --- hct\n\n    xyz-d65 --- jzazbz --- jzczhz\n\n    xyz-d65 --- ipt\n\n    xyz-d65 --- ictcp\n\n    xyz-d65 --- igpgtg\n\n    xyz-d65 --- din99o --- lch99o\n\n    xyz-d65 --- hunter-lab\n\n    xyz-d65 --- rlab\n\n    xyz-d65 --- luv --- lchuv\n        luv --- hsluv\n        luv --- hpluv\n\n    xyz-d65 --- xyy\n\n    xyz-d65 --- ucs\n\n    xyz-d65(XYZ D65)\n    xyz-d50(XYZ D50)\n    rec2020(Rec. 2020)\n    rec2020-linear(Linear Rec. 2020)\n    rec2100-pq(Rec. 2100 PQ)\n    rec2100-hlg(Rec. 2100 HLG)\n    srgb-linear(Linear sRGB)\n    srgb(sRGB)\n    rec709(Rec. 709)\n    hsl(HSL)\n    hsv(HSV)\n    hwb(HWB)\n    display-p3-linear(Linear Display P3)\n    display-p3(Display P3)\n    a98-rgb-linear(Linear A98 RGB)\n    a98-rgb(A98 RGB)\n    prophoto-rgb-linear(Linear ProPhoto RGB)\n    prophoto-rgb(ProPhoto RGB)\n    lab(Lab)\n    lch(LCh)\n    lab-d65(Lab D65)\n    lch-d65(LCh D65)\n    oklab(Oklab)\n    oklch(OkLCh)\n    okhsl(Okhsl)\n    okhsv(Okhsv)\n    luv(Luv)\n    lchuv(LChuv)\n    hsluv(HSLuv)\n    hpluv(HPLuv)\n    din99o(DIN99o)\n    lch99o(DIN99o LCh)\n    jzazbz(Jzazbz)\n    jzczhz(JzCzhz)\n    ictcp(ICtCp)\n    orgb(oRGB)\n    ipt(IPT)\n    igpgtg(IgPgTg)\n    hunter-lab(Hunter Lab)\n    rlab(RLAB)\n    hsi(HSI)\n    cmy(CMY)\n    cmyk(CMYK)\n    xyy(xyY)\n    ucs(CIE 1960 UCS)\n    prismatic(Prismatic)\n    aces2065-1(ACES2065-1)\n    acescg(ACEScg)\n    acescc(ACEScc)\n    acescct(ACEScct)\n    cam16(CAM16)\n    cam16-jmh(CAM16 JMh)\n    cam16-ucs(CAM16 UCS)\n    cam16-scd(CAM16 SCD)\n    cam16-lcd(CAM16 LCD)\n    hct(HCT)\n    xyb(XYB)\n    ryb(RYB)\n    cubehelix(Cubehelix)\n\n    click xyz-d65 \"./xyz_d65/\" _self\n    click xyz-d50 \"./xyz_d50/\" _self\n    click rec2020 \"./rec2020/\" _self\n    click rec2020-linear \"./rec2020_linear/\" _self\n    click rec2100-pq \"./rec2100_pq/\" _self\n    click rec2100-hlg \"./rec2100_hlg/\" _self\n    click srgb-linear \"./srgb_linear/\" _self\n    click srgb \"./srgb/\" _self\n    click rec709 \"./rec709/\" _self\n    click hsl \"./hsl/\" _self\n    click hsv \"./hsv/\" _self\n    click hwb \"./hwb/\" _self\n    click display-p3-linear \"./display_p3_linear/\" _self\n    click display-p3 \"./display_p3/\" _self\n    click a98-rgb-linear \"./a98_rgb_linear/\" _self\n    click a98-rgb \"./a98_rgb/\" _self\n    click prophoto-rgb-linear \"./prophoto_rgb_linear/\" _self\n    click prophoto-rgb \"./prophoto_rgb/\" _self\n    click lab \"./lab/\" _self\n    click lch \"./lch/\" _self\n    click lab-d65 \"./lab_d65/\" _self\n    click lch-d65 \"./lch_d65/\" _self\n    click oklab \"./oklab/\" _self\n    click oklch \"./oklch/\" _self\n    click okhsl \"./okhsl/\" _self\n    click okhsv \"./okhsv/\" _self\n    click luv \"./luv/\" _self\n    click lchuv \"./lchuv/\" _self\n    click hsluv \"./hsluv/\" _self\n    click hpluv \"./hpluv/\" _self\n    click din99o \"./din99o/\" _self\n    click lch99o \"./lch99o/\" _self\n    click jzazbz \"./jzazbz/\" _self\n    click jzczhz \"./jzczhz/\" _self\n    click ictcp \"./ictcp/\" _self\n    click orgb \"./orgb/\" _self\n    click ipt \"./ipt/\" _self\n    click igpgtg \"./igpgtg/\" _self\n    click hunter-lab \"./hunter_lab/\" _self\n    click rlab \"./rlab/\" _self\n    click hsi \"./hsi/\" _self\n    click cmy \"./cmy/\" _self\n    click cmyk \"./cmyk/\" _self\n    click xyy \"./xyy/\" _self\n    click ucs \"./ucs/\" _self\n    click prismatic \"./prismatic/\" _self\n    click aces2065-1 \"./aces2065_1/\" _self\n    click acescg \"./acescg/\" _self\n    click acescc \"./acescc/\" _self\n    click acescct \"./acescct/\" _self\n    click cam16 \"./cam16/\" _self\n    click cam16-jmh \"./cam16_jmh/\" _self\n    click cam16-ucs \"./cam16_ucs/\" _self\n    click cam16-scd \"./cam16_scd/\" _self\n    click cam16-lcd \"./cam16_lcd/\" _self\n    click hct \"./hct/\" _self\n    click xyb \"./xyb/\" _self\n    click ryb \"./ryb/\" _self\n    click cubehelix \"./cubehelix/\" _self
"},{"location":"colors/a98_rgb/","title":"A98 RGB","text":"

The A98 RGB color space is registered in Color by default

Properties

Name: a98-rgb

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Adobe\u00ae RGB 1998 Chromaticities

The Adobe\u00ae RGB (1998) color space or opRGB is a color space developed by Adobe Systems\u00ae, Inc. in 1998. It was designed to encompass most of the colors achievable on CMYK color printers, but by using RGB primary colors on a device such as a computer display. The Adobe\u00ae RGB (1998) color space encompasses roughly 50% of the visible colors specified by the CIELab color space - improving upon the gamut of the sRGB color space, primarily in cyan-green hues.

A98 RGB is an Adobe\u00ae 98 Compatible color space.

Learn about A98 RGB

"},{"location":"colors/a98_rgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/a98_rgb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(a98-rgb r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"a98-rgb\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(a98-rgb r g b / a) form.

>>> Color('a98-rgb', [0.85859, 0, 0])\ncolor(a98-rgb 0.85859 0 0 / 1)\n>>> Color('a98-rgb', [0.91489, 0.64117, 0.15031]).to_string()\n'color(a98-rgb 0.91489 0.64117 0.15031)'\n
"},{"location":"colors/a98_rgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.a98_rgb import A98RGB\n\nclass Color(Base): ...\n\nColor.register(A98RGB())\n
"},{"location":"colors/a98_rgb_linear/","title":"Linear A98 RGB","text":"

The Linear A98 RGB color space is registered in Color by default

Properties

Name: a98-rgb-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Adobe\u00ae RGB 1998 Chromaticities

The Linear A98 RGB space is the same as A98 RGB except that the transfer function is linear-light (there is no gamma-encoding).

Learn about A98 RGB

"},{"location":"colors/a98_rgb_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/a98_rgb_linear/#inputoutput","title":"Input/Output","text":"

Linear A98 RGB is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --a98-rgb-linear:

color(--a98-rgb-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"a98-rgb-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--a98-rgb-linear r g b / a) form.

>>> Color(\"a98-rgb\", [0.71513, 0, 0])\ncolor(a98-rgb 0.71513 0 0 / 1)\n>>> Color(\"a98-rgb-linear\", [0.82231, 0.37626, 0.01549]).to_string()\n'color(--a98-rgb-linear 0.82231 0.37626 0.01549)'\n
"},{"location":"colors/a98_rgb_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.a98_rgb_linear import A98RGBLinear\n\nclass Color(Base): ...\n\nColor.register(A98RGBLinear())\n
"},{"location":"colors/aces2065_1/","title":"ACES 2065-1","text":"

The ACES 2065-1 color space is not registered in Color by default

Properties

Name: aces2065-1

White Point: D60 / 2\u02da

Coordinates:

Name Range r [0, 65504] g [0, 65504] b [0, 65504]

CIE 1931 xy Chromaticity \u2013 ACES AP0 Chromaticities

ACES 2065-1 is a linear color space that uses a set of primaries known as AP0 and has the widest gamut of all the ACES color spaces and fully encompasses the entire visible spectrum. It is meant primarily as an archival format due to its ability to encapsulate all visible colors. Typically, this is the color space you would use to transfer images/animations between production studios.

While it is considered an RGB color space, it also has enormous dynamic range with channels being able to well exceed the traditional range of 1.

Learn about ACES 2065-1

"},{"location":"colors/aces2065_1/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/aces2065_1/#inputoutput","title":"Input/Output","text":"

ACES 2065-1 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --aces2065-1:

color(--aces2065-1 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"aces2065-1\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--aces2065-1 r g b / a) form.

>>> Color(\"aces2065-1\", [0.43963, 0.08978, 0.01754])\ncolor(--aces2065-1 0.43963 0.08978 0.01754 / 1)\n>>> Color(\"aces2065-1\", [0.58374, 0.39584, 0.05951]).to_string()\n'color(--aces2065-1 0.58374 0.39584 0.05951)'\n
"},{"location":"colors/aces2065_1/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.aces2065_1 import ACES20651\n\nclass Color(Base): ...\n\nColor.register(ACES20651())\n
"},{"location":"colors/acescc/","title":"ACEScc","text":"

The ACEScc color space is not registered in Color by default

Properties

Name: acescc

White Point: D60 / 2\u02da

Coordinates:

Name Range* r [-0.0729,\u00a01.468] g [-0.0729,\u00a01.468] b [-0.0729,\u00a01.468]

* Ranges are approximate and have been rounded.

ACEScc is a color space based on the API primaries and is primarily used for color grading. It is a logarithmic color space, unlike ACEScg, and maps black at 0 and white at 1.

Learn about ACEScc

"},{"location":"colors/acescc/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/acescc/#inputoutput","title":"Input/Output","text":"

ACEScc is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --acescc:

color(--acescc r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"acescc\", [1, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--acescc r g b / a) form.

>>> Color('acescc', [0.51451, 0.33604, 0.23515])\ncolor(--acescc 0.51451 0.33604 0.23515 / 1)\n>>> Color('acescc', [0.53009, 0.48237, 0.32561]).to_string()\n'color(--acescc 0.53009 0.48237 0.32561)'\n
"},{"location":"colors/acescc/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.acescc import ACEScc\n\nclass Color(Base): ...\n\nColor.register(ACEScc())\n
"},{"location":"colors/acescct/","title":"ACEScct","text":"

The ACEScc color space is not registered in Color by default

Properties

Name: acescct

White Point: D60 / 2\u02da

Coordinates:

Name Range* r [-0.3584,\u00a01.468] g [-0.3584,\u00a01.468] b [-0.3584,\u00a01.468]

* Ranges are approximate and rounded to 3 decimal places.

ACEScct is very similar to ACEScc except that it adds a \"toe\" or a gamma curve in the dark region of the color space. This encoding is more appropriate for legacy color correction operators.

Learn about ACEScct

"},{"location":"colors/acescct/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/acescct/#inputsoutput","title":"Inputs/Output","text":"

ACEScct is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --acescct:

color(--acescct r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"acescct\", [1, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--acescct r g b / a) form.

>>> Color(\"acescct\", [0.51451, 0.33604, 0.23515])\ncolor(--acescct 0.51451 0.33604 0.23515 / 1)\n>>> Color(\"acescct\", [0.53009, 0.48237, 0.32561]).to_string()\n'color(--acescct 0.53009 0.48237 0.32561)'\n
"},{"location":"colors/acescct/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.acescct import ACEScct\n\nclass Color(Base): ...\n\nColor.register(ACEScct())\n
"},{"location":"colors/acescg/","title":"ACEScg","text":"

The ACEScg color space is not registered in Color by default

Properties

Name: acescg

White Point: D60 / 2\u02da

Coordinates:

Name Range r [0, 65504] g [0, 65504] b [0, 65504]

CIE 1931 xy Chromaticity \u2013 ACES AP1 Chromaticities

ACEScg is a color space often used by CG artists. It is \"scene-referred\" or linear. It doesn't have as wide a color gamut as ACES 2065-1 as it uses a different set of primaries called AP1, but it is far larger than most other color spaces one might use and has an enormous dynamic range.

Learn about ACEScg

"},{"location":"colors/acescg/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/acescg/#inputoutput","title":"Input/Output","text":"

ACEScg is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --acescg:

color(--acescg r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"acescg\", [1, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--acescg r g b / a) form.

>>> Color(\"acescg\", [0.6131, 0.07019, 0.02062])\ncolor(--acescg 0.6131 0.07019 0.02062 / 1)\n>>> Color(\"acescg\", [0.74085, 0.41498, 0.06184]).to_string()\n'color(--acescg 0.74085 0.41498 0.06184)'\n
"},{"location":"colors/acescg/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.acescg import ACEScg\n\nclass Color(Base): ...\n\nColor.register(ACEScg())\n
"},{"location":"colors/cam16/","title":"CAM16","text":"

The CAM16 color space is not registered in Color by default

Properties

Name: cam16

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-90, 90] b [-90, 90]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 color space.

A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source.

CAM16 is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous different attributes:

Name Description J Lightness C Chroma h hue s saturation Q Brightness M Colorfulness H Hue Quadrature

A color space can be constructed of using a subset of these attributes: JCh, JMh, Jsh, QCh, QMh, Qsh, etc. You can also construct Lab like spaces taking using the hue and either C, M, or s. The cam16 color space in ColorAide represents a Jab configuration based off M (Colorfulness).

Learn more.

"},{"location":"colors/cam16/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16/#inputoutput","title":"Input/Output","text":"

The CAM16 UCS space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16:

color(--cam16 j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16 j a b / a) form.

>>> Color(\"cam16\", [46.026, 72.143, 37.385], 1)\ncolor(--cam16 46.026 72.143 37.385 / 1)\n>>> Color(\"cam16\", [68.056, 13.955, 41.212], 1).to_string()\n'color(--cam16 68.056 13.955 41.212)'\n
"},{"location":"colors/cam16/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16 import CAM16\n\nclass Color(Base): ...\n\nColor.register(CAM16())\n
"},{"location":"colors/cam16_jmh/","title":"CAM16 JMh","text":"

The CAM16 JMh color space is not registered in Color by default

Properties

Name: cam16-jmh

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] m [0, 105] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 JMh color space.

A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source.

CAM16 is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous different attributes:

Name Description J Lightness C Chroma h hue s saturation Q Brightness M Colorfulness H Hue Quadrature

A color space can be constructed of using a subset of these attributes: JCh, JMh, Jsh, QCh, QMh, Qsh, etc. You can also construct Lab like spaces taking using the hue and either C, M, or s. The cam16-jmh color space in ColorAide represents the JMh configuration.

Learn more.

"},{"location":"colors/cam16_jmh/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness m colorfulness h hue"},{"location":"colors/cam16_jmh/#inputoutput","title":"Input/Output","text":"

The CAM16 JMh space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-jmh:

color(--cam16-jmh j m h / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-jmh j m h / a) form.

>>> Color(\"cam16-jmh\", [59.178, 40.82, 21.153], 1)\ncolor(--cam16-jmh 59.178 40.82 21.153 / 1)\n>>> Color(\"cam16-jmh\", [78.364, 9.6945, 28.629], 1).to_string()\n'color(--cam16-jmh 78.364 9.6945 28.629)'\n
"},{"location":"colors/cam16_jmh/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_jmh import CAM16JMh\n\nclass Color(Base): ...\n\nColor.register(CAM16JMh())\n
"},{"location":"colors/cam16_lcd/","title":"CAM16 LCD","text":"

The CAM16 LCD color space is not registered in Color by default

Properties

Name: cam16-lcd

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-75, 75] b [-75, 75]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 LCD color space.

This is the LCD variant of the CAM16 UCS color space and is optimized for \"large\" color distancing. See CAM16 UCS for more info.

Learn more.

"},{"location":"colors/cam16_lcd/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16_lcd/#inputoutput","title":"Input/Output","text":"

The CAM16 LCD space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-lcd:

color(--cam16-lcd j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-lcd j a b / a) form.

>>> Color(\"cam16-lcd\", [46.026, 81.254, 27.393], 1)\ncolor(--cam16-lcd 46.026 81.254 27.393 / 1)\n>>> Color(\"cam16-lcd\", [68.056, 43.51, 71.293], 1).to_string()\n'color(--cam16-lcd 68.056 43.51 71.293)'\n
"},{"location":"colors/cam16_lcd/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_ucs import CAM16LCD\n\nclass Color(Base): ...\n\nColor.register(CAM16LCD())\n
"},{"location":"colors/cam16_scd/","title":"CAM16 SCD","text":"

The CAM16 SCD color space is not registered in Color by default

Properties

Name: cam16-scd

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-40, 40] b [-40, 40]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 SCD color space.

This is the SCD variant of the CAM16 UCS color space and is optimized for \"small\" color distancing. See CAM16 UCS for more info.

Learn more.

"},{"location":"colors/cam16_scd/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16_scd/#inputoutput","title":"Input/Output","text":"

The CAM16 SCD space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-scd:

color(--cam16-scd j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-scd j a b / a) form.

>>> Color(\"cam16-scd\", [59.178, 33.597, 17.41], 1)\ncolor(--cam16-scd 59.178 33.597 17.41 / 1)\n>>> Color(\"cam16-scd\", [78.364, 8.3723, 24.725], 1).to_string()\n'color(--cam16-scd 78.364 8.3723 24.725)'\n
"},{"location":"colors/cam16_scd/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_ucs import CAM16SCD\n\nclass Color(Base): ...\n\nColor.register(CAM16SCD())\n
"},{"location":"colors/cam16_ucs/","title":"CAM16 UCS","text":"

The CAM16 UCS color space is not registered in Color by default

Properties

Name: cam16-ucs

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-50, 50] b [-50, 50]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 UCS color space.

A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source.

The CAM16 model is a successor of CIECAM02 with various fixes and improvements. The CAM16 UCS space takes the CAM16 model and applies an additional nonlinear transformation to lightness and colorfulness so that a color difference metric \u0394E can be based more closely on Euclidean distance. The cam16-ucd color space in ColorAide is based off CAM16 (Jab) which uses M (colorfulness) to derive the a and b values. There are also SCD and LCD variants which optimize the spaces for \"small\" and \"large\" color distancing respectively.

Learn more.

"},{"location":"colors/cam16_ucs/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16_ucs/#inputoutput","title":"Input/Output","text":"

The CAM16 UCS space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-ucs:

color(--cam16-ucs j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-ucs j a b / a) form.

>>> Color(\"cam16-ucs\", [59.178, 40.82, 21.153], 1)\ncolor(--cam16-ucs 59.178 40.82 21.153 / 1)\n>>> Color(\"cam16-ucs\", [78.364, 9.6945, 28.629], 1).to_string()\n'color(--cam16-ucs 78.364 9.6945 28.629)'\n
"},{"location":"colors/cam16_ucs/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_ucs import CAM16UCS\n\nclass Color(Base): ...\n\nColor.register(CAM16UCS())\n
"},{"location":"colors/cmy/","title":"CMY","text":"

The CMY color space is not registered in Color by default

Properties

Name: cmy

White Point: D65 / 2\u02da

Coordinates:

Name Range* c [0, 1] m [0, 1] y [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

The sRGB gamut represented within the CMY color space.

The CMY color model is a subtractive color model in which cyan, magenta and yellow pigments or dyes are added together in various ways to reproduce a broad array of colors. The name of the model comes from the initials of the three subtractive primary colors: cyan, magenta, and yellow.

The CMY color space, as ColorAide Extras has chosen to implement it, is directly calculated from the sRGB color space, and as such, is based off the sRGB primaries.

Learn more.

"},{"location":"colors/cmy/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases c cyan m magenta y yellow"},{"location":"colors/cmy/#inputoutput","title":"Input/Output","text":"

CMY is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cmy:

color(--cmy c m y / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cmy c m y / a) form.

>>> Color(\"cmy\", [0, 1, 1])\ncolor(--cmy 0 1 1 / 1)\n>>> Color(\"cmy\", [0, 0.35294, 1]).to_string()\n'color(--cmy 0 0.35294 1)'\n
"},{"location":"colors/cmy/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.cmy import CMY\n\nclass Color(Base): ...\n\nColor.register(CMY())\n
"},{"location":"colors/cmyk/","title":"CMYK","text":"

The CMYK color space is not registered in Color by default

Properties

Name: cmyk

White Point: D65 / 2\u02da

Coordinates:

Name Range* c [0, 1] m [0, 1] y [0, 1] k [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

The CMYK color model is a just like CMY except that it adds an additional channel k to control blackness.

The CMYK color space, as ColorAide Extras has chosen to implement it, is directly calculated from the sRGB color space, and as such, is based off the sRGB primaries.

Learn more.

"},{"location":"colors/cmyk/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases c cyan m magenta y yellow k black"},{"location":"colors/cmyk/#inputoutput","title":"Input/Output","text":"

CMY is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cmyk:

color(--cmyk c m y k / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cmyk c m y k / a) form.

>>> Color(\"cmyk\", [0, 1, 1, 0])\ncolor(--cmyk 0 1 1 0 / 1)\n>>> Color(\"cmyk\", [0, 0.35294, 1, 0]).to_string()\n'color(--cmyk 0 0.35294 1 0)'\n
"},{"location":"colors/cmyk/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.cmyk import CMYK\n\nclass Color(Base): ...\n\nColor.register(CMYK())\n
"},{"location":"colors/cubehelix/","title":"Cubehelix","text":"

The Cubehelix color space is not registered in Color by default

Properties

Name: cubehelix

White Point: D65 / 2\u02da

Coordinates:

Name Range* h [0, 360) s [0, 4.614] l [0, 1]

* The maximum saturation represents how high saturation can go, not that all colors with that saturation will be valid. As seen in the 3D rendering, while the coordinates are cylindrical, the shape of the space is not a cylinder.

The sRGB gamut represented within the Cubehelix color space.

Cubehelix is a color scheme created by Dave Green. It was originally created for the display of astronomical intensity images. It is not really one color scheme, but a method to generate various \"cubehelix\" color schemes. The name comes from the way the colors spiral through the sRGB color space.

Mike Bostock of Observable and D3 fame along with Jason Davies took the color scheme and created a cylindrical color space with it. This is the color space that is implemented in ColorAide.

Cubehelix color schemes can be easily generated by interpolating in the color space.

>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [360, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ba1150>\n>>> Color.interpolate([c1, c2], space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5a91490>\n

You can change the scheme by changing the start and end angle.

>>> c1 = Color('cubehelix', [0 + 180, 1, 0])\n>>> c2 = Color('cubehelix', [360 + 180, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5b9d6d0>\n>>> Color.interpolate([c1, c2], space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab59fa210>\n

You can increase the rotations by setting hue interpolation to specified and extending the angle difference to a distance greater than 360.

>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [360 * 3, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ebac90>\n>>> Color.interpolate([c1, c2], space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5b821d0>\n

You can even reverse the rotation by utilizing a negative difference in hue.

>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [-360, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d31310>\n>>> Color.interpolate([c1, c2], steps=16, space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5ba17d0>\n

To adjust gamma, simply apply a gamma easing to lightness.

>>> def ease_gamma(y=1.0):\n...     \"\"\"Ease gamma.\"\"\"\n... \n...     return lambda t: t ** y\n... \n>>> gamma = ease_gamma(0.2)\n>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [-360, 1, 1])\n>>> Color.discrete(\n...     [c1, c2],\n...     steps=16,\n...     space='cubehelix',\n...     hue='specified',\n...     progress={'l': gamma}\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab59db710>\n>>> Color.interpolate(\n...     [c1, c2],\n...     space='cubehelix',\n...     hue='specified',\n...     progress={'l': gamma}\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5a0b810>\n

Viewing the interpolation in 3D, we can see the spiraling of colors that gave the color scheme the name Cubehelix.

Learn more.

"},{"location":"colors/cubehelix/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/cubehelix/#inputoutput","title":"Input/Output","text":"

The Cubehelix space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cubehelix:

color(--cubehelix h s l / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cubehelix h s l / a) form.

>>> Color(\"cubehelix\", [351.81, 1.9489, 0.3])\ncolor(--cubehelix 351.81 1.9489 0.3 / 1)\n>>> Color(\"cubehelix\", [36.577, 1.7357, 0.68176]).to_string()\n'color(--cubehelix 36.577 1.7357 0.68176)'\n
"},{"location":"colors/cubehelix/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.cubehelix import Cubehelix\n\nclass Color(Base): ...\n\nColor.register(Cubehelix())\n
"},{"location":"colors/din99o/","title":"DIN99o","text":"

The DIN99o color space is not registered in Color by default

Properties

Name: din99o

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] a [-55, 55] b [-55, 55]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the DIN99o color space.

The DIN99 color space system is a further development of the CIELab color space system developed by the FNF / FNL 2 Colorimetry Working Committee. It takes the CIELab space (with a D65 illuminant) and compresses it such that the space yields better equidistant using Euclidean distance. The whole color space is essentially modified to better fit the color distancing algorithm opposed to CIELab which has adapted the color distancing algorithm to better fit the color space, the latest iteration being \u2206E*00.

Learn about DIN99o

"},{"location":"colors/din99o/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/din99o/#inputoutput","title":"Input/Output","text":"

As DIN99o is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --din99o:

color(--din99o l u v / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"din99o\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--din99o l u v / a) form.

>>> Color(\"din99o\", [57.289, 39.498, 30.518])\ncolor(--din99o 57.289 39.498 30.518 / 1)\n>>> Color(\"din99o\", [77.855, 16.444, 40.318]).to_string()\n'color(--din99o 77.855 16.444 40.318)'\n
"},{"location":"colors/din99o/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.din99o import DIN99o\n\nclass Color(Base): ...\n\nColor.register(DIN99o())\n
"},{"location":"colors/display_p3/","title":"Display P3","text":"

The Display P3 color space is registered in Color by default

Properties

Name: display-p3

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Display P3 Chromaticities

Display P3 is a combination of the DCI-P3 color gamut with the D65 white point together with the sRGB gamma curve. It originated from the DCI-P3 color gamut's implementation in digital cinema projectors, as this standard offers more vibrant greens and reds than the traditional sRGB color gamut.

Learn about Display P3

"},{"location":"colors/display_p3/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/display_p3/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(display-p3 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"display-p3\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(display-p3 r g b / a) form.

>>> Color('display-p3', [0.91749, 0.20029, 0.13856])\ncolor(display-p3 0.91749 0.20029 0.13856 / 1)\n>>> Color('display-p3', [0.94965, 0.6629, 0.23297]).to_string()\n'color(display-p3 0.94965 0.6629 0.23297)'\n
"},{"location":"colors/display_p3/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.display_p3 import DisplayP3\n\nclass Color(Base): ...\n\nColor.register(DisplayP3())\n
"},{"location":"colors/display_p3_linear/","title":"Linear Display P3","text":"

The Linear Display P3 color space is registered in Color by default

Properties

Name: display-p3-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Display P3 Chromaticities

The Linear Display P3 space is the same as Display P3 except that the transfer function is linear-light (there is no gamma-encoding).

Learn about Display P3

"},{"location":"colors/display_p3_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/display_p3_linear/#inputoutput","title":"Input/Output","text":"

Linear Display P3 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --display-p3-linear:

color(--display-p3-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"display-p3-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--display-p3-linear r g b / a) form.

>>> Color(\"display-p3-linear\", [0.82246, 0.03319, 0.01708])\ncolor(--display-p3-linear 0.82246 0.03319 0.01708 / 1)\n>>> Color(\"display-p3-linear\", [0.88926, 0.39697, 0.04432]).to_string()\n'color(--display-p3-linear 0.88926 0.39697 0.04432)'\n
"},{"location":"colors/display_p3_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.display_p3_linear import DisplayP3\n\nclass Color(Base): ...\n\nColor.register(DisplayP3Linear())\n
"},{"location":"colors/hct/","title":"HCT","text":"

The HCT color space is not registered in Color by default

Properties

Name: hct

White Point: D65 / 2\u02da

Coordinates:

Name Range* h [0, 360) c [0, 145] t [0, 100]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the HCT color space.

The HCT color space is Google's attempt at a perceptually accurate color system. Essentially, it is two color spaces glued together: 'H' (hue) and 'C' (chroma) come from the CAM16 color appearance model and 'T' (tone) is the lightness from the CIELAB (D65) color space. The space was created to take the more consistent perceptual hues from CAM16 and use the better lightness prediction found in CIELAB. The color space has the advantage of being well suited for creating color schemes with decent contrast and makes it easy to create nice tonal palettes, but the downside is that it is expensive to translate to and from compared to other color spaces.

Since HCT is partly based on CAM16, it inherits the expensive operations used to translate color to and from the CAM16 color model. In the forward direction (to HCT) color conversions are only marginally more expensive than CAM16, but in the reverse direction (from HCT) the conversions are much more expensive. This is because the CAM16 color model needs the context of chroma, hue, and lightness in order to translate any of its components, but HCT throws away CAM16 lightness and uses CIELAB lightness which has no direct relation to the other components. In order to translate color from HCT, more complex methods are needed to approximate the missing CAM16 lightness in order for a good round trip conversion.

Google implements the HCT color space in their \"Material Color Utilities\" library, but in that library it is restricted to sRGB and only to 8 bit precision. Wide gamut colors such as Display P3 cannot be used.

ColorAide's goal was not to port Material's Color Utilities, but to implement HCT as a proper color space that can be used in sRGB and other wide gamut color spaces. In ColorAide we implement the HCT color space exactly as described and create the space from both CIELAB and CAM16. We then provide a generic approximation back out of HCT at a higher precision to better support not only sRGB, but other wide gamut color spaces such as: Display P3, Rec. 2020, A98 RGB, etc.

Conversion Limitations

Extreme colors, like those in ProPhoto RGB that fall outside the visible spectrum, may be difficult to round trip with the same high accuracy as other colors well inside the visible spectrum. These colors naturally stress the CAM16 color model and make approximation from HCT even more difficult. With that said, most color spaces within the visible spectrum should convert reasonably well.

Learn more.

"},{"location":"colors/hct/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue c chroma t tone, lightness"},{"location":"colors/hct/#inputoutput","title":"Input/Output","text":"

The HCT space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hct:

color(--hct h c t / a)  // Color function\n

The string representation of the color object and the default string output use the color(--hct h c t / a) form.

>>> Color(\"hct\", [27.41, 113.36, 53.237], 1)\ncolor(--hct 27.41 113.36 53.237 / 1)\n>>> Color(\"hct\", [71.257, 60.528, 74.934], 1).to_string()\n'color(--hct 71.257 60.528 74.934)'\n
"},{"location":"colors/hct/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hct import HCT\n\nclass Color(Base): ...\n\nColor.register(HCT())\n
"},{"location":"colors/hct/#tonal-palettes","title":"Tonal Palettes","text":"

One of the applications of HCT is generating tonal palettes. When coupled with ColorAide's \u2206Ehct distancing algorithm and the hct-chroma gamut mapping algorithm, we can produce tonal palettes just like in Material Color Utilities.

>>> c = Color('hct', [325, 24, 50])\n>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit='hct-chroma') for tone in tones])\n['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']\n

Material Color Utilities, as they currently implement it, only works within the sRGB color space, but ColorAide implements HCT such that it can be used in various wide gamuts as well.

>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n>>> c1 = Color('display-p3', [1, 0, 1]).convert('hct')\n>>> Steps([c1.clone().set('tone', tone).convert('display-p3').to_string(fit='hct-chroma') for tone in tones])\n['color(display-p3 0 0 0)', 'color(display-p3 0.21092 0 0.21039)', 'color(display-p3 0.34434 0 0.34368)', 'color(display-p3 0.48744 0 0.48675)', 'color(display-p3 0.63855 0 0.63794)', 'color(display-p3 0.79657 0 0.79617)', 'color(display-p3 0.96066 0 0.96062)', 'color(display-p3 1 0.42437 0.97127)', 'color(display-p3 1 0.65324 0.95784)', 'color(display-p3 1 0.8358 0.96452)', 'color(display-p3 1 0.92002 0.97366)', 'color(display-p3 1 1 1)']\n>>> c2 = Color('rec2020', [0, 0, 1]).convert('hct')\n>>> Steps([c2.clone().set('tone', tone).convert('rec2020').to_string(fit='hct-chroma') for tone in tones])\n['color(rec2020 0 0 0)', 'color(rec2020 0 0.00078 0.41888)', 'color(rec2020 0 0.00088 0.70689)', 'color(rec2020 0.00881 0.01642 1)', 'color(rec2020 0.15806 0.21748 1)', 'color(rec2020 0.29634 0.36043 1)', 'color(rec2020 0.43177 0.49013 1)', 'color(rec2020 0.5696 0.61616 1)', 'color(rec2020 0.71119 0.74184 1)', 'color(rec2020 0.85694 0.86865 1)', 'color(rec2020 0.93139 0.93271 1)', 'color(rec2020 1 1 1)']\n

Due to differences in approximation techniques, general precision differences, and gamut mapping of the two implementations internally, ColorAide may return colors slightly different from Material Color Utilities. These differences are extremely small and not perceptible to the eye.

Below we have two examples. We've taken the results from Material's tests and we've generated the same tonal palettes and output both as HCT. We can compare which hues stay overall more constant, which chroma gets reduced more than others, and which hue and tone are less affected by the gamut mapping. Can you definitively say that one looks more correct than the other? Can you say there is notable, visual difference?

>>> def tonal_palette(c):\n...     tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n...     return [c.clone().set('tone', tone).fit('srgb', method='hct-chroma') for tone in tones]\n... \n>>> material1 = ['#000000', '#00006e', '#0001ac',\n...              '#0000ef', '#343dff', '#5a64ff',\n...              '#7c84ff', '#9da3ff', '#bec2ff',\n...              '#e0e0ff', '#f1efff', '#ffffff']\n>>> c = Color('blue').convert('hct')\n>>> Steps([x.to_string() for x in tonal_palette(c)])\n['color(--hct 209.55 0 0)', 'color(--hct 282.79 51.624 10.006)', 'color(--hct 282.79 68.208 20.007)', 'color(--hct 282.77 83.8 30.018)', 'color(--hct 282.76 82.348 39.985)', 'color(--hct 282.76 73.352 49.989)', 'color(--hct 282.76 62.073 59.993)', 'color(--hct 282.76 49.091 69.995)', 'color(--hct 282.76 34.775 79.997)', 'color(--hct 282.75 19.213 89.998)', 'color(--hct 282.75 10.819 94.999)', 'color(--hct 209.54 2.8716 100)']\n>>> Steps([Color(x).convert('hct').to_string() for x in material1])\n['color(--hct 209.55 0 0)', 'color(--hct 282.84 51.709 9.9973)', 'color(--hct 282.74 68.127 20.044)', 'color(--hct 282.77 83.756 29.989)', 'color(--hct 282.81 82.297 40.059)', 'color(--hct 282.79 73.236 50.106)', 'color(--hct 283.04 62.214 59.895)', 'color(--hct 282.95 49.257 69.882)', 'color(--hct 282.15 34.694 80.039)', 'color(--hct 282.23 19.146 90.035)', 'color(--hct 282.07 10.786 95.015)', 'color(--hct 209.54 2.8716 100)']\n>>> material2 = ['#000000', '#191a2c', '#2e2f42',\n...              '#444559', '#5c5d72', '#75758b',\n...              '#8f8fa6', '#a9a9c1', '#c5c4dd',\n...              '#e1e0f9', '#f1efff', '#ffffff']\n>>> c['chroma'] = 16\n>>> Steps([x.to_string() for x in tonal_palette(c)])\n['color(--hct 209.55 0 0)', 'color(--hct 282.76 16 10)', 'color(--hct 282.76 16 20)', 'color(--hct 282.76 16 30)', 'color(--hct 282.76 16 40)', 'color(--hct 282.76 16 50)', 'color(--hct 282.76 16 60)', 'color(--hct 282.76 16 70)', 'color(--hct 282.76 16 80)', 'color(--hct 282.76 16 90)', 'color(--hct 282.75 10.819 94.999)', 'color(--hct 209.54 2.8716 100)']\n>>> Steps([Color(x).convert('hct').to_string() for x in material2])\n['color(--hct 209.55 0 0)', 'color(--hct 283.31 16.104 10.01)', 'color(--hct 282.9 16.074 20.073)', 'color(--hct 282.41 16.065 29.927)', 'color(--hct 281.87 16.078 40.112)', 'color(--hct 283.49 16.042 49.94)', 'color(--hct 282.85 16.13 60.103)', 'color(--hct 282.27 16.258 69.938)', 'color(--hct 283.58 16.297 79.937)', 'color(--hct 282.75 15.918 89.933)', 'color(--hct 282.07 10.786 95.015)', 'color(--hct 209.54 2.8716 100)']\n
"},{"location":"colors/hpluv/","title":"HPLuv","text":"

The HPLuv color space is not registered in Color by default

Properties

Name: hpluv

Color CSS ID: --hpluv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) p [0, 100] l [0, 100]

HSLuv color space in 3D

HPLuv is similar to HSLuv but takes as many colors as it can from CIELChuv without distorting the chroma. This ends up reducing the gamut to a subset of the sRGB gamut. In the end, only more pastel colors remain.

Learn about HSLuv

"},{"location":"colors/hpluv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s perpendiculars l lightness"},{"location":"colors/hpluv/#inputsoutput","title":"Inputs/Output","text":"

HPLuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hpluv:

color(--hpluv h p l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hpluv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--hpluv h p l / a) form.

>>> Color(\"hpluv\", [23.881, 100, 53.237])\ncolor(--hpluv 23.881 100 53.237 / 1)\n>>> Color(\"hpluv\", [49.45, 100, 74.934]).to_string()\n'color(--hpluv 49.45 100 74.934)'\n
"},{"location":"colors/hpluv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hpluv import HSPLuv\n\nclass Color(Base): ...\n\nColor.register(HPLuv())\n
"},{"location":"colors/hsi/","title":"HSI","text":"

The HSI color space is not registered in Color by default

Properties

Name: hsi

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] i [0, 1]

The sRGB gamut represented within the HSI color space.

The HSI model is similar to models like HSL and HSV except that it uses I for intensity instead of Lightness or Value. It does not attempt to \"fill\" a cylinder by its definition of saturation leading to a very different look when we plot it.

Learn more.

"},{"location":"colors/hsi/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation i intensity"},{"location":"colors/hsi/#inputoutput","title":"Input/Output","text":"

The HSI space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hsi:

color(--hsi h s i / a)  // Color function\n

The string representation of the color object and the default string output use the color(--hsi h s i / a) form.

>>> Color(\"hsi\", [0, 1, 0.33333])\ncolor(--hsi 0 1 0.33333 / 1)\n>>> Color(\"hsi\", [38.824, 1, 0.54902]).to_string()\n'color(--hsi 38.824 1 0.54902)'\n
"},{"location":"colors/hsi/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsi import HSI\n\nclass Color(Base): ...\n\nColor.register(HSI())\n
"},{"location":"colors/hsl/","title":"HSL","text":"

The HSL color space is registered in Color by default

Properties

Name: hsl

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] l [0, 1]

HSL color space in 3D

HSL is an alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers to more closely align with the way human vision perceives color-making attributes. In these models, colors of each hue are arranged in a radial slice, around a central axis of neutral colors which ranges from black at the bottom to white at the top.

HSL models the way different paints mix together to create color in the real world, with the lightness dimension resembling the varying amounts of black or white paint in the mixture.

Learn about HSL

"},{"location":"colors/hsl/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/hsl/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --hsl:

hsl(h s l / a)          // HSL function\nhsl(h, s, l)            // Legacy HSL function\nhsla(h, s, l, a)        // Legacy HSLA function\ncolor(--hsl h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hsl\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--hsl h s l / a) form, but the default string output will be the hsl(h s l / a) form.

>>> Color(\"hsl\", [0, 1, 0.5])\ncolor(--hsl 0 1 0.5 / 1)\n>>> Color(\"hsl\", [38.824, 1, 0.5], ).to_string()\n'hsl(38.824 100% 50%)'\n>>> Color(\"hsl\", [60, 1, 0.5]).to_string(comma=True)\n'hsl(60, 100%, 50%)'\n>>> Color(\"hsl\", [120, 1, 0.25098]).to_string(color=True)\n'color(--hsl 120 1 0.25098)'\n>>> Color(\"hsl\", [240, 1, 0.5]).to_string(percent=False)\n'hsl(240 100 50)'\n
"},{"location":"colors/hsl/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsl import HSL\n\nclass Color(Base): ...\n\nColor.register(HSL())\n
"},{"location":"colors/hsluv/","title":"HSLuv","text":"

The HSLuv color space is not registered in Color by default

Properties

Name: hsluv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 100] l [0, 100]

HSLuv color space in 3D

HSLuv is a human-friendly alternative to HSL. It was formerly known as \"HUSL\" and is a variation of the CIELChuv color space, where the chroma component is replaced by a saturation component which allows you to span all the available chroma as a percentage. HSLuv is constrained to the sRGB gamut.

Learn about HSLuv

"},{"location":"colors/hsluv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/hsluv/#inputoutput","title":"Input/Output","text":"

HSLuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hsluv:

color(--hsluv h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hsluv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--hsluv h s l / a) form.

>>> Color(\"hsluv\", [12.177, 100, 53.237])\ncolor(--hsluv 12.177 100 53.237 / 1)\n>>> Color(\"hsluv\", [44.683, 100, 74.934]).to_string()\n'color(--hsluv 44.683 100 74.934)'\n
"},{"location":"colors/hsluv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsluv import HSLuv\n\nclass Color(Base): ...\n\nColor.register(HSLuv())\n
"},{"location":"colors/hsv/","title":"HSV","text":"

The HSV color space is registered in Color by default

Properties

Name: hsv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] v [0, 1]

HSV color space in 3D

HSV is a color space similar to the modern RGB and CMYK models. The HSV color space has three components: hue, saturation and value. 'Value' is sometimes substituted with 'brightness' and then it is known as HSB. HSV models how colors appear under light.

Learn about HSV

"},{"location":"colors/hsv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation v value"},{"location":"colors/hsv/#inputoutput","title":"Input/Output","text":"

HSV is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --hsv:

color(--hsv 0 0% 0% / 1)\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hsv\", [0, 0, 0], 1)\n

The string representation of the color object and default string output will always use the color(hsv h s v / a) form.

>>> Color(\"hsv\", [0, 1, 1])\ncolor(--hsv 0 1 1 / 1)\n>>> Color(\"hsv\", [38.824, 1, 1]).to_string()\n'color(--hsv 38.824 1 1)'\n
"},{"location":"colors/hsv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsv import HSV\n\nclass Color(Base): ...\n\nColor.register(HSV())\n
"},{"location":"colors/hunter_lab/","title":"Hunter Lab","text":"

The Hunter Lab color space is not registered in Color by default

Properties

Name: hunter-lab

White Point: D65 / 2\u02da

Coordinates:

Name Range l [0, 100] a [-210, 210] b [-210, 210]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the Hunter Lab color space.

The Hunter Lab color space, defined in 1948 by Richard S. Hunter, is another color space referred to as \"Lab\". Like CIELab, it was also designed to be computed via simple formulas from the CIE XYZ space, but to be more perceptually uniform than CIE XYZ. Hunter named his coordinates L, a, and b. The CIE named the coordinates for CIELab as L, a, b* to distinguish them from Hunter's coordinates.

Learn more.

"},{"location":"colors/hunter_lab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/hunter_lab/#inputoutput","title":"Input/Output","text":"

The Hunter Lab space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hunter-lab:

color(--hunter-lab l a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--hunter-lab l a b / a) form.

>>> Color(\"hunter-lab\", [46.113, 82.672, 28.408])\ncolor(--hunter-lab 46.113 82.672 28.408 / 1)\n>>> Color(\"hunter-lab\", [69.407, 23.266, 40.946]).to_string()\n'color(--hunter-lab 69.407 23.266 40.946)'\n
"},{"location":"colors/hunter_lab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hunter_lab import HunterLab\n\nclass Color(Base): ...\n\nColor.register(HunterLab())\n
"},{"location":"colors/hwb/","title":"HWB","text":"

The HWB color space is registered in Color by default

Properties

Name: hwb

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) w [0, 1] b [0, 1]

HWB color space in 3D

HWB is a cylindrical-coordinate representation of points in an RGB color model, similar to HSL and HSV. It was developed by HSV's creator Alvy Ray Smith in 1996 to address some of the issues with HSV. HWB was designed to be more intuitive for humans to use and slightly faster to compute. The first coordinate, H (Hue), is the same as the Hue coordinate in HSL and HSV. W and B stand for Whiteness and Blackness respectively and range from 0-100% (or 0-1). The mental model is that the user can pick a main hue and then \"mix\" it with white and/or black to produce the desired color.

Learn about HWB

"},{"location":"colors/hwb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue w whiteness b blackness"},{"location":"colors/hwb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --hwb:

hwb(h w b / a)          // HWB function\ncolor(--hwb h w b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hwb\", [0, 0, 100], 1)\n

The string representation of the color object will always default to the color(--hwb h w b / a) form, but the default string output will be the hwb(h s l / a) form.

>>> Color(\"hwb\", [0, 0, 0])\ncolor(--hwb 0 0 0 / 1)\n>>> Color(\"hwb\", [38.824, 0, 0]).to_string()\n'hwb(38.824 0% 0%)'\n>>> Color(\"hwb\", [60, 0, 0]).to_string(percent=False)\n'hwb(60 0 0)'\n>>> Color(\"hwb\", [120, 0, 0]).to_string(color=True)\n'color(--hwb 120 0 0)'\n
"},{"location":"colors/hwb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hwb import HWB\n\nclass Color(Base): ...\n\nColor.register(HWB())\n
"},{"location":"colors/ictcp/","title":"ICtCp","text":"

The ICtCp color space is not registered in Color by default

Properties

Name: ictcp

White Point: D65 / 2\u02da

Coordinates:

Name Range* i [0, 1] ct [-0.5, 0.5] cp [-0.5, 0.5]

* Space is not bound to the range but is specified to enclose the full range of an HDR BT.2020 gamut and is used to define percentage inputs/outputs.

The sRGB gamut represented within the ICtCp color space.

ICtCp is a color space format with better perceptual uniformity than CIELab and is used as a part of the color image pipeline in video and digital photography systems for high dynamic range (HDR) and wide color gamut (WCG) imagery. It was developed by Dolby Laboratories from the IPT color space by Ebner and Fairchild. It was designed with the intention to replace YCbCr.

Learn about ICtCp

"},{"location":"colors/ictcp/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases i intensity ct tritan cp protan"},{"location":"colors/ictcp/#inputoutput","title":"Input/Output","text":"

As ICtCp is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ictcp:

color(--ictcp i ct cp / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"ictcp\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--ictcp i ct cp / a) form.

>>> Color(\"ictcp\", [0.42785, -0.11574, 0.2788])\ncolor(--ictcp 0.42785 -0.11574 0.2788 / 1)\n>>> Color(\"ictcp\", [0.50497, -0.20797, 0.11077]).to_string()\n'color(--ictcp 0.50497 -0.20797 0.11077)'\n
"},{"location":"colors/ictcp/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ictcp import ICtCp\n\nclass Color(Base): ...\n\nColor.register(ICtCp())\n
"},{"location":"colors/igpgtg/","title":"IgPgTg","text":"

The IgPgTg color space is not registered in Color by default

Properties

Name: ipt

White Point: D65 / 2\u02da

Coordinates:

Name Range ig [0, 1] pg [-1, 1] tg [-1, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the IgPgTg color space.

IgPgTg uses the same structure as IPT, an established hue-uniform color space utilized in gamut mapping applications. While IPT was fit to visual data on the perceived hue, IgPgTg was optimized based on evidence linking the peak wavelength of Gaussian-shaped light spectra to their perceived hues.

Learn more.

"},{"location":"colors/igpgtg/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases ig intensity pg protan tg tritan"},{"location":"colors/igpgtg/#inputoutput","title":"Input/Output","text":"

The IgPgTg space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --igpgtg:

color(--igpgtg ig pg tg / a)  // Color function\n

The string representation of the color object and the default string output use the color(--igpgtg ig pg tg / a) form.

>>> Color(\"igpgtg\", [0.54834, 0.15366, 0.43674])\ncolor(--igpgtg 0.54834 0.15366 0.43674 / 1)\n>>> Color(\"igpgtg\", [0.73238, 0.0397, 0.32108]).to_string()\n'color(--igpgtg 0.73238 0.0397 0.32108)'\n
"},{"location":"colors/igpgtg/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.igpgtg import IgPgTg\n\nclass Color(Base): ...\n\nColor.register(IgPgTg())\n
"},{"location":"colors/ipt/","title":"IPT","text":"

The IPT color space is not registered in Color by default

Properties

Name: ipt

White Point: D65 / 2\u02da

Coordinates:

Name Range* i [0, 1] p [-1, 1] t [-1, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the IPT color space.

Ebner and Fairchild addressed the issue of non-constant lines of hue in their color space dubbed IPT. The IPT color space converts D65-adapted XYZ data (XD65, YD65, ZD65) to long-medium-short cone response data (LMS) using an adapted form of the Hunt-Pointer-Estevez matrix (MHPE(D65)).

The IPT color appearance model excels at providing a formulation for hue where a constant hue value equals a constant perceived hue independent of the values of lightness and chroma (which is the general ideal for any color appearance model, but hard to achieve). It is therefore well-suited for gamut mapping implementations.

Learn more.

"},{"location":"colors/ipt/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases i intensity p protan t tritan

Inputs

The IPT space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ipt:

color(--ipt i p t / a)  // Color function\n

The string representation of the color object and the default string output use the color(--ipt i p t / a) form.

>>> Color(\"ipt\", [0.45616, 0.62086, 0.44282])\ncolor(--ipt 0.45616 0.62086 0.44282 / 1)\n>>> Color(\"ipt\", [0.64877, 0.189, 0.5303]).to_string()\n'color(--ipt 0.64877 0.189 0.5303)'\n
"},{"location":"colors/ipt/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ipt import IPT\n\nclass Color(Base): ...\n\nColor.register(IPT())\n
"},{"location":"colors/jzazbz/","title":"Jzazbz","text":"

The Jzazbz color space is not registered in Color by default

Properties

Name: jzazbz

White Point: D65 / 2\u02da

Coordinates:

Name Range* jz [0, 1] az [-0.5, 0.5] bz [-0.5, 0.5]

* Space is not bound to the range but is specified to enclose the full range of an HDR BT.2020 gamut and is used to define percentage inputs/outputs.

The sRGB gamut represented within the Jzazbz color space.

Jzazbz is a a color space designed for perceptual uniformity in high dynamic range (HDR) and wide color gamut (WCG) applications. Conceptually it is similar to CIELab, but claims the following improvements:

  • Perceptual color difference is predicted by Euclidean distance.
  • Perceptually uniform: MacAdam ellipses of just-noticeable-difference (JND) are more circular, and closer to the same sizes.
  • Hue linearity: changing saturation or lightness has less shift in hue.

Learn about Jzazbz

"},{"location":"colors/jzazbz/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases jz lightness az a bz b"},{"location":"colors/jzazbz/#inputoutput","title":"Input/Output","text":"

As Jzazbz is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --jzazbz:

color(--jzazbz jz az bz / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"jzazbz\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--jzazbz jz az bz / a) form.

>>> Color(\"jzazbz\", [0.13438, 0.11789, 0.11188])\ncolor(--jzazbz 0.13438 0.11789 0.11188 / 1)\n>>> Color(\"jzazbz\", [0.16937, 0.0312, 0.12308]).to_string()\n'color(--jzazbz 0.16937 0.0312 0.12308)'\n
"},{"location":"colors/jzazbz/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.jzazbz import Jzazbz\n\nclass Color(Base): ...\n\nColor.register(Jzazbz())\n
"},{"location":"colors/jzczhz/","title":"JzCzhz","text":"

The JzCzhz color space is not registered in Color by default

Properties

Name: jzczhz

White Point: D65 / 2\u02da

Coordinates:

Name Range jz [0, 1] cz [0, 0.5] hz [0, 360)

* Space is not bound to the range but is specified to enclose the full range of an HDR BT.2020 gamut and is used to define percentage inputs/outputs.

The sRGB gamut represented within the JzCzhz color space.

JzCzhz is the cylindrical form of Jzazbz.

Learn about JzCzhz

"},{"location":"colors/jzczhz/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases jz lightness cz chroma hz hue"},{"location":"colors/jzczhz/#inputoutput","title":"Input/Output","text":"

As JzCzhz is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --jzczhz:

color(--jzczhz jz cz hz / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"jzczhz\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--jzczhz jz cz hz / a) form.

>>> Color(\"jzczhz\", [0.13438, 0.16252, 43.502])\ncolor(--jzczhz 0.13438 0.16252 43.502 / 1)\n>>> Color(\"jzczhz\", [0.16937, 0.12698, 75.776]).to_string()\n'color(--jzczhz 0.16937 0.12698 75.776)'\n
"},{"location":"colors/jzczhz/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.jzczhz import JzCzhz\n\nclass Color(Base): ...\n\nColor.register(JzCzhz())\n
"},{"location":"colors/lab/","title":"LAB D50","text":"

The Lab D50 color space is registered in Color by default

Properties

Name: lab

White Point: D50 / 2\u02da

Coordinates:

Name Range* l [0, 100] a [-125, 125] b [-125, 125]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELab D50 color space.

The CIELab color space also referred to as L*a*b* is a color space defined by the International Commission on Illumination (abbreviated CIE) in 1976. It expresses color as three values: L* for perceptual lightness, and a* and b* for the four unique colors of human vision: red, green, blue, and yellow. CIELab was intended as a perceptually uniform space, where a given numerical change corresponds to similar perceived change in color. While the CIELab space is not truly perceptually uniform, it nevertheless is useful in industry for detecting small differences in color.

Learn about CIELab

"},{"location":"colors/lab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/lab/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --lab:

lab(l a b / a)          // Lab function\ncolor(--lab l a b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lab\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--lab l a b / a) form, but the default string output will be the lab(l a b / a) form.

>>> Color(\"lab\", [54.291, 80.805, 69.891])\ncolor(--lab 54.291 80.805 69.891 / 1)\n>>> Color(\"lab\", [75.59, 27.516, 79.121]).to_string()\n'lab(75.59 27.516 79.121)'\n>>> Color(\"lab\", [97.607, -15.75, 93.394]).to_string(percent=True)\n'lab(97.607% -12.6% 74.715%)'\n>>> Color(\"lab\", [46.278, -47.552, 48.586]).to_string(color=True)\n'color(--lab 46.278 -47.552 48.586)'\n
"},{"location":"colors/lab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lab import Lab\n\nclass Color(Base): ...\n\nColor.register(Lab())\n
"},{"location":"colors/lab_d65/","title":"Lab D65","text":"

The Lab D65 color space is registered in Color by default

Properties

Name: lab-d65

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] a [-130, 130] b [-130, 130]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELab D50 color space.

CIELab D65 is the same as CIELab except it uses a D65 white point.

Learn about CIELab

"},{"location":"colors/lab_d65/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/lab_d65/#inputoutput","title":"Input/Output","text":"

As a D65 variant of CIELab is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lab-d65:

color(--lab-d65 l a b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lab-d65\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lab-d65 l a b / a) form.

>>> Color(\"lab-d65\", [53.237, 80.09, 67.203])\ncolor(--lab-d65 53.237 80.09 67.203 / 1)\n>>> Color(\"lab-d65\", [74.934, 23.927, 78.953]).to_string()\n'color(--lab-d65 74.934 23.927 78.953)'\n
"},{"location":"colors/lab_d65/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lab_d65 import LabD65\n\nclass Color(Base): ...\n\nColor.register(LabD65())\n
"},{"location":"colors/lch/","title":"LCh D50","text":"

The LCh D50 color space is registered in Color by default

Properties

Name: lch

White Point: D50 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 150] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELCh D50 color space.

The \"CIELCh\" space is a color space based on CIELab, which uses the polar coordinates C* (chroma, relative saturation) and h\u00b0 (hue angle, angle of the hue in the CIELab color wheel) instead of the Cartesian coordinates a* and b*. The CIELab lightness L* remains unchanged.

Learn about CIELCh

"},{"location":"colors/lch/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lch/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --lch:

lch(l c h / a)          // LCh function\ncolor(--lch l c h / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lch\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--lch l c h / a) form, but the default string output will be the lch(l c h / a) form.

>>> Color(\"lch\", [54.291, 106.84, 40.858])\ncolor(--lch 54.291 106.84 40.858 / 1)\n>>> Color(\"lch\", [75.59, 83.769, 70.824]).to_string()\n'lch(75.59 83.769 70.824)'\n>>> Color(\"lch\", [97.607, 94.712, 99.572]).to_string(percent=True)\n'lch(97.607% 63.141% 99.572)'\n>>> Color(\"lch\", [46.278, 67.984, 134.38]).to_string(color=True)\n'color(--lch 46.278 67.984 134.38)'\n
"},{"location":"colors/lch/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lch import LCh\n\nclass Color(Base): ...\n\nColor.register(LCh())\n
"},{"location":"colors/lch99o/","title":"DIN99o LCh","text":"

The DIN99o LCh color space is not registered in Color by default

Properties

Name: lch99o

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 60] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the DIN99o LCh color space.

DIN99o LCh is the cylindrical form of DIN99o.

Learn about DIN99o LCh

"},{"location":"colors/lch99o/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lch99o/#inputoutput","title":"Input/Output","text":"

As DIN99o LCh is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lch99o:

color(--lch99o jz cz hz / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lch99o\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lch99o jz cz hz / a) form.

>>> Color(\"lch99o\", [57.289, 49.915, 37.692])\ncolor(--lch99o 57.289 49.915 37.692 / 1)\n>>> Color(\"lch99o\", [77.855, 43.543, 67.811]).to_string()\n'color(--lch99o 77.855 43.543 67.811)'\n
"},{"location":"colors/lch99o/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lch99o import LCh99o\n\nclass Color(Base): ...\n\nColor.register(LCh99o())\n
"},{"location":"colors/lch_d65/","title":"LCh D65","text":"

The LCh D65 color space is registered in Color by default

Properties

Name: lch-d65

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 160] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELCh D65 color space.

CIELCh D65 is the same as CIELCh except it uses a D65 white point.

Learn about CIELCh

"},{"location":"colors/lch_d65/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lch_d65/#inputoutput","title":"Input/Output","text":"

As a D65 variant of CIELCh is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lch-d65:

color(--lch-d65 l c h / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lch-d65\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lch-d65 l c h / a) form.

>>> Color(\"lch-d65\", [53.237, 104.55, 40])\ncolor(--lch-d65 53.237 104.55 40 / 1)\n>>> Color(\"lch-d65\", [74.934, 82.499, 73.14]).to_string()\n'color(--lch-d65 74.934 82.499 73.14)'\n
"},{"location":"colors/lch_d65/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lch_d65 import LChD65\n\nclass Color(Base): ...\n\nColor.register(LChD65())\n
"},{"location":"colors/lchuv/","title":"LChuv","text":"

The LCHuv color space is not registered in Color by default

Properties

Name: lchuv

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 220] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELChuv color space.

CIELuv is not an intuitive space to work with directly and instead is often converted to cylindrical coordinates with hues represented as degrees and a chroma and lightness channel. The shape of the color space doesn't really change, just how the colors are manipulated.

Learn about CIELChuv

"},{"location":"colors/lchuv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lchuv/#inputoutput","title":"Input/Output","text":"

As CIELChuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lchuv:

color(--lchuv l c h / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lchuv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lchuv l c h / a) form.

>>> Color(\"lchuv\", [53.237, 179.04, 12.177])\ncolor(--lchuv 53.237 179.04 12.177 / 1)\n>>> Color(\"lchuv\", [74.934, 105.26, 44.683]).to_string()\n'color(--lchuv 74.934 105.26 44.683)'\n
"},{"location":"colors/lchuv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lchuv import LChuv\n\nclass Color(Base): ...\n\nColor.register(LChuv())\n
"},{"location":"colors/luv/","title":"Luv","text":"

The Luv color space is not registered in Color by default

Properties

Name: luv

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] u [-215, 215] v [-215, 215]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELuv D65 color space.

CIELuv is similar to CIELab as they were both developed in 1976 as perceptually uniform color spaces, both are derived from the color experiments in 1931 that brought us the XYZ color space, and neither are truly perceptually uniform.

The difference between the two comes from their intent. CIELab attempted to create a space that aligned well with human vision. CIELuv, on the other hand, was designed to be an easier-to-compute transformation of the 1931 CIE XYZ color space.

CIELab is more commonly used in subtractive color applications (printed pages, dyes, etc.), while CIELuv is better suited in additive color applications such as display colorimetry (monitors, TVs, etc.).

Learn about CIELuv

"},{"location":"colors/luv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness u v"},{"location":"colors/luv/#inputoutput","title":"Input/Output","text":"

As CIELuv D65 is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --luv:

color(--luv l u v / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"luv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--luv l u v / a) form.

>>> Color(\"luv\", [53.237, 175.01, 37.765])\ncolor(--luv 53.237 175.01 37.765 / 1)\n>>> Color(\"luv\", [74.934, 74.839, 74.014]).to_string()\n'color(--luv 74.934 74.839 74.014)'\n
"},{"location":"colors/luv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.luv import Luv\n\nclass Color(Base): ...\n\nColor.register(Luv())\n
"},{"location":"colors/okhsl/","title":"Okhsl","text":"

The Okhsl color space is not registered in Color by default

Properties

Name: okhsl

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] l [0, 1]

Okhsl color space in 3D

Okhsl is a another color space created by Bj\u00f6rn Ottosson. It is based off his early work and leverages the Oklab color space. The aim was to create a color space that was better suited for being used in color pickers than the current HSL.

Learn about Okhsv

"},{"location":"colors/okhsl/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/okhsl/#inputoutput","title":"Input/Output","text":"

Okhsl is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --okhsl:

color(--okhsl h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"okhsl\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--okhsl h s l / a) form.

>>> Color(\"okhsl\", [29.234, 1, 0.56808], 1)\ncolor(--okhsl 29.234 1 0.56808 / 1)\n>>> Color(\"okhsl\", [70.67, 1, 0.75883], 1).to_string()\n'color(--okhsl 70.67 1 0.75883)'\n
"},{"location":"colors/okhsl/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.okhsl import Okhsl\n\nclass Color(Base): ...\n\nColor.register(Okhsl())\n
"},{"location":"colors/okhsv/","title":"Okhsv","text":"

The Okhsv color space is not registered in Color by default

Properties

Name: okhsv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] v [0, 1]

Okhsv color space in 3D

Okhsv is a color space created by Bj\u00f6rn Ottosson. It is based off his early work and leverages the Oklab color space. The aim was to create a color space that was better suited for being used in color pickers than the current HSV.

Learn about Okhsv

??? abstract \"ColorAide Details\"

"},{"location":"colors/okhsv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation v value"},{"location":"colors/okhsv/#inputoutput","title":"Input/Output","text":"

Okhsv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --okhsv:

color(--okhsv h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"okhsv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--okhsv h s l / a) form.

>>> Color(\"okhsv\", [29.234, 1, 1])\ncolor(--okhsv 29.234 1 1 / 1)\n>>> Color(\"okhsv\", [70.67, 1, 1]).to_string()\n'color(--okhsv 70.67 1 1)'\n
"},{"location":"colors/okhsv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.okhsv import Okhsv\n\nclass Color(Base): ...\n\nColor.register(Okhsv())\n
"},{"location":"colors/oklab/","title":"Oklab","text":"

The Oklab color space is registered in Color by default

Properties

Name: oklab

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 1] a [-0.4, 0.4] b [-0.4, 0.4]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the Oklab color space.

A new perceptual color space that claims to be simple to use, while doing a good job at predicting perceived lightness, chroma and hue. It is called the Oklab color space, because it is an OK Lab color space.

Learn about Oklab

"},{"location":"colors/oklab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/oklab/#inputoutput","title":"Input/Output","text":"

Oklab is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --oklab:

oklab(l a b / a)          // Oklab function\ncolor(--oklab l a b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"oklab\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--oklab l a b / a) form, but the default string output will be the oklab(l a b / a) form.

>>> Color(\"oklab\", [0.62796, 0.22486, 0.12585])\ncolor(--oklab 0.62796 0.22486 0.12585 / 1)\n>>> Color(\"oklab\", [0.79269, 0.05661, 0.16138]).to_string()\n'oklab(0.79269 0.05661 0.16138)'\n>>> Color(\"oklab\", [0.96798, -0.07137, 0.19857]).to_string(percent=True)\n'oklab(96.798% -17.842% 49.643%)'\n>>> Color(\"oklab\", [0.51975, -0.1403, 0.10768]).to_string(color=True)\n'color(--oklab 0.51975 -0.1403 0.10768)'\n
"},{"location":"colors/oklab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.oklab import Oklab\n\nclass Color(Base): ...\n\nColor.register(Oklab())\n
"},{"location":"colors/oklch/","title":"OkLCh","text":"

The OkLCh color space is registered in Color by default

Properties

Name: oklch

White Point: D65 / 2\u02da / 2\u02da

Coordinates:

Name Range* l [0, 1] c [0, 0.4] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the OkLCh color space.

OkLCh is the cylindrical form of Oklab.

Learn about OkLCh

"},{"location":"colors/oklch/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/oklch/#inputoutput","title":"Input/Output","text":"

Oklab is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --oklch:

oklch(l c h / a)          // OkLCh function\ncolor(--oklch l c h / a)  // Color function\n

The string representation of the color object will always default to the color(--oklch l c h / a) form, but the default string output will be the oklch(l a b / a) form.

>>> Color(\"oklch\", [0.62796, 0.25768, 29.234])\ncolor(--oklch 0.62796 0.25768 29.234 / 1)\n>>> Color(\"oklch\", [0.79269, 0.17103, 70.67]).to_string()\n'oklch(0.79269 0.17103 70.67)'\n>>> Color(\"oklch\", [0.96798, 0.21101, 109.77]).to_string(percent=True)\n'oklch(96.798% 52.753% 109.77)'\n>>> Color(\"oklch\", [0.51975, 0.17686, 142.5]).to_string(color=True)\n'color(--oklch 0.51975 0.17686 142.5)'\n
"},{"location":"colors/oklch/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.oklch import OkLCh\n\nclass Color(Base): ...\n\nColor.register(OkLCh())\n
"},{"location":"colors/orgb/","title":"oRGB","text":"

The oRGB color space is not registered in Color by default

Properties

Name: orgb

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 1] cyb [-1, 1] crg [-1, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

The sRGB gamut represented within the oRGB color space.

A new color model that is based on opponent color theory. Like HSV, it is designed specifically for computer graphics. However, it is also designed to work well for computational applications such as color transfer, where HSV falters. Despite being geared towards computation, oRGB's natural axes facilitate HSV-style color selection and manipulation. oRGB also allows for new applications such as a quantitative cool-to-warm metric, intuitive color manipulations and variations, and simple gamut mapping. This new color model strikes a balance between simplicity and the computational qualities of color spaces such as CIELab.

Learn more.

"},{"location":"colors/orgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l luma cyb crb"},{"location":"colors/orgb/#inputoutput","title":"Input/Output","text":"

The oRGB space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --orgb:

color(--orgb l cyb crb / a)  // Color function\n

The string representation of the color object and the default string output use the color(--orgb l cyb crg / a) form.

>>> Color(\"orgb\", [0.299, 0.00002, 0.99998])\ncolor(--orgb 0.299 2e-05 0.99998 / 1)\n>>> Color(\"orgb\", [0.67882, 0.75654, 0.4464]).to_string()\n'color(--orgb 0.67882 0.75654 0.4464)'\n
"},{"location":"colors/orgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.orgb import oRGB\n\nclass Color(Base): ...\n\nColor.register(oRGB())\n
"},{"location":"colors/prismatic/","title":"Prismatic","text":"

The oRGB color space is not registered in Color by default

Properties

Name: prismatic

White Point: D65 / 2\u02da

Coordinates:

Name Range l [0, 1] r [0, 1] g [0, 1] b [0, 1]

Prismatic Illustrations

The Prismatic model introduces a simple transform of the RGB color cube into a light/dark dimension and a 2D hue. The hue is a normalized (barycentric)triangle with pure red, green, and blue at the vertices, often called the Maxwell Color Triangle. Each cross section of the space is the same barycentric triangle, and the light/dark dimension runs zero to one for each hue so the whole color volume takes the form of a prism.

Learn more.

"},{"location":"colors/prismatic/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness r red g green b blue"},{"location":"colors/prismatic/#inputoutput","title":"Input/Output","text":"

The Prismatic space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --prismatic:

color(--prismatic l r g b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--prismatic l r g b / a) form.

>>> Color(\"prismatic\", [1, 1, 0, 0])\ncolor(--prismatic 1 1 0 0 / 1)\n>>> Color(\"prismatic\", [1, 0.60714, 0.39286, 0], 1).to_string()\n'color(--prismatic 1 0.60714 0.39286 0)'\n
"},{"location":"colors/prismatic/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.prismatic import Prismatic\n\nclass Color(Base): ...\n\nColor.register(Prismatic())\n
"},{"location":"colors/prophoto_rgb/","title":"ProPhoto","text":"

The ProPhoto color space is registered in Color by default

Properties

Name: prophoto-rgb

White Point: D50 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 ProPhoto RGB Chromaticities

The ProPhoto RGB color space, also known as ROMM RGB (Reference Output Medium Metric), is an output referred RGB color space developed by Kodak. It offers an especially large gamut designed for use with photographic output in mind. The ProPhoto RGB color space encompasses over 90% of possible surface colors in the CIE L*a*b* color space, and 100% of likely occurring real-world surface colors documented by Pointer in 1980.

Learn about ProPhoto

"},{"location":"colors/prophoto_rgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/prophoto_rgb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(prophoto-rgb r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"prophoto-rgb\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(prophoto-rgb r g b / a) form.

>>> Color(\"prophoto-rgb\", [0.78951, 0.62329, 0.21172], 1)\ncolor(prophoto-rgb 0.78951 0.62329 0.21172 / 1)\n>>> Color(\"prophoto-rgb\", [0.70225, 0.27572, 0.10355]).to_string()\n'color(prophoto-rgb 0.70225 0.27572 0.10355)'\n
"},{"location":"colors/prophoto_rgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.prophoto_rgb import ProPhotoRGB\n\nclass Color(Base): ...\n\nColor.register(ProPhotoRGB())\n
"},{"location":"colors/prophoto_rgb_linear/","title":"Linear ProPhoto","text":"

The Linear ProPhoto color space is registered in Color by default

Properties

Name: prophoto-rgb-linear

White Point: D50 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 ProPhoto RGB Chromaticities

The Linear ProPhoto space is the same as ProPhoto except that the transfer function is linear-light (there is no gamma-encoding).

Learn about ProPhoto

"},{"location":"colors/prophoto_rgb_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/prophoto_rgb_linear/#inputoutput","title":"Input/Output","text":"

Linear ProPhoto is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --prophoto-rgb-linear:

color(--prophoto-rgb-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"prophoto-rgb-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--prophoto-rgb-linear r g b / a) form.

>>> Color(\"prophoto-rgb-linear\", [0.52928, 0.09837, 0.01688])\ncolor(--prophoto-rgb-linear 0.52928 0.09837 0.01688 / 1)\n>>> Color(\"prophoto-rgb-linear\", [0.6535, 0.42702, 0.06115]).to_string()\n'color(--prophoto-rgb-linear 0.6535 0.42702 0.06115)'\n
"},{"location":"colors/prophoto_rgb_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.prophoto_rgb_linear import ProPhotoRGBLinear\n\nclass Color(Base): ...\n\nColor.register(ProPhotoRGBLinear())\n
"},{"location":"colors/rec2020/","title":"REC. 2020","text":"

The Rec. 2020 color space is registered in Color by default

Properties

Name: rec2020

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Rec. 2020 Chromaticities

ITU-R Recommendation BT.2020, more commonly known by the abbreviations Rec. 2020 or BT.2020, defines various aspects of ultra-high-definition television (UHDTV) with standard dynamic range (SDR) and wide color gamut (WCG), including picture resolutions, frame rates with progressive scan, bit depths, color primaries, RGB and luma-chroma color representations, chroma subsamplings, and an opto-electronic transfer function. The color is used in 4k and 8k UHDTV.

Learn about REC.2020

"},{"location":"colors/rec2020/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2020/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(rec2020 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2020\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(rec2020 r g b / a) form.

>>> Color(\"rec2020\", [0.79198, 0.23098, 0.07376])\ncolor(rec2020 0.79198 0.23098 0.07376 / 1)\n>>> Color(\"rec2020\", [0.86727, 0.64078, 0.18496]).to_string()\n'color(rec2020 0.86727 0.64078 0.18496)'\n
"},{"location":"colors/rec2020/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2020 import Rec2020\n\nclass Color(Base): ...\n\nColor.register(Rec2020())\n
"},{"location":"colors/rec2020_linear/","title":"Linear REC. 2020","text":"

The Linear Rec. 2020 color space is registered in Color by default

Properties

Name: rec2020-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Rec. 2020 Chromaticities

The Linear Rec. 2020 space is the same as Rec. 2020 except that the transfer function is linear-light (there is no gamma-encoding).

Learn about REC.2020

"},{"location":"colors/rec2020_linear/#channel-aliases","title":"Channel Aliases:**","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2020_linear/#inputoutput","title":"Input/Output","text":"

Linear Rec. 2020 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec2020-linear:

color(--rec2020-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2020-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec2020-linear r g b / a) form.

>>> Color(\"rec2020-linear\", [0.6274, 0.0691, 0.01639])\ncolor(--rec2020-linear 0.6274 0.0691 0.01639 / 1)\n>>> Color(\"rec2020-linear\", [0.7513, 0.41509, 0.04951]).to_string()\n'color(--rec2020-linear 0.7513 0.41509 0.04951)'\n
"},{"location":"colors/rec2020_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2020_linear import Rec2020Linear\n\nclass Color(Base): ...\n\nColor.register(Rec2020Linear())\n
"},{"location":"colors/rec2100_hlg/","title":"REC. 2100 HLG","text":"

The Rec. 2100 HLG is not registered in Color by default

Properties

Name: rec2100-hlg

White Point: D65 / 2\u02da

Coordinates:

Name Range r [0, 1] g [0, 1] b [0, 1]

CIE 1931 xy Chromaticity \u2013 Rec. 2100 Chromaticities (Same as Rec. 2020)

BT.2100, more commonly known by the abbreviations Rec. 2100 or BT.2100, introduced high-dynamic-range television (HDR-TV) by recommending the use of the perceptual quantizer (PQ) or hybrid log\u2013gamma (HLG) transfer functions instead of the traditional \"gamma\" previously used for SDR-TV. Rec. 2100 HLG specifically uses the hybrid log-gamma transfer function.

The actual gamut of Rec. 2100 uses the same wide color gamut of Rec. 2020, but the color space itself supports an HDR range.

Learn about REC.2100

"},{"location":"colors/rec2100_hlg/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2100_hlg/#inputoutput","title":"Input/Output","text":"

Rec. 2100 HLG is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec2100-hlg:

color(--rec2100-hlg r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2100-hlg\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec2100-hlg r g b / a) form.

>>> Color(\"rec2100-hlg\", [0.65587, 0.23436, 0.11415], 1)\ncolor(--rec2100-hlg 0.65587 0.23436 0.11415 / 1)\n>>> Color(\"rec2100-hlg\", [0.69294, 0.56608, 0.19838], 1).to_string()\n'color(--rec2100-hlg 0.69294 0.56608 0.19838)'\n
"},{"location":"colors/rec2100_hlg/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2100_hlg import Rec2100HLG\n\nclass Color(Base): ...\n\nColor.register(Rec2100HLG())\n
"},{"location":"colors/rec2100_pq/","title":"REC. 2100 PQ","text":"

The Rec. 2100 PQ is not registered in Color by default

Properties

Name: rec2100-pq

White Point: D65 / 2\u02da

Coordinates:

Name Range r [0, 1] g [0, 1] b [0, 1]

CIE 1931 xy Chromaticity \u2013 Rec. 2100 Chromaticities (Same as Rec. 2020)

BT.2100, more commonly known by the abbreviations Rec. 2100 or BT.2100, introduced high-dynamic-range television (HDR-TV) by recommending the use of the perceptual quantizer (PQ) or hybrid log\u2013gamma (HLG) transfer functions instead of the traditional \"gamma\" previously used for SDR-TV. Rec. 2100 PQ specifically uses the perceptual quantizer.

The actual gamut of Rec. 2100 uses the same wide color gamut of Rec. 2020, but the color space itself supports an HDR range.

Learn about REC.2100

"},{"location":"colors/rec2100_pq/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2100_pq/#inputoutput","title":"Input/Output","text":"

Rec. 2100 PQ is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec2100-pq:

color(--rec2100-pq r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2100-pq\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec2100-pq r g b / a) form.

>>> Color(\"rec2100-pq\", [0.53255, 0.32702, 0.22007], 1)\ncolor(--rec2100-pq 0.53255 0.32702 0.22007 / 1)\n>>> Color(\"rec2100-pq\", [0.55101, 0.49099, 0.30009], 1).to_string()\n'color(--rec2100-pq 0.55101 0.49099 0.30009)'\n
"},{"location":"colors/rec2100_pq/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2100_pq import Rec2100PQ\n\nclass Color(Base): ...\n\nColor.register(Rec2100PQ())\n
"},{"location":"colors/rec709/","title":"Rec. 709","text":"

New 2.4

The Rec. 709 color space is not registered in Color by default

Properties

Name: rec709

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Rec. 709 Chromaticities

Rec. 709 (also known as Rec.709, BT.709, and ITU 709) is a standard developed by ITU-R for image encoding and signal characteristics of high-definition television (HDTV). The color space is similar to sRGB in the fact that the primary chromaticities and white points are identical, the difference is the transfer function that more resembles Rec. 2020, though the precision of the constants are at 10 bit instead of 12 bit or greater.

Learn about Rec. 709

"},{"location":"colors/rec709/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec709/#inputoutput","title":"Input/Output","text":"

Rec. 709 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec709:

color(--rec709 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec709\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec709 r g b / a) form.

>>> Color(\"rec709\", [1, 0, 0], 1)\ncolor(--rec709 1 0 0 / 1)\n>>> Color(\"rec709\", [1, 0.60879, 0], 1).to_string()\n'color(--rec709 1 0.60879 0)'\n
"},{"location":"colors/rec709/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec709 import Rec709\n\nclass Color(Base): ...\n\nColor.register(Rec709())\n
"},{"location":"colors/rlab/","title":"RLAB","text":"

The RLAB color space is not registered in Color by default

Properties

Name: rlab

White Point: D65 / 2\u02da

Coordinates:

Name Range l [0, 100] a [-125, 125] b [-125, 125]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the RLAB color space.

The RLAB color-appearance space was developed by Fairchild and Berns for cross-media color reproduction applications in which images are reproduced with differing white points, luminance levels, and/or surrounds.

ColorAide provides RLAB by default with average surround, and discounting set to \"hard copy\" (or full discounting of the illuminant). It is also configured to use an absolute adapting luminance of 318 cd/m2.

Learn more.

"},{"location":"colors/rlab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/rlab/#inputoutput","title":"Input/Output","text":"

The RLAB space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --rlab:

color(--rlab l a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--rlab l a b / a) form.

>>> Color(\"rlab\", [51.012, 79.742, 57.26])\ncolor(--rlab 51.012 79.742 57.26 / 1)\n>>> Color(\"rlab\", [72.793, 25.151, 74.11]).to_string()\n'color(--rlab 72.793 25.151 74.11)'\n
"},{"location":"colors/rlab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rlab import RLAB\n\nclass Color(Base): ...\n\nColor.register(RLAB())\n
"},{"location":"colors/ryb/","title":"RYB","text":"

The RYB color space is not registered in Color by default

Properties

Name: ryb

White Point: D65 / 2\u02da

Coordinates:

Name Range r [0, 1] y [0, 1] b [0, 1]

The RYB color wheel.

The RYB color model is a subtractive color model in which red, yellow and blue pigments or dyes are added together in various ways to reproduce different colors. RYB is the classical way of thinking about colors. While in schools it is often still taught that red, yellow, and blue are the primary colors (for pigments), modern wisdom shows that these \"primaries\" are not sufficient. Spaces like CMYK which use cyan, magenta and yellow can create a much wider array of colors, even red, yellow, and blue. We've also since learned that the primary colors of light (which operates in an additive color space) are red, green, and blue.

Even though in modern day electronic screens and printing we rely on color models such as RGB and CMYK, RYB still holds a special place with artists. Color harmony is often used as the model when talking about color harmonies.

There is no standard RYB color model. No standard primaries. The RYB model that ColorAide implements is based on the work of Nathan Gossett and Baoquan Chen at the University of Minnesota at Twin Cities. Their paper devised an approach of using trilinear interpolation to create a transform from RYB to sRGB. The bases of the model uses colors similar to Johannes Itten's color wheel (which we showed an example of above), but in all the literature, there are variants of the same color wheel and no precise color codes.

ColorAide implements Gossett and Chen's algorithm to convert from RYB to sRGB, but also uses an algorithm that employs Newton's method to approximates the reverse transform. Additionally, their paper implements an easing function that biases the color transforms to corners of the cube. While we've also implemented this behavior to be true to the paper, the color spaces does not utilize this biasing by default, but we provide an alternative version does. The biasing does not improve the color space, but limits the intermediate colors, essentially clumping them near the corners of the RYB color cube which was a requirement for the types of data representations that they were performing.

Notice how the colors in the \"biased\" RYB are more concentrated at the corners while in the normal RYB they blend more.

RYBRYB Biased

While precisely emulating paint mixing was not entirely the focus, the RYB space does do a better job than other color spaces.

>>> Color.interpolate(['color(--ryb 0 0 1)', 'color(--ryb 0 1 0)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5be2f10>\n>>> Color.interpolate(['color(--ryb 1 0 0)', 'color(--ryb 0 1 0)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5a248d0>\n>>> Color.interpolate(['color(--ryb 1 0 1)', 'color(--ryb 0 1 0)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab7108ad0>\n

It should also be noted that when mixing all the colors, you do not get black, but a muddy brown, much like with paint. To be precise, the color black is not defined within this color space.

>>> Color.interpolate(['color(--ryb 0 0 0)', 'color(--ryb 1 1 1)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab650dd10>\n>>> Color.interpolate(['color(--ryb 0 0 1)', 'color(--ryb 1 1 1)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5db4c10>\n

It should be noted that the RYB model does a great job at translating colors within the RYB color gamut, but translation of colors outside the gamut will have poor conversions.

Learn more.

"},{"location":"colors/ryb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red y yellow b blue"},{"location":"colors/ryb/#inputoutput","title":"Input/Output","text":"

RYB is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ryb:

color(--ryb r y b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--ryb r y b / a) form.

>>> Color(\"ryb\", [1, 0, 0])\ncolor(--ryb 1 0 0 / 1)\n>>> Color(\"ryb\", [1, 1, 0]).to_string()\n'color(--ryb 1 1 0)'\n
"},{"location":"colors/ryb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ryb import RYB\n\nclass Color(Base): ...\n\nColor.register(RYB())\n
"},{"location":"colors/ryb/#ryb-biased","title":"RYB Biased","text":"

The biased RYB from Gosset and Chen's paper can be used via the ryb-biased color space, CSS custom name --ryb-biased. It has the same channel ranges as ryb, but conversions will be biased to the corners of the RYB cube.

>>> Color.interpolate(Color('ryb', [1, 0, 0]).harmony('wheel', space='ryb'), space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5d5d6d0>\n>>> Color.interpolate(Color('ryb-biased', [1, 0, 0]).harmony('wheel', space='ryb-biased'), space='ryb-biased')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x7fbab5b543d0>\n

Space can be registered via:

from coloraide import Color as Base\nfrom coloraide.spaces.ryb import RYBBiased\n\nclass Color(Base): ...\n\nColor.register(RYBBiased())\n
"},{"location":"colors/srgb/","title":"sRGB","text":"

The sRGB color space is registered in Color by default

Properties

Name: srgb

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 sRGB Chromaticities

The sRGB space is a standard RGB (red, green, blue) color space that HP and Microsoft created cooperatively in 1996 to use on monitors, printers, and the Web. sRGB stands for \"Standard RGB\". It is the most widely used color space and is supported by most operating systems, software programs, monitors, and printers.

Learn about sRGB

"},{"location":"colors/srgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/srgb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

black                  // Color name\n#RRGGBBAA              // Hex\nrgb(r g b / a)         // RGB function\nrgb(r, g, b)           // Legacy RGB Function\nrgba(r, g, b, a)       // Legacy RGBA function\ncolor(srgb r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"srgb\", [0, 0, 0], 1)\n
The string representation of the color object will always default to the color(srgb r g b / a) form, but the default string output will be the rgb(r g b / a) form.

>>> Color('red').to_string()\n'rgb(255 0 0)'\n>>> Color('orange').to_string(comma=True)\n'rgb(255, 165, 0)'\n>>> Color('yellow').to_string(percent=True)\n'rgb(100% 100% 0%)'\n>>> Color('green').to_string(names=True)\n'green'\n>>> Color('blue').to_string(hex=True)\n'#0000ff'\n>>> Color('indigo').to_string(color=True)\n'color(srgb 0.29412 0 0.5098)'\n
"},{"location":"colors/srgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.srgb.css import sRGB\n\nclass Color(Base): ...\n\nColor.register(sRGB())\n
"},{"location":"colors/srgb_linear/","title":"Linear sRGB","text":"

The Linear sRGB color space is registered in Color by default

Properties

Name: srgb-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 sRGB Chromaticities

The sRGB Linear space is the same as sRGB except that the transfer function is linear-light (there is no gamma-encoding).

Learn about sRGB

"},{"location":"colors/srgb_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/srgb_linear/#inputsoutput","title":"Inputs/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(srgb-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"srgb-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(srgb-linear r g b / a) form.

>>> Color(\"srgb\", [1, 0, 0])\ncolor(srgb 1 0 0 / 1)\n>>> Color(\"srgb\", [1, 0.37626, 0]).to_string()\n'rgb(255 95.946 0)'\n
"},{"location":"colors/srgb_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.srgb_linear import sRGBLinear\n\nclass Color(Base): ...\n\nColor.register(sRGB())\n
"},{"location":"colors/ucs/","title":"CIE 1960 UCS","text":"

New 2.4

The CIE 1960 UCS color space is not registered in Color by default

Properties

Name: ucs

White Point: D65 / 2\u02da

Coordinates:

Name Range* u [0.0, 1.0] v [0.0, 1.0] w [0.0, 1.0]

* Space is not bound to the range and is used to define percentage inputs/outputs.

The sRGB gamut represented within the CIE 1960 UCS color space.

The CIE 1960 color space (\"CIE 1960 UCS\", variously expanded Uniform Color Space, Uniform Color Scale, Uniform Chromaticity Scale, Uniform Chromaticity Space) is another name for the (u, v) chromaticity space devised by David MacAdam. The color space is implemented using the relation between this space and the XYZ space as coordinates U, V, and W.

Learn more.

"},{"location":"colors/ucs/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases u v w"},{"location":"colors/ucs/#inputoutput","title":"Input/Output","text":"

The UCS space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ucs:

color(--ucs u v w / a)  // Color function\n

The string representation of the color object and the default string output use the color(--ucs u v w / a) form.

>>> Color(\"ucs\", [0.27493, 0.21264, 0.12243])\ncolor(--ucs 0.27493 0.21264 0.12243 / 1)\n>>> Color(\"ucs\", [0.36462, 0.48173, 0.48122]).to_string()\n'color(--ucs 0.36462 0.48173 0.48122)'\n
"},{"location":"colors/ucs/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ucs import UCS\n\nclass Color(Base): ...\n\nColor.register(UCS())\n
"},{"location":"colors/xyb/","title":"XYB","text":"

The XYB color space is not registered in Color by default

Properties

Name: xyb

White Point: D65 / 2\u02da

Coordinates:

Name Range* x [-0.05, 0.05] y [0.0, 0.845] b [-0.45, 0.45]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the XYB color space.

XYB is a color space that was designed for use with the JPEG XL Image Coding System. It is an LMS-based color model inspired by the human visual system, facilitating perceptually uniform quantization. It uses a gamma of 3 for computationally efficient decoding.

Chroma/Luma Adjustments

Per the creator, the default subtracts the Y component from the B component which makes Y function as lightness and X and Y like Lab a and b. When X=Y=0, the color is achromatic. You may find other implementations do not do this only because it is not documented well.

Learn more.

"},{"location":"colors/xyb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y b

Inputs

The XYB space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --xyb:

color(--xyb x y b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--xyb x y b / a) form.

>>> Color(\"xyb\", [0.0281, 0.48819, 0.01157])\ncolor(--xyb 0.0281 0.48819 0.01157 / 1)\n>>> Color(\"xyb\", [0.01132, 0.64596, -0.10359]).to_string()\n'color(--xyb 0.01132 0.64596 -0.10359)'\n
"},{"location":"colors/xyb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyb import XYB\n\nclass Color(Base): ...\n\nColor.register(XYB())\n
"},{"location":"colors/xyy/","title":"xyY","text":"

The xyY color space is not registered in Color by default

Properties

Name: xyy

White Point: D65 / 2\u02da

Coordinates:

Name Range* x [0, 1] y [0, 1] Y [0, 1]

* Space is not bound to the range and is used to define percentage inputs/outputs.

The sRGB gamut represented within the xyY color space.

A derivative of the CIE 1931 XYZ space, the CIE xyY color space, is often used as a way to graphically present the chromaticity of colors.

Tip

The color space, as implemented, is relative to the D65 white point, meaning it is created from XYZ D65. If colors are needed relative to different white points, the color space can be subclassed. If proper chromaticity coordinates are desired for a given color, you can checkout the API for chromaticity coordinates.

Learn more.

"},{"location":"colors/xyy/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y Y"},{"location":"colors/xyy/#inputoutput","title":"Input/Output","text":"

The xyY space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --xyy:

color(--xyy x y Y / a)  // Color function\n

The string representation of the color object and the default string output use the color(--xyy x y Y / a) form.

>>> Color(\"xyy\", [0.64, 0.33, 0.21264])\ncolor(--xyy 0.64 0.33 0.21264 / 1)\n>>> Color(\"xyy\", [0.50047, 0.4408, 0.48173]).to_string()\n'color(--xyy 0.50047 0.4408 0.48173)'\n
"},{"location":"colors/xyy/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyy import xyY\n\nclass Color(Base): ...\n\nColor.register(xyY())\n
"},{"location":"colors/xyz_d50/","title":"XYZ D50","text":"

The XYZ D50 color space is registered in Color by default

Properties

Name: xyz-d50

White Point: D50 / 2\u02da

Coordinates:

Name Range* x [0, 1] y [0, 1] z [0, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the XYZ D50 color space.

XYZ D50 is the same as XYZ D65 except it uses a D50 white point.

Learn about XYZ

"},{"location":"colors/xyz_d50/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y z"},{"location":"colors/xyz_d50/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(xyz-d50 x y z / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"xyz-d50\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(xyz x y z / a) form.

>>> Color(\"xyz-d50\", [0.43607, 0.22249, 0.01392])\ncolor(xyz-d50 0.43607 0.22249 0.01392 / 1)\n>>> Color(\"xyz-d50\", [0.58098, 0.49223, 0.05045]).to_string()\n'color(xyz-d50 0.58098 0.49223 0.05045)'\n
"},{"location":"colors/xyz_d50/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyz_d50 import XYZD50\n\nclass Color(Base): ...\n\nColor.register(XYZD50())\n
"},{"location":"colors/xyz_d65/","title":"XYZ D65","text":"

The XYZ D65 color space is registered in Color by default

Properties

Name: xyz-d65

White Point: D65 / 2\u02da

Coordinates:

Name Range* x [0, 1] y [0, 1] z [0, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the XYZ D65 color space.

The CIE 1931 RGB color space and CIE 1931 XYZ color space were created by the International Commission on Illumination (CIE) in 1931. They resulted from a series of experiments done in the late 1920s by William David Wright using ten observers and John Guild using seven observers. The experimental results were combined into the specification of the CIE RGB color space, from which the CIE XYZ color space was derived. The CIE 1931 color spaces are the first defined quantitative links between distributions of wavelengths in the electromagnetic visible spectrum, and physiologically perceived colors in human color vision.

Learn about XYZ

"},{"location":"colors/xyz_d65/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y z"},{"location":"colors/xyz_d65/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats use the color() format with either xyz-d65 or xyz as the identifier with the latter being an alias of the former.

color(xyz x y z / a)      // Color function\ncolor(xyz-d65 x y z / a)  // Color function alternate name\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"xyz-d65\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(xyz-d65 x y z / a) form.

>>> Color(\"xyz-d65\", [0.41239, 0.21264, 0.01933])\ncolor(xyz-d65 0.41239 0.21264 0.01933 / 1)\n>>> Color(\"xyz-d65\", [0.54694, 0.48173, 0.06418]).to_string()\n'color(xyz-d65 0.54694 0.48173 0.06418)'\n
"},{"location":"colors/xyz_d65/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyz_d65 import XYZD65\n\nclass Color(Base): ...\n\nColor.register(XYZD65())\n
"},{"location":"demos/","title":"ColorAide Demos","text":""},{"location":"demos/#online-color-picker","title":"Online Color Picker","text":"

Use ColorAide to pick a color in any of the color spaces available. ACES color spaces have been arbitrarily limited has they have ginormous headroom.

Try it out

"},{"location":"demos/#interactive-3d-color-space-models","title":"Interactive 3D Color Space Models","text":"

Generate interactive 3D color models in the browser using ColorAide and Plotly! Most color spaces are supported, but color spaces with more than 3 color components (not including alpha) are not supported. Colors can be generated in a number of color gamuts, though a few models are restricted to their own color space for practical reasons.

Try it out

"},{"location":"plugins/","title":"ColorAide Plugins","text":"

ColorAide implements extendable portions of the Color object as plugins. This makes adding things such as new \u2206E methods or even new color spaces quite easy. Currently, ColorAide implements the following areas as plugins:

  • \u2206E methods
  • Fit/Gamut mapping
  • Chromatic adaptation
  • Filters
  • Contrast
  • Color spaces
  • Interpolation
  • CCT

While these documents will touch on each plugin, looking at the source code will provide a better view on how plugins are actually used as all functionality for all of these categories are implemented as plugins in ColorAide.

"},{"location":"plugins/cat/","title":"Chromatic Adaptation","text":""},{"location":"plugins/cat/#description","title":"Description","text":"

CAT plugins chromatically adapt a given XYZ coordinate from its current reference white point to a new desired white point. This is useful during conversion when one color space is converted to another color space that uses a difference reference white.

"},{"location":"plugins/cat/#plugin-class","title":"Plugin Class","text":"

Plugins are are created by subclassing coloraide.cat.CAT.

class CAT(Plugin, metaclass=ABCMeta):\n\"\"\"Chromatic adaptation.\"\"\"\n\n    NAME = \"\"\n\n    @abstractmethod\n    def adapt(self, w1: Tuple[float, float], w2: Tuple[float, float], xyz: VectorLike) -> Vector:\n\"\"\"Adapt a given XYZ color using the provided white points.\"\"\"\n

Once registered, the plugin can then be used via chromatic_adaptation by passing its NAME via the the method along two white points (as XYZ values): w1 as the current white point and w2 as the target white point.

It should be noted that chromatic_adaptation is not usually directly used by the user, so a more likely approach is to override the DELTA_E parameter of a subclassed Color object to specify the plugin as the default for chromatic adaptation.

"},{"location":"plugins/cat/#von-kries-cat","title":"Von Kries CAT","text":"

Currently, ColorAide only ships with Von Kries based adaptation methods. If it is desired to create a Von Kries based plugin, it is recommended to subclass the VonKries class which is based on CAT. When subclassing a VonKries based CAT, the NAME and a MATRIX must be provided. The general calculations related to the source and target white point, will automatically be calculated and an appropriate matrix and inverted matrix will be returned to perform the adaptation without any additional logic.

class Bradford(VonKries):\n\"\"\"\n    Bradford CAT.\n\n    http://brucelindbloom.com/Eqn_ChromAdapt.html\n    https://hrcak.srce.hr/file/95370\n    \"\"\"\n\n    NAME = \"bradford\"\n\n    MATRIX = [\n        [0.8951000, 0.2664000, -0.1614000],\n        [-0.7502000, 1.7135000, 0.0367000],\n        [0.0389000, -0.0685000, 1.0296000]\n    ]\n
"},{"location":"plugins/cct/","title":"CCT","text":""},{"location":"plugins/cct/#description","title":"Description","text":"

CCT plugins allow you to calculate a color from a CCT and \u2206uv or calculate a CCT and \u2206uv from a color.

"},{"location":"plugins/cct/#plugin-class","title":"Plugin Class","text":"
class CCT(Plugin, metaclass=ABCMeta):\n\"\"\"Delta E plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def to_cct(self, color: Color, **kwargs: Any) -> Vector:\n\"\"\"Calculate a color's CCT.\"\"\"\n\n    @abstractmethod\n    def from_cct(\n        self,\n        color: type[Color],\n        space: str,\n        kelvin: float,\n        duv: float,\n        scale: bool,\n        scale_space: str | None,\n        **kwargs: Any\n    ) -> Color:\n\"\"\"Calculate a color that satisfies the CCT.\"\"\"\n

Once registered, the plugin can then be used via the cct() or blackbody() methods by passing its NAME via the method parameter.

Color('orange').cct(method=NAME, **kwargs)\nColor.blackbody(space, kelvin, duv, method=NAME, **kwargs)\n

If you'd like the user to configure the plugin on registration, you can define an __init__ method. To register the plugin, just the register() method.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/contrast/","title":"Contrast","text":""},{"location":"plugins/contrast/#description","title":"Description","text":"

Contrast returns a numerical value that is meant to determine how much visual contrast exists between two colors. While the current default is agnostic to ordering of the colors, some algorithms can be sensitive to order.

"},{"location":"plugins/contrast/#plugin-class","title":"Plugin Class","text":"
class ColorContrast(Plugin, metaclass=ABCMeta):\n\"\"\"Color contrast plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def contrast(self, color1: 'Color', color2: 'Color', **kwargs: Any) -> float:\n\"\"\"Get the contrast of the two provided colors.\"\"\"\n

Once registered, the plugin can then be used via contrast by passing its NAME via the method parameter along with a secondary color (color2) representing the background color. The calling color (color1) will be considered the text color. Any additional key word arguments can be specified to override default behavior. The \"contrast\" will be returned as a float.

color1.contrast(color2, method=NAME, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/delta_e/","title":"Delta E","text":""},{"location":"plugins/delta_e/#description","title":"Description","text":"

\u2206E plugins allow for getting color differences with different methods. ColorAide provides a number of methods by default which are documented under Color Distance and Delta E. All of the default \u2206E methods are provided as plugins, and users can create their own as well.

"},{"location":"plugins/delta_e/#plugin-class","title":"Plugin Class","text":"

\u2206E plugins are subclassed from coloraide.distance.DeltaE.

class DeltaE(Plugin, metaclass=ABCMeta):\n\"\"\"Delta E plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def distance(self, color: 'Color', sample: 'Color', **kwargs: Any) -> float:\n\"\"\"Get distance between color and sample.\"\"\"\n

Once registered, the plugin can then be used via delta_e by passing its NAME via the method parameter along with any additional key word arguments to override default behavior. The return will be a float indicating the distance.

color.delta_e(sample, method=NAME, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/filter/","title":"Filters","text":""},{"location":"plugins/filter/#description","title":"Description","text":"

Filter plugins allow you to apply a filter to a given color, altering its appearance.

"},{"location":"plugins/filter/#plugin-class","title":"Plugin Class","text":"
class Filter(Plugin, metaclass=ABCMeta):\n\"\"\"Filter plugin.\"\"\"\n\n    NAME = \"\"\n    DEFAULT_SPACE = 'srgb-linear'\n    ALLOWED_SPACES = ('srgb-linear', 'srgb')\n\n    @abstractmethod\n    def filter(self, color: 'Color', amount: float | None, **kwargs: Any) -> None:\n\"\"\"Filter the given color.\"\"\"\n

Once registered, the plugin can then be used via filter by passing its NAME via the method parameter along with the amount specifying to what magnitude the filter is applied. Any additional key word arguments also be specified to allow for overriding default behaviors. The current color will then be altered by the filter.

DEFAULT_SPACE describes the default color space under which the filter is applied, while ALLOWED_SPACES defines optional, allowed spaces that can be specified. Color space conversion is handled before passing the color to the plugin.

color.filter(NAME, amount, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/fit/","title":"Fit/Gamut Mapping","text":""},{"location":"plugins/fit/#description","title":"Description","text":"

Fit plugins (or gamut mapping plugins) allow for mapping an out of gamut color to be within the current color space's gamut. All default gamut mapping methods provided by ColorAide are provided via plugins.

"},{"location":"plugins/fit/#plugin-class","title":"Plugin Class","text":"

Plugins are are created by subclassing coloraide.gamut.Fit.

class Fit(Plugin, metaclass=ABCMeta):\n\"\"\"Fit plugin class.\"\"\"\n\"\"\"Fit plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def fit(self, color: 'Color', **kwargs) -> None:\n\"\"\"Get coordinates of the new gamut mapped color.\"\"\"\n

Once registered, the plugin can then be used via fit (and in some places like convert) by passing its NAME via the method parameter along with any additional key word arguments to override default behavior. The current color will be gamut mapped accordingly.

color.fit(method=NAME, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n

Reserved Name

clip is a special, reserved name and the associated plugin cannot be overridden. Another clip plugin can be written, but it cannot override the original.

"},{"location":"plugins/interpolate/","title":"Interpolation","text":""},{"location":"plugins/interpolate/#description","title":"Description","text":"

Interpolation plugins allow for interpolation between one or more colors. All interpolation in ColorAide is provided via plugins.

"},{"location":"plugins/interpolate/#plugin-class","title":"Plugin Class","text":"

Plugins are are created by subclassing coloraide.interpolate.Interpolate.

class Interpolate(Plugin, metaclass=ABCMeta):\n\"\"\"Interpolation plugin.\"\"\"\n\n    NAME = \"\"\n\n    @abstractmethod\n    def interpolator(\n        self,\n        coordinates: List[Vector],\n        channel_names: Sequence[str],\n        create: Type['Color'],\n        easings: List[Callable[..., float] | None],\n        stops: Dict[int, float],\n        space: str,\n        out_space: str,\n        progress: Union[Mapping[str, Callable[..., float]], Callable[..., float]] | None,\n        premultiplied: bool,\n        extrapolate: bool = False,\n        domain: List[float] | None = None,\n        **kwargs: Any\n    ) -> Interpolator:\n\"\"\"Get the interpolator object.\"\"\"\n

Once registered, the plugin can then be used via interpolate, steps, or mix by passing its NAME via the method parameter along with any additional key word arguments to override default behavior. An Interpolator object will be returned which allows for interpolating between the given list of colors.

color.interpolate(colors, method=NAME)\n

In general, the Interpolate plugin is mainly a wrapper to ensure the interpolation setup uses an appropriate Interpolator object which does the actual work. An interpolation plugin should derive their Interpolator class from coloraide.interpolate.Interpolator. While we won't show all the methods of the class, we will show the two functions that must be defined.

class Interpolator(metaclass=ABCMeta):\n\"\"\"Interpolator.\"\"\"\n\n    @abstractmethod\n    def setup(self) --> None:\n\"\"\"Setup.\"\"\"\n\n    @abstractmethod\n    def interpolate(\n        self,\n        point: float,\n        index: int,\n    ) -> Vector:\n\"\"\"Interpolate.\"\"\"\n

__init__ usually shouldn't be changed as it handles the general initialization for all interpolations. It could be extended with super() to set some class specific initialization flags for specific features, but generally, interpolation specific setup logic should be done in Interpolator.setup(). This is often used to restructure data points to a more agreeable format for a given interpolation method, precalculate premultiplication, or normalize undefined values when required. There are cases where ColorAide may update data points and re-call setup() directly. As an example. setup() can be recalled when a continuous interpolation is converted to a discretized one.

Interpolator.interpolate is where the actual interpolation takes place. It expects an index from [1, n], the index referencing the second color out of the two colors to be interpolated. point, usually between [0, 1], represents the point on the interpolation line between the two colors under evaluation.

While point is usually a value between [0, 1], where 0 would be the color stop to the left, and 1 would be the color stop to the right, if point exceeds the range of [0, 1], it can be assumed that the request is on the far left or far right of all color stops and could be beyond the absolute range of the entire color interpolation chain.

By default, extrapolation is disabled between all colors in an interpolation chain, and any point that exceeds the range of [0, 1], before easing functions are applied, will be clamped. If extrapolate is set to $!py True, the points will not be clamped between any colors, in which case, it is an easing functions responsibility to ensure a value between 0 or 1 if extreme values are not desired.

Check out the source code to see some example plugins.

"},{"location":"plugins/space/","title":"Color Space","text":""},{"location":"plugins/space/#description","title":"Description","text":"

All color spaces supported by ColorAide are specified via color space plugins. These Space objects specify color channel properties, gamut bounds, input matching/parsing logic, string output logic, conversion to and from a specified base color, etc.

Color space plugins are a little more complex compared to Delta E, Fit, and other plugins.

"},{"location":"plugins/space/#plugin-class","title":"Plugin Class","text":"

In general, a color space plugin is created by subclassing from coloraide.spaces.Space. When defining a color space, there are a couple things that must be defined. Using XYZ as an example, we will go over them.

from coloraide import cat\nfrom coloraide.channels import Channel\n\n\nclass XYZD65(Space):\n\"\"\"XYZ D65 class.\"\"\"\n\n    # A base color though which a color is converted through.\n    # XYZ is our absolute base, so it doesn't have a real base,\n    # but something like HSL might have a base color of `srgb`.\n    BASE = \"xyz-d65\"\n\n    # The name of the color space.\n    NAME = \"xyz-d65\"\n\n    # One or more accepted identifiers that are allowed for the `color(space ...)` format.\n    # For this this specific color space, both `color(xyz x y z / a)` and `color(xyz-d65 x y z / a)` are accepted.\n    # As `xyz` is listed first, `xyz` is the default used when printing in this format.\n    SERIALIZE = (\"xyz-d65\", \"xyz\")\n\n    # Specify channel attributes, bounds, etc. of the non-alpha color channels.\n    # Each channel is defined via a `Channel` object\n    #\n    #```\n    #class Channel(str):\n    #    \"\"\"Channel.\"\"\"\n    #\n    #    def __new__(\n    #        cls,\n    #        name: str,\n    #        low: float,\n    #        high: float,\n    #        bound: bool = False,\n    #        flags: int = 0,\n    #        limit: Tuple[float | None, float | None] = (None, None),\n    #        nans: float = 0.0\n    #    ) -> 'Channel':\n    #```\n    #\n    # - `name`: The name of the channel.\n    # - `low`: Lower limit of the channel, for unbound channels, the value will be arbitrary.\n    # - `high`: Upper limit of the channel, for unbound channels, the value will be arbitrary.\n    # - `bound`: Whether the channel enforces the gamut range.\n    # - `limit`: Optional upper and lower limit. Used to define a hard limit for the channel that is clamped\n    #            when the channel is set. This differs from gamut boundaries which can be exceeded until gamut\n    #            mapping occurs. For instance, `chroma` often enforces no values below zero as these values\n    #            do not naturally occur, not even with normal out of gamut colors. So, we could clamp the lower\n    #            bound: `(0, None)`.\n    # - `flags`: Flags used to provide additional context for the channel.\n    # - `nans`: Default value to use for a given channel when it is undefined. More advanced handling can be\n    #           done by overriding `resolve_channel()` on the color space object.\n    #\n    # The following flags are supported:\n    # - FLG_ANGLE: denotes that channel is a angle or degree value.\n    # - FLG_PERCENT: denotes the value is considered a percent input. This is usually used in named CSS functions\n    #                like `hsl()` which require string inputs for saturation and lightness to always be in a\n    #                percentage format. The CSS `color()` function ignores this flags as no channels are always\n    #                required to be percentages. Percentage range will be determined by `high` and `low`.\n    # - FLG_OPT_PERCENT: denotes the value can optionally be considered as a percent.\n    #                    This is also only used for CSS string input and output. CSS `oklab()`, `lab()`, `oklch()`,\n    #                    `lch()`, and `srgb()` allow for channels to be provided as percentages or normal\n    #                    numbers in certain cases. This tells the parser and serializer which channels allow this.\n    #                    Percentage range will be determined by `high` and `low`.\n    # - FLG_MIRROR_PERCENT: The channel, when importing or exporting to a percent should mirror the percentage\n    #                       for negative values. This is used mainly in Lab and Lab like spaces which have `a`\n    #                       `b` channels that allow for both negative and positive values. If set, `high` and `low`\n    #                       should fulfill `abs(low) == high`.\n    CHANNELS = (\n        Channel(\"x\", 0.0, 1.0),\n        Channel(\"y\", 0.0, 1.0),\n        Channel(\"z\", 0.0, 1.0)\n    )\n\n    # A dictionary containing a mapping of aliases to `name` attribute of `CHANNELS` found above.\n    CHANNEL_ALIASES = {}\n\n    # If you'd like this color space to parse as and export a `color(space ...)` format.\n    # If set to `False` the space will not recognize the color format as an input.\n    # This only affects input matching. To override output of the color format, you will also\n    # need to override the `to_string` method.\n    COLOR_FORMAT = True\n\n    # Specify the white point that the color space uses\n    # White point should be a `tuple` containing the x and y chromaticity points.\n    # Some basic ones are provided in the `cat` module for both 2 degree and 10 degree observer.\n    WHITE = cat.WHITES['2deg']['D65']\n\n    # If `GAMUT_CHECK` is set to a color space name, the provided color space will be used to verify the an \"in gamut\"\n    # check in addition to the current color space's channel ranges. This is often used with color spaces such as:\n    # HSL, HSV, and HWB where `GAMUT_CHECK` will be set to `srgb`.\n    #\n    # Gamut checking:\n    #   The specified color space will be checked first followed by the original. Assuming the parent color space fits,\n    #   the original should fit as well, but there are some cases when a parent color space that is slightly out of\n    #   gamut, when evaluated with a threshold, may appear to be in gamut enough, but when checking the original color\n    #   space, the values can be greatly out of specification (looking at you HSL).\n    GAMUT_CHECK = None\n\n    # What is the color space's dynamic range\n    DYNAMIC_RANGE = 'sdr'\n\n    ############################\n    # To and from conversion functions that transform the color to and from the `BASE` color.\n    ############################\n    def to_base(self, coords: Vector) -> Vector:\n\"\"\"\n        To XYZ (no change).\n\n        Any needed chromatic adaptation is handled in the parent Color object.\n        \"\"\"\n\n        return coords\n\n    def from_base(self, coords: Vector) -> Vector:\n\"\"\"\n        From XYZ (no change).\n\n        Any needed chromatic adaptation is handled in the parent Color object.\n        \"\"\"\n\n        return coords\n

Once registered, colors can be created using the NAME via normal instantiation methods or conversions:

Color(NAME, [...])\nColor(red).convert(NAME)\n

By default, assuming COLOR_FORMAT is True, color strings will be parsed in the following format, where SERIALIZE is one one of the IDs specified via the SERIALIZE plugin property.

Color('color(SERIALIZE ...)')\n
"},{"location":"plugins/space/#plugin-defaults","title":"Plugin Defaults","text":"

It is important to note that color space plugins are often not isolated. They are convert to from some BASE color and may be a BASE color for some other color space. Essentially, color spaces are chained together via the BASE property to ensure proper conversion to and from the color space. Because of this, it is not advisable to have any configurable defaults that would fundamentally change how the color coordinates are calculated, as such a change could affect not only the targeted color space, but other color spaces up and down the color conversion change.

If configuration of a color space's fundamental calculations of coordinates is desired, it is recommended that the given Space plugin gets subclassed and provided a new NAME, along with SERIALIZE IDs that do not conflict with other spaces. Such changes would include changing a white point, changing viewing conditions, and even changing the algorithm for color space conversion.

Defaults can be provided and configured via an __init__ method, but it is strongly recommended that only superficial things are controlled by such options, like controlling recognized input/output string formats.

Additionally, if provided an __init__, it is required that super().__init__() also gets called.

"},{"location":"plugins/space/#chromatic-adaptation","title":"Chromatic Adaptation","text":"

Chromatic adaptation is usually applied to a color when it is passing from one XYZ color space to another XYZ color space that has a different white point. In ColorAide, any time XYZ D65 is either the target or origin color, and the other color space has a different white point, the XYZ coordinates, will either be adapted to XYZ D65 or XYZ (new white point) respectively. This all happens without The Space plugin needing to do anything additional.

White points are specified via the WHITE property, and should contain a tuple of xy coordinates of the white point.

"},{"location":"plugins/space/#achromatic-rules","title":"Achromatic Rules","text":"

A given color space can define its rules for determining whether a color is achromatic. If one is not defined, the color will be convert to XYZ D65 and its achromatic method will be used. In order to have reasonably fast checks, it is better to evaluate the achromatic state without converting to another color. In order to define achromatic rules, simply override is_achromatic(). For instance, LCh is achromatic when chroma is very close to zero:

    def is_achromatic(self, coords: Vector) -> bool | None:\n\"\"\"Check if color is achromatic.\"\"\"\n\n        return coords[1] < ACHROMATIC_THRESHOLD\n
"},{"location":"plugins/space/#resolve-undefined-values","title":"Resolve Undefined Values","text":"

By default, undefined color channels are resolved as 0, but there are color spaces where zero just does not work well for a given channel. Such color spaces can choose a different default for undefined values. Generally, 0 is encouraged, but if zero is fundamentally a bad value for a color space and/or can decrease accuracy of colors, a different default can be specified when defining a channel on the color space via the nans parameter.

class ACEScct(sRGB):\n\"\"\"The ACEScct color class.\"\"\"\n\n    BASE = \"acescg\"\n    NAME = \"acescct\"\n    SERIALIZE = (\"--acescct\",)\n    WHITE = (0.32168, 0.33767)\n    CHANNELS = (\n        Channel(\"r\", CCT_MIN, CCT_MAX, bound=True, nans=CCT_MIN),\n        Channel(\"g\", CCT_MIN, CCT_MAX, bound=True, nans=CCT_MIN),\n        Channel(\"b\", CCT_MIN, CCT_MAX, bound=True, nans=CCT_MIN)\n    )\n

If the channel requires more advanced handling, you can override resolve_channel() on the color space itself:

    def resolve_channel(self, index: int, coords: Vector) -> float:\n\"\"\"Resove channels.\"\"\"\n\n        if index in (1, 2):\n            if not math.isnan(coords[index]):\n                return coords[index]\n\n            return self.ACHROMATIC.get_ideal_ab(coords[0])[index - 1]\n\n        value = coords[index]\n        return self.channels[index].nans if math.isnan(value) else value\n
"},{"location":"plugins/space/#mix-ins","title":"Mix-ins","text":"

ColorAide provides some various mixins for some common color space types. It should be noted that all cylindrical type color mixins are derived from Cylindrical. Regular is used for normal, 3 channel color spaces usually with ranges of [0, 1], CMY and sRGB as examples.

CylindricalRegularRGBishHSLishHSVishHWBishLabishLChish
class Cylindrical:\n\"\"\"Cylindrical space.\"\"\"\n\n    def hue_name(self) -> str:\n\"\"\"Hue channel name.\"\"\"\n\n        return \"h\"\n\n    def hue_index(self) -> int:\n\"\"\"Get hue index.\"\"\"\n\n        return cast('Space', self).get_channel_index(self.hue_name())\n
class Regular:\n    \"\"\"Regular, 3 channel color space usually with range of [0, 1].\n
class RGBish(Regular):\n\"\"\"RGB-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return RGB-ish names in order R G B.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of RGB-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class HSLish(Cylindrical):\n\"\"\"HSL-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return HSL-ish names in order H S L.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of HSL-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class HSVish(Cylindrical):\n\"\"\"HSV-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return HSV-ish names in order H S V.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of HSV-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class HWBish(Cylindrical):\n\"\"\"HWB-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return HWB-ish names in order H W B.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of HWB-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class Labish:\n\"\"\"Lab-ish color spaces.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return Lab-ish names in the order L a b.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of the Lab-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class LChish(Cylindrical):\n\"\"\"LCh-ish color spaces.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return LCh-ish names in the order L c h.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of the Lab-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n

Mix-in classes are mainly available so that a color space can be inspected to see if it falls into a specific generic color space type in order to allow for some generic handling of the color. For instance, you may not care specifically what color space you are dealing with, but you may want to extract the hue from all cylindrical spaces, or grab the lightness (or lightness equivalent) from all Lab-ish color spaces.

The mix-in classes provide methods mainly to extract expected channels on color spaces that may use different names for similar channels or to determine the index of a specific channel type. Occasionally, these methods may need to be overridden for a color space.

Below, we can see that both jzazbz and ictcp identify as Lab-ish spaces. If we just care about accessing the equivalent of Lab lightness on these spaces, we can simply can access them with the following logic.

>>> from coloraide.spaces import Labish\n>>> srgb = Color('red')\n>>> jzazbz = srgb.convert('jzazbz')\n>>> ictcp = srgb.convert('ictcp')\n>>> for c in (srgb, jzazbz, ictcp):\n...     if isinstance(c._space, Labish):\n...         print('color: ', c)\n...         l = c._space.names()[0]\n...         print('channel: ', l)\n...         print('value: ', c.get(l))\n... \ncolor:  color(--jzazbz 0.13438 0.11789 0.11188 / 1)\nchannel:  jz\nvalue:  0.13438473104350065\ncolor:  color(--ictcp 0.42785 -0.11574 0.2788 / 1)\nchannel:  i\nvalue:  0.4278524836087273\n
"},{"location":"plugins/space/#adding-new-inputoutput-formats","title":"Adding New Input/Output Formats","text":"

One common thing that may be desired is altering an existing color space to accept and output a specialized format. While using hex color codes or rgb() formats are fairly common, there are many places were other forms are used to represent colors. It may be beneficial for a user working with colors in some more obscure form to repurpose a color space to handle different input/output formats.

The base of every color space is defined to accept and output the color(space ...) format. As this is a common input form across all color spaces, it is handled generically for all spaces in one action for performance reasons. Iterating each color space to perform the same match with a different color spaces name is obviously slower. A color can opt out of this input format by simply setting COLOR_FORMAT to False. This only disables input parsing. In order to disable this format during serialization the color space's #py3 to_string() method would need to be overridden.

New, per color space matching logic can be achieved by simply by overriding the match() method. If it is desired to also accept the color(space ...) format, just keep the COLOR_FORMAT flag enabled; otherwise, disable it.

As an example, let's consider the default sRGB space. We wanted to add additional CSS formats in addition to the color(space ...) format. While we won't go into the specific parsing logic, the general top-level logic can be seen below.

We simply override the match() method and call into our CSS parser. The parser will handle the appropriate syntax for our color spaces. It is not configured to process the color(space ...) format as that is already handled more efficiently when with COLOR_FORMAT enabled. Also, notice that match() is expected to return two things: a tuple containing the color channel coordinates and the alpha value, and the end position (([r, g, b], a), end). If the match fails, it simply returns None.

from coloraide.spaces import srgb as base\nfrom coloraide.css import parse\n\n\nclass sRGB(base.sRGB):\n\"\"\"sRGB class.\"\"\"\n\n    # This color class should opt into the generic `color(space ...)` input format.\n    # This is `True` by default, but shown for demonstration purposes.\n    COLOR_FORMAT: True\n\n    # If the color format above is not found, continue with our custom match to handle all other formats.\n    def match(\n        self,\n        string: str,\n        start: int = 0,\n        fullmatch: bool = True\n    ) -> Tuple[Tuple[Vector, float], int] | None:\n\"\"\"Match a CSS color string.\"\"\"\n\n        return parse.parse_css(self, string, start, fullmatch)\n

Additionally, we control the output formats by overriding the to_string() function. We ensure that it accepts all the parameters we need, in our case we accept the common parameters and later check for our special inputs in kwargs.

    def to_string(\n        self,\n        parent: 'Color',\n        *,\n        alpha: bool | None = None,\n        precision: int | None = None,\n        fit: Union[bool, str] = True,\n        none: bool = False,\n        color: bool = False,\n        hex: bool = False,\n        names: bool = False,\n        comma: bool = False,\n        upper: bool = False,\n        percent: bool = False,\n        compress: bool = False,\n        **kwargs: Any\n    ) -> str:\n\"\"\"Convert to CSS.\"\"\"\n\n        return serialize.serialize_css(\n            parent,\n            func='rgb',\n            alpha=alpha,\n            precision=precision,\n            fit=fit,\n            none=none,\n            color=color,\n            hexa=hex,\n            name=names,\n            legacy=comma,\n            upper=upper,\n            percent=percent,\n            compress=compress,\n            scale=255\n        )\n

As all ColorAide color spaces are defined as plugins, there should be ample examples to help someone start writing a new color space.

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Introduction","text":""},{"location":"#what-is-coloraide","title":"What is ColorAide?","text":"

ColorAide is a pure Python, object oriented approach to colors.

>>> from coloraide import Color\n>>> Color.steps(['lch(75% 50 0)', 'lch(75% 50 300)'], steps=8, space='lch', hue='longer')\n[color(--lch 75 50 0 / 1), color(--lch 75 50 42.857 / 1), color(--lch 75 50 85.714 / 1), color(--lch 75 50 128.57 / 1), color(--lch 75 50 171.43 / 1), color(--lch 75 50 214.29 / 1), color(--lch 75 50 257.14 / 1), color(--lch 75 50 300 / 1)]\n

ColorAide particularly has a focus on the following:

  • Accurate colors.

  • Proper round tripping (where reasonable).

  • Be generally easy to pick up for the average user.

  • Support modern CSS color spaces and syntax.

  • Make accessible many new and old non-CSS color spaces.

  • Provide a number of useful utilities such as interpolation, color distancing, blending, gamut mapping, filters, correlated color temperature, color vision deficiency simulation, color harmonies, etc.

  • Provide a plugin API to extend supported color spaces and approaches to various utilities.

  • Allow users to configure defaults to their liking.

ColorAide is not meant to be the one library to replace all other color libraries. There are many great libraries out there such such as: Colour Science, Colorio, Python Color Math, and many others. Some focus on the scientific aspects of colors and provide a wealth of various spaces, illuminants, access to complex color space visualizers, and numerous esoteric tools. Some are highly focused on speed. Some are powerful, but can be more complex to pick up by the average user.

At its heart, ColorAide was designed for convenience, flexibility, and to be very easy to pick up and work with. There are, of course, some trade offs with speed when using a pure Python, object oriented approach, but there are also many advantages as well. ColorAide might not always be the tool for every job, but hopefully it is a great tool all the same.

"},{"location":"#installation","title":"Installation","text":"

ColorAide can be installed via Python's pip:

$ pip install coloraide\n
"},{"location":"advanced/","title":"Advanced Topics","text":"

Colors are complicated, and sometimes it may not be understood why colors or color transformations yield the results that they do. Here we'd like to cover more advanced or specific topics that don't fit well in existing topics or are too verbose to be included elsewhere.

"},{"location":"advanced/#round-trip-accuracy","title":"Round Trip Accuracy","text":"

In general, ColorAide is careful to provide good round trip conversions where practical. What this means is that we try to maintain a high level of accuracy so that when a color is converted to a different color and back that it will be very close, if not exactly, the same.

In general, we are able to keep decent round tripping by not not clipping values during conversion and maintaining as high a level of precision as we can, but there are some cases where the high level of round trip accuracy cannot be maintained, or even at all. There are even reasons where we willfully choose to sacrifice some accuracy for convenience in order to uphold intuitive expectations for the user.

If you are a color scientist or you work in certain industries, there are definite reasons to uphold accuracy at all costs, but sometimes, you just want the colors to do the what you expect them to do. ColorAide tries to live in the space between. We try to provide accurate color round tripping except when it comes at the cost of practicality.

"},{"location":"advanced/#limitations-of-the-color-space","title":"Limitations of The Color Space","text":"

One situation that can affect round tripping is when one color model cannot properly handle a color due to its gamut being beyond the conversion algorithm's capabilities.

Consider a wide gamut, HDR color space like Jzazbz. Jzazbz is an unbounded color space with plenty of headroom for HDR. Now, let's compare it to HSLuv, an SDR color space derived from the Luv color space and confined to the sRGB gamut. It is essentially a more perceptually uniform version of HSL, but the algorithm specifically requires lightness to be clamped to the SDR range. If we convert an HDR color from Jzazbz to HSLuv, round trip will be broken as the color space simply does not support the HDR range.

>>> jz = Color('color(--jzazbz 0.25 0 0)')\n>>> jz\ncolor(--jzazbz 0.25 0 0 / 1)\n>>> hsluv = jz.convert('hsluv')\n>>> hsluv\ncolor(--hsluv none 0 100 / 1)\n>>> hsluv.convert('jzazbz')\ncolor(--jzazbz 0.22207 -0.00016 -0.00012 / 1)\n
If a color space algorithm does not support a specific color, the conversion may be clamped or come back with an unexpected value.

"},{"location":"advanced/#floating-point-math","title":"Floating Point Math","text":"

Floating point math can also be responsible for some differences in round tripping. Floating point issues are not specific to this library or even the language of Python, but to all computers in general. For example, computers cannot store infinite repeating decimals to properly represent all floating point numbers.

What this means is that no matter how much floating point precision you maintain, some error is introduced when doing floating point operations. Certain rounding conventions are used in order to average out the errors to stay as close as possible to the intended, real value, but it does not prevent floating point errors. This is simply the nature of computers and floating point math.

>>> color = Color('white')\n>>> color[:]\n[1.0, 1.0, 1.0, 1.0]\n>>> color.convert('prophoto-rgb').convert('srgb')[:]\n[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]\n
"},{"location":"advanced/#special-handling-cylindrical-spaces","title":"Special Handling: Cylindrical Spaces","text":"

Sometimes, round trip accuracy can be compromised further for practical reasons. A common case where we make compromises is with cylindrical color models.

ColorAide aims to make colors easy to use, but the one case that can frustrate users is interpolating with an achromatic color using a cylindrical color space.

Achromatic colors do not have a hue, but all conversions end up yielding something for hue, even it it has no practical meaning. This can cause odd color shifts when interpolating with an achromatic color. In order to get logical results when doing interpolation, we detect when a color is achromatic (or very close to achromatic) and set the hues to undefined. This helps us to identify achromatic cases and helps us to prevent weird color shifts when interpolating between achromatic colors. Only if a user manually defines a hue do we respect it.

>>> Color.interpolate(['lch(75 100 180)', 'lch(75 0 0)'], space='lch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128f78d0>\n>>> Color.interpolate(['lch(75 100 180)', 'lch(75 0 none)'], space='lch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112908610>\n

Because of floating point issues, conversions to cylindrical color spaces do not always satisfy the requirements to be recognized as achromatic colors.

As an example, HSL colors are achromatic when the sRGB color it is derived from has all color channels equal to each other. Let's say we convert the color darkgray to the XYZ D65 color space and then back again. We can see that what was once a color with all color channels equal to each other is now a color that has color channels very nearly equal to each other.

>>> c1 = Color('darkgray')\n>>> c1[:-1]\n[0.6627450980392157, 0.6627450980392157, 0.6627450980392157]\n>>> c2 = c1.convert('xyz-d65').convert('srgb')\n>>> c2[:-1]\n[0.6627450980392157, 0.6627450980392156, 0.6627450980392156]\n

These two colors are intended to be the same, but one satisfies the requirement to have the HSL hue set to NaN, but the other does not. This is a case where accuracy vs practicality comes into play. We all know the color is essentially still darkgray, and that is what the user intends. To allow this to work seamlessly, we apply a little leniency to the achromatic rules and state that if the color is very, very close to being achromatic, we will consider it achromatic, and we sacrifice a little accuracy to gain practicality. Or maybe it is better to say that we compensate for the natural inaccuracies that exist.

>>> Color('darkgray').convert('hsl')[:-1]\n[nan, 0.0, 0.6627450980392157]\n>>> Color('darkgray').convert('xyz-d65').convert('hsl')[:-1]\n[nan, 3.2919403637141254e-16, 0.6627450980392156]\n

This problem can exist in various scenarios in pretty much all cylindrical color spaces. Some have tighter algorithms and may give really good results with sRGB, but then when converting from some other color space we'll see maybe not as tight a translation to and from.

Additionally, some color spaces have very dynamic achromatic responses, as an interesting example, let's consider CAM16 JMh. This color space actually has its lower limit for achromatic colors gradually rise higher and higher as lightness increases. Not only that, the achromatic line actually passes mainly through hue ~209.5 for most achromatic colors lighter than black.

>>> Color('color(srgb 0 0 0)').convert('cam16-jmh', norm=False)[:]\n[0.0, 0.0, 0.0, 1.0]\n>>> Color('color(srgb 0.5 0.5 0.5)').convert('cam16-jmh', norm=False)[:]\n[42.841343691205466, 1.4635354939944947, 209.5351055763844, 1.0]\n>>> Color('color(srgb 1 1 1)').convert('cam16-jmh', norm=False)[:]\n[99.99999999999997, 2.236898445770595, 209.53333446353506, 1.0]\n

This can make it hard to specify a simple chroma check for achromatic colors. Simply lowering the chroma or changing the hue can make the color no longer achromatic.

>>> white = Color('color(srgb 1 1 1)')\n>>> white.convert('cam16-jmh', norm=False).convert('srgb').to_string(hex=True)\n'#ffffff'\n>>> white.convert('cam16-jmh', norm=False).set('h', 0).convert('srgb').to_string(hex=True)\n'#fffdfe'\n>>> white.convert('cam16-jmh', norm=False).set('m', 0.0).convert('srgb').to_string(hex=True)\n'#fffefd'\n

For these types of color spaces, ColorAide will map the achromatic response with a spline and use it as a reference to give detect achromatic values for undefined chroma and hue.

>>> Color('cam16-jmh', [100, NaN, NaN]).convert('srgb').to_string(hex=True)\n'#ffffff'\n>>> Color('cam16-jmh', [50, NaN, NaN]).convert('srgb').to_string(hex=True)\n'#919191'\n>>> Color('cam16-jmh', [20, NaN, NaN]).convert('srgb').to_string(hex=True)\n'#424242'\n

Depending on how well we can fit the achromatic response, the better the accuracy, but we do purposely allow some wiggle room to ensure we can capture achromatic colors within the threshold of the spline's accuracy. This can introduce some loss of accuracy, but makes working with achromatic colors in difficult spaces like CAM16 JMh more reasonable.

>>> gray = Color('cam16-jmh', [50, NaN, NaN])\n>>> gray.normalize()\ncolor(--cam16-jmh 50 1.5823 none / 1)\n>>> Color.interpolate([gray, 'green'], space='cam16-jmh')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128756d0>\n
"},{"location":"average/","title":"Color Averaging","text":"

Color averaging is the process of calculating an average color from a set of other colors by taking the mean of each color channel.

Averaging under ColorAide can take as many colors as desired and will return a color that represents the average. This is not to be confused with interpolation which employs a different technique, but in certain situations, it can sort of function like mixing multiple colors.

"},{"location":"average/#rectangular-space-averaging","title":"Rectangular Space Averaging","text":"

ColorAide, by default, averages in rectangular color spaces, the default being Linear sRGB. If desired, other color spaces can be used, such as perceptually uniform spaces like Oklab.

>>> Color.average(['red', 'blue'])\ncolor(srgb-linear 0.5 0 0.5 / 1)\n>>> Color.average(['red', 'blue'], space='srgb')\ncolor(srgb 0.5 0 0.5 / 1)\n>>> Color.average(['red', 'blue'], space='oklab')\ncolor(--oklab 0.53998 0.0962 -0.09284 / 1)\n

Averaging is not restricted to any certain amount of colors.

>>> Color.average(['red', 'yellow', 'orange', 'green'])\ncolor(srgb-linear 0.75 0.39803 0 / 1)\n
"},{"location":"average/#cylindrical-space-averaging","title":"Cylindrical Space Averaging","text":"

ColorAide can average colors in rectangular spaces and cylindrical spaces. When applying averaging in a cylindrical space, hues will be averaged taking the circular mean.

Cylindrical averaging may not provide as good of results as using rectangular spaces, but is provided to provide a sane approach if a cylindrical space is used.

>>> Color.average(['orange', 'yellow', 'red'])\ncolor(srgb-linear 1 0.45875 0 / 1)\n>>> Color.average(['orange', 'yellow', 'red'], space='hsl')\ncolor(--hsl 33.227 1 0.5 / 1)\n

Because calculations are done in a cylindrical space, the averaged colors can be different than what is acquired with rectangular space averaging.

>>> Color.average(['purple', 'green', 'blue'])\ncolor(srgb-linear 0.07195 0.07195 0.40529 / 1)\n>>> Color.average(['purple', 'green', 'blue'], space='hsl')\ncolor(--hsl -120 1 0.33399 / 1)\n
"},{"location":"average/#averaging-with-transparency","title":"Averaging with Transparency","text":"

ColorAide, by default, will account for transparency when averaging colors. Colors which are more transparent will have less of an impact on the average. This is done by premultiplying the colors before averaging.

>>> Steps([Color('darkgreen'), Color('color(srgb 0 0.50196 0 / 1)'), Color('color(srgb 0 0 1)')])\n[color(srgb 0 0.39216 0 / 1), color(srgb 0 0.50196 0 / 1), color(srgb 0 0 1 / 1)]\n>>> for i in range(12):\n...     Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'])\n... \ncolor(srgb-linear 0 0.06372 0.5 / 0.66667)\ncolor(srgb-linear 0 0.07033 0.47826 / 0.69697)\ncolor(srgb-linear 0 0.0764 0.45833 / 0.72727)\ncolor(srgb-linear 0 0.08198 0.44 / 0.75758)\ncolor(srgb-linear 0 0.08713 0.42308 / 0.78788)\ncolor(srgb-linear 0 0.09189 0.40741 / 0.81818)\ncolor(srgb-linear 0 0.09632 0.39286 / 0.84848)\ncolor(srgb-linear 0 0.10044 0.37931 / 0.87879)\ncolor(srgb-linear 0 0.10429 0.36667 / 0.90909)\ncolor(srgb-linear 0 0.10789 0.35484 / 0.93939)\ncolor(srgb-linear 0 0.11126 0.34375 / 0.9697)\ncolor(srgb-linear 0 0.11443 0.33333 / 1)\n

If you'd like to average the channels without taking transparency into consideration, simply set premultiplied to False.

>>> Steps([Color('darkgreen'), Color('color(srgb 0 0.50196 0 / 1)'), Color('color(srgb 0 0 1)')])\n[color(srgb 0 0.39216 0 / 1), color(srgb 0 0.50196 0 / 1), color(srgb 0 0 1 / 1)]\n>>> for i in range(12):\n...     Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'], premultiplied=False)\n... \ncolor(srgb-linear 0 0.11443 0.33333 / 0.66667)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.69697)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.72727)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.75758)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.78788)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.81818)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.84848)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.87879)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.90909)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.93939)\ncolor(srgb-linear 0 0.11443 0.33333 / 0.9697)\ncolor(srgb-linear 0 0.11443 0.33333 / 1)\n
"},{"location":"average/#averaging-with-undefined-values","title":"Averaging with Undefined Values","text":"

When averaging with undefined values, ColorAide will not consider the undefined values in the average. This is mainly provided for averaging cylindrical colors, particularly achromatic colors.

>>> Color.average(['white', 'color(srgb 0 0 1)'], space='hsl')\ncolor(--hsl -120 0.5 0.75 / 1)\n

Implied achromatic hues are only considered undefined if powerless is enabled. This is similar to how interpolation works. By default, explicitly defined hues are respected if working directly in the averaging color space.

>>> Color.average(['hsl(30 0 100)', 'hsl(240 100 50 / 1)'], space='hsl')\ncolor(--hsl -45 0.5 0.75 / 1)\n>>> Color.average(['hsl(30 0 100)', 'hsl(240 100 50 / 1)'], space='hsl', powerless=True)\ncolor(--hsl -120 0.5 0.75 / 1)\n

While undefined logic is intended to handle achromatic hues, this logic will be applied to any channel. It should be noted that no attempt to carry forward the undefined values through conversion is made at this time. Conversions will remove any undefined status unless the channel is an achromatic hues.

>>> for i in range(12):\n...     Color.average(['darkgreen', f'color(srgb 0 none 0 / {i / 11})', 'color(srgb 0 0 1)'])\n... \ncolor(srgb-linear 0 0.06372 0.5 / 0.66667)\ncolor(srgb-linear 0 0.06095 0.47826 / 0.69697)\ncolor(srgb-linear 0 0.05841 0.45833 / 0.72727)\ncolor(srgb-linear 0 0.05607 0.44 / 0.75758)\ncolor(srgb-linear 0 0.05392 0.42308 / 0.78788)\ncolor(srgb-linear 0 0.05192 0.40741 / 0.81818)\ncolor(srgb-linear 0 0.05006 0.39286 / 0.84848)\ncolor(srgb-linear 0 0.04834 0.37931 / 0.87879)\ncolor(srgb-linear 0 0.04673 0.36667 / 0.90909)\ncolor(srgb-linear 0 0.04522 0.35484 / 0.93939)\ncolor(srgb-linear 0 0.04381 0.34375 / 0.9697)\ncolor(srgb-linear 0 0.04248 0.33333 / 1)\n

When premultiplied is enabled, premultiplication will not be applied to a color if its alpha is undefined.

>>> Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / none)', 'color(srgb 0 0 1)'], space='srgb')\ncolor(srgb 0 0.29804 0.33333 / 1)\n
"},{"location":"cat/","title":"Chromatic Adaptation","text":"

Chromatic adaptation is the human visual system's ability to adjust to changes in illumination in order to preserve the appearance of object colors. It is responsible for the stable appearance of object colors despite the wide variation of light which might be reflected from an object and observed by our eyes. A chromatic adaptation transform (CAT) emulates this important aspect of color perception in color appearance models.

In short, colors look different under different lighting, and CATs are used to predict what a color should look like from one lighting source to another.

"},{"location":"cat/#illuminants","title":"Illuminants","text":"

Viewing a color in daylight will look different than viewing it by candle light. Color spaces usually define a reference illuminant that clarifies the assumed lighting for the given space. For instance, sRGB is a color space defined with an illuminant of D65 (light in the shade - no direct sunlight - at noon). On the other hand, the ProPhoto RGB space uses a D50 illuminant (direct sunlight at noon).

When translating a color from one illuminant to another, it is desirable to ensure that the color under the original illuminant appears as it should under the new illuminant, just as it would in real life. CATs are used to predict what the new color under the new illuminant should be in order to fulfill these requirements.

For a quick example, let's take the color blue under sRGB (D65 white point) and the same blue under Pro Photo RGB (D50 white point). If we take the raw chromaticity points from the color under each color space and use them to generate a color, both under the same color space (in this case sRGB), we can see that the values are different. We can see the values are different.

>>> d65 = Color('blue').split_chromaticity()\n>>> d50 = Color('blue').convert('prophoto-rgb').split_chromaticity()\n>>> color_d50 = Color.chromaticity('srgb', d50)\n>>> color_d65 = Color.chromaticity('srgb', d65)\n>>> Row([color_d50, color_d65])\n[color(srgb 0.12557 0.05823 0.88102 / 1), color(srgb 0 0 1 / 1)]\n

The same color looks different because it is reflecting a different light source. The illuminant of a color space can affect how the color appears, and each illuminant has a different color temperature which can provide a warmer or cooler color tone to the colors under a particular color space.

We can visualize this concept a bit more clearly by taking the raw chromaticities from the D50 and D65 white and scaling them both under the same color space. Here we will take both the D50 and D65 white point and first scale them under the D65 sRGB color space and then scale them under the D50 Pro Photo color space. Notice that the D50 white (Pro Photo) has a red shift when rendered under sRGB, but the D65 white (sRGB) has a blue shift under Pro Photo. Relative to each other, the D65 white has a cooler temperature than D50, and this changes the color.

>>> d65 = Color('srgb', [1, 1, 1]).split_chromaticity()\n>>> d50 = Color('prophoto-rgb', [1, 1, 1]).split_chromaticity()\n>>> color_d50 = Color.chromaticity('srgb', d50, scale=True)\n>>> color_d65 = Color.chromaticity('srgb', d65, scale=True)\n>>> Row([color_d50, color_d65])\n[color(srgb 1 0.92084 0.80569 / 1), color(srgb 1 1 1 / 1)]\n>>> color_d50 = Color.chromaticity('prophoto-rgb', d50, scale=True)\n>>> color_d65 = Color.chromaticity('prophoto-rgb', d65, scale=True)\n>>> Row([color_d50, color_d65])\n[color(prophoto-rgb 1 1 1 / 1), color(prophoto-rgb 0.82447 0.84559 0.97953 / 1)]\n

In order to account for the differences in illuminants, we use chromatic adaptation to modify the chromaticities of the color so that that they account for the different illuminant and appear as they should under the new light source. This happens automatically when we do call convert(). We can see that the white point gets adjusted such that the D50 white looks like the D65 white when in sRGB and D65 white looks like D50 white under Pro Photo.

>>> color_d50 = Color('prophoto-rgb', [1, 1, 1]).convert('srgb')\n>>> color_d65 = Color('srgb', [1, 1, 1])\n>>> Row([color_d50, color_d65])\n[color(srgb 1 1 1 / 1), color(srgb 1 1 1 / 1)]\n

Generally, chromatic adaptation takes place within the XYZ color space. So in ColorAide, any color transform that must account for the differences of illuminants between two color spaces must go through chromatic adaptation, and it must occur in the XYZ color space. ColorAide satisfies this by making the registration of the XYZ D65 color space mandatory and using it as the transition color space when chromatic adaptation is needed.

For instance, if a color space such as Pro Photo is being translated to sRGB, Pro Photo will first be transformed to XYZ D50, then it will be chromatically adapted to XYZ D65, next it will be transformed sRGB.

So, we can actually do this manually and compare the results to convert() which automatically handles chromatic adaptation. In order to do this, we need to provide the specified \"white point\" for the source color and the \"white point\" for the destination color along with the XYZ coordinates we wish to transform. ColorAide uses the Bradford CAT by default, so we will specify that CAT for consistency.

>>> from coloraide import cat\n>>> xyzd50 = Color('prophoto-rgb', [1, 1, 1]).convert('xyz-d50').coords()\n>>> xyzd50\n[0.9642956764295677, 1.0, 0.8251046025104602]\n>>> xyzd65 = Color.chromatic_adaptation(cat.WHITES['2deg'][\"D50\"], cat.WHITES['2deg'][\"D65\"], xyzd50, method='bradford')\n>>> manual = Color('xyz-d65', xyzd65).convert('srgb')\n>>> auto = Color('prophoto-rgb', [1, 1, 1]).convert('srgb')\n>>> manual, auto\n(color(srgb 1 1 1 / 1), color(srgb 1 1 1 / 1))\n

ColorAide, currently defines the following illuminants for both 2\u02da observer and 10\u02da observer, but most people are probably only concerned with D65 and D50 (2\u02da degree observer) which are the only the illuminants used in the default color spaces provided by ColorAide. Illuminants are not restricted to what is listed below, but those are the ones available by default.

Illuminants A B C D50 D55 D65 D75 E F2 F7 F11"},{"location":"cat/#supported-cats","title":"Supported CATs","text":"

There are various CATs, all varying in complexity and accuracy. We will not go through all of them and instead will leave that up to the user to research as needed. Suffice it to say, the Bradford CAT is currently the industry standard (in most cases), but there are a variety of options available, and research continues to try and improve upon CATs of the past to come up with better CATs for the future.

Currently, ColorAide mainly supports von Kries type CATs (named after an early 20th century color scientist), or CATs that are similar to and/or are built upon the original von Kries CAT. We also do not currently support every known von Kries CAT out there, but a good number are available. In the future, support may be expanded.

CAT bradford von-kries xyz-scaling sharp cat02 cat16 cmccat97 cmccat2000"},{"location":"cat/#changing-the-default-cat","title":"Changing the Default CAT","text":"

Changing the default CAT is easy and follows the same pattern as the rest of the available class overrides. Simply derive a new Color() class from the original and override the CHROMATIC_ADAPTATION property with the name of the desired CAT. Afterwards, all color transforms will use the specified CAT.

>>> class Custom(Color):\n...     CHROMATIC_ADAPTATION = 'cat02'\n... \n>>> d50 = Custom('color(xyz-d50 0.11627 0.07261 0.23256 / 1)')\n>>> d65 = d50.convert('xyz-d65')\n>>> d50, d65\n(color(xyz-d50 0.11627 0.07261 0.23256 / 1), color(xyz-d65 0.12476 0.07614 0.30581 / 1))\n
"},{"location":"chromaticity/","title":"Chromaticity Coordinates","text":"

Colors are generally composed of two parts, luminance and chromaticity. Luminance refers to the brightness while chromaticity refers to the hue and colorfulness.

Separating out chromaticity from luminance, we can create a 2D from the chromaticity where we are able to plot the full spectrum of visible color. Over time, there have been multiple approaches to expressing chromaticity, the most common being: CIE 1931, CIE 1964, or CIE 1976.

1931 xy Chromaticity Diagram1960 uv Chromaticity Diagram1931 u'v' Chromaticity Diagram

Chromaticity coordinates are an important part of color science and are often used to define characteristics of color spaces, including gamuts and white points. This is often why depictions of white points and gamuts are overlaid onto chromaticity diagrams.

When combined with luminance, we can add depth when viewing a gamut within the chromaticity space.

"},{"location":"chromaticity/#getting-chromaticity-coordinates","title":"Getting Chromaticity Coordinates","text":"

ColorAide provides a few ways to access chromaticity. The first method, split_chromaticity(), allows for decomposing a color into it's two basic parts: chromaticity and luminance. The result is a 3 coordinates list containing the 2D chromaticity coordinates followed by the luminance. By default, values are exported in the format u'v'Y, where u'v' is the chromaticity coordinates in the CIE 1976 system and Y is the luminance taken directly from XYZ.

>>> Color('red').split_chromaticity()\n[0.4507042253521127, 0.522887323943662, 0.21263900587151024]\n

If chromaticity coordinates are desired in a different format, any of the following can be manually specified.

Key Output Description xy-1931 [x, y, Y] Chromaticity in the CIE 1931 xy system and luminance. uv-1960 [u, v, Y] Chromaticity in the CIE 1960 uv system and luminance. uv-1976 [u', v', Y] Chromaticity in the CIE 1976 u'v' system and luminance.
>>> Color('red').split_chromaticity('xy-1931')\n[0.64, 0.33, 0.21263900587151024]\n

All results are returned with chromaticities being relative to the current color's white point. This allows you to get the true chromaticities of that color space. If a pair of white point chromaticities are provided, the values will be chromatically adapted to match the given white point. white must be specified as an xy chromaticity pair. If you have chromaticity values in a non-xy pair, see converting chromaticity coordinates to learn how to convert them to the expected format.

>>> from coloraide import cat\n>>> Color('red').split_chromaticity(white=cat.WHITES['2deg']['D50'])\n[0.45718361011203096, 0.5248532543501369, 0.22249317711056513]\n

Tip

If you ever need to get the white point from an already registered, supported color space, ColorAide makes these available via white(). The value is returned by default as the tristimulus values (XYZ coordinates), but it can also be returned as any of the supported chromaticity coordinate formats by specifying the desired output.

>>> Color('red').white()\n[0.9504559270516716, 1.0, 1.0890577507598784]\n>>> Color('red').white('uv-1960')\n[0.1978300066428368, 0.312213329959194]\n

If all that is desired is the 2D chromaticity coordinates, you can also use the two, simple convenience methods: xy() and uv(). xy() will return chromaticity in the CIE 1931 xy system and uv() will return chromaticity within the CIE 1976 u'v' system (default) or the CIE 1960 uv system, uv output is controlled by explicitly passing the desired year of the uv system.

>>> Color('red').xy()\n[0.64, 0.33]\n>>> Color('red').uv()\n[0.4507042253521127, 0.522887323943662]\n>>> Color('red').uv('1960')\n[0.4507042253521127, 0.3485915492957747]\n

The white parameter is also accepted by xy() and uv().

Luminance

ColorAide also allows for grabbing luminance via the luminance() method. It should be noted that by default this function returns luminance relative to the D65 white point as it is common for people to use luminance normalized like this, but if you'd like to quickly get luminance and have it relative to the current color's white point, just set white to None and ColorAide will calculate the value relative to the current color.

>>> Color('red').luminance(white=None)\n0.21263900587151024\n

New 2.4

  • split_chromaticity() is new in 2.4.
  • Chromaticity specifier in white() is new in 2.4.
  • white parameter of luminance() is new in 2.4.
"},{"location":"chromaticity/#create-color-from-chromaticity-coordinates","title":"Create Color From Chromaticity Coordinates","text":"

New 2.4

ColorAide also provides an easy way to create colors from chromaticity coordinates. chromaticity() is a generalized method that takes a color space to create the color in and a set of chromaticity coordinates. The coordinates should be supplied using the same white point as the targeted color space. Chromaticity coordinates can be passed as 2D coordinates without luminance or with luminance. When passed with luminance the color should be identical to the original.

>>> uvY = Color('red').uv()\n>>> Color.chromaticity('srgb', uvY)\ncolor(srgb 1.9559 0 0 / 1)\n

If only 2D chromaticity points are given, Y will be assumed as 1. When luminance is maxed out like this, it may be desirable to normalize/scale the color in a linear RGB space to make the color displayable. This can be done by enabling scale which, by default, scales the color in linear sRGB. If a wider gamut is needed, you can change it via scale_space. Using a non-linear RGB space is not recommended as non-linear spaces will cause the chromaticity coordinates to shift.

>>> uv = Color('red').uv()\n>>> Color.chromaticity('srgb', uv, scale=True)\ncolor(srgb 1 0 0 / 1)\n>>> uv = Color('display-p3', [1, 0, 0]).uv()\n>>> Color.chromaticity('display-p3', uv, scale=True, scale_space='display-p3-linear')\ncolor(display-p3 1 0 0 / 1)\n

This generally preserves chromaticity, scaling luminance, but if a color is out of gamut, the chromaticity of the resultant color will be affected.

Tip

There is no RGB color space that perfectly encompasses the entire visible gamut. No matter what scaling space is selected, colors outside the gamut of the scaling space will not exactly match the specified chromaticity coordinates. If exact values are needed, scaling should be avoided. If displaying the colors is desired, then sacrificing accuracy of the colors by scaling or some other gamut mapping method is necessary. Scaling/normalization is how we colorize all of our chromaticity diagrams in these documents.

It is important to be consistent with white point usage. If we wanted to create a color in sRGB with ProPhoto chromaticities, it is important that we create the color first under a color space that uses the same white point. ProPhoto uses D50 and sRGB uses D65. So if we had ProPhoto chromaticities, it makes sense to first create the color under ProPhoto and then convert to sRGB.

>>> c1 = Color('prophoto-rgb', [1, 0, 0])\n>>> c2 = Color.chromaticity('prophoto-rgb', c1.split_chromaticity()).convert('srgb')\n>>> c1, c2.convert('prophoto-rgb')\n(color(prophoto-rgb 1 0 0 / 1), color(prophoto-rgb 1 0 0 / 1))\n

With that said, there may be times when you have chromaticity coordinates that use a white point which no current supported color space supports. chromaticity() does provide a way of gamut mapping such coordinates by simply specifying the white point of the provided chromaticity coordinates. To illustrate, we'll take the same example, but this time create a color under sRGB directly with ProPhoto chromaticities, the difference is that we will pass ProPhoto's white point.

>>> c1 = Color('prophoto-rgb', [1, 0, 0])\n>>> c2 = Color.chromaticity('srgb', c1.split_chromaticity(), white=c1.white('xy-1931'))\n>>> c1, c2.convert('prophoto-rgb')\n(color(prophoto-rgb 1 0 0 / 1), color(prophoto-rgb 1 0 0 / 1))\n
"},{"location":"chromaticity/#converting-chromaticity-coordinates","title":"Converting Chromaticity Coordinates","text":"

New 2.4

ColorAide normally expects you are working with chromaticity points that are compatible with at least one of the registered color spaces. In general, the API is set up with this expectation to make things easy for users. Normally, the user will not need to manually specify a white point, but it is possible that a user may be working with or exporting chromaticity coordinates to/from an unsupported, external space using an altogether different white point. We may need to specify that white point, but it may be in a format that ColorAide doesn't expect. Luckily, ColorAide provides a simple way to convert from between various chromaticity formats and convert to and from tristimulus (XYZ) values.

>>> from coloraide import util\n>>> Color('white').xy()\n[0.3127, 0.329]\n>>> Color.convert_chromaticity('uv-1976', 'xy-1931', Color('white').uv())\n[0.3126999999999999, 0.32899999999999985, 1.0]\n>>> Color.convert_chromaticity('uv-1960', 'xy-1931', Color('white').uv('1960'))\n[0.3126999999999999, 0.3289999999999999, 1.0]\n>>> Color.convert_chromaticity('xyz', 'xy-1931', Color('white').convert('xyz-d65').coords())\n[0.3127, 0.329, 0.9999999999999999]\n

Tip

When converting from XYZ tristimulus values to chromaticity values, the color black resolves to [0, 0] in xy, uv, or u'v'. This does not align with other achromatic values within the color space if displaying in 3D. This isn't technically incorrect as any chromaticity pair with zero luminance will be equal to black in XYZ. ColorAide normally accounts for this and aligns the point using the targeted color's white point, but when manually converting using convert_chromaticity(), such context is unavailable. If you are converting external XYZ values to chromaticity coordinates and would like to align, black on the achromatic axis, simply pass in the white point for context.

>>> black = Color('black')\n>>> black.xy()\n[0.3127, 0.329]\n>>> Color.convert_chromaticity('xyz', 'xy-1931', black.convert('xyz-d65').coords())\n[0.0, 0.0, 0.0]\n>>> Color.convert_chromaticity('xyz', 'xy-1931', black.convert('xyz-d65').coords(), white=black.white('xy-1931'))\n[0.3127, 0.329, 0.0]\n
"},{"location":"color/","title":"The Color Object","text":"

The Color object is where all the magic of ColorAide happens and provides access to all the color manipulation methods available. The Color object is used to represent a given color within a particular color space. In order to perform most operations, you will need to create a color instance to begin.

There are a number of ways to instantiate new colors. Here we will cover the basics of creating colors, cloning colors, converting colors, and a few other Color class specific topics.

"},{"location":"color/#importing","title":"Importing","text":"

The Color object contains all the logic to create and manipulate colors. It can be imported from coloraide.

from coloraide import Color\n

By default, the Color object registers only a subset of the available color spaces and features that are shipped with ColorAide. This keeps the object a bit lighter and provides the more commonly used color spaces and features. Color spaces, additional color distancing algorithms, gamut mapping algorithms, etc. are implemented via plugins. The normal way to get access to these additional spaces and features is to subclass the Color object and resister the desired spaces and features that are needed, but if you just want to explore all that ColorAide offers, you can import the ColorAll object from everything.

from coloraide.everything import ColorAll as Color\n

Custom Color Objects

To add more plugins or tweak color defaults, see Custom Color Classes for more.

"},{"location":"color/#creating-colors","title":"Creating Colors","text":"

Once the Color class is imported, colors can be created using various forms of input, including: numerical inputs, dictionaries, CSS color strings, and even other Color instances.

"},{"location":"color/#numerical-inputs","title":"Numerical Inputs","text":"

The quickest way to create a color is by simply specifying the color space, color coordinates, and the optional alpha channel. Numerical inputs require very little processing, but it should be noted that inputs must be specified according to the way the color points are stored. Some people may be aware of the old CSS convention of specifying sRGB colors with a range of 0 - 255, but ColorAide stores these as values between 0 - 1. If transparency is omitted, transparency is assumed to be fully opaque, or a value of 1.

>>> Color(\"srgb\", [0.5, 0, 1], 0.3)\ncolor(srgb 0.5 0 1 / 0.3)\n>>> Color(\"srgb\", [0.5, 0, 1])\ncolor(srgb 0.5 0 1 / 1)\n
"},{"location":"color/#dictionary-inputs","title":"Dictionary Inputs","text":"

It may be desired to store and retrieve colors from some serialized format such as JSON. To make this easier, ColorAide allows exporting and importing colors via dictionaries as well.

Dictionaries must define the space key and the coords key containing values for all of the color channels. The alpha channel is kept separate and can be omitted, and if so, will be assumed as 1.

>>> d = Color('red').to_dict()\n>>> print(d)\n{'space': 'srgb', 'coords': [1.0, 0.0, 0.0], 'alpha': 1.0}\n>>> Color(d)\ncolor(srgb 1 0 0 / 1)\n
"},{"location":"color/#string-inputs","title":"String Inputs","text":"

By default, ColorAide accepts input strings as outlined in the CSS color specification. Accepted syntax includes legacy CSS color formats as defined in CSS Level 3, but also allows for CSS Level 4 Color syntax!

>>> Color(\"red\")\ncolor(srgb 1 0 0 / 1)\n>>> Color(\"#00ff00\")\ncolor(srgb 0 1 0 / 1)\n>>> Color(\"rgb(0 0 255 / 1)\")\ncolor(srgb 0 0 1 / 1)\n

ColorAide supports all the color spaces as defined in the CSS Level 4 Color spec, but is not restricted to only supported CSS colors. In order to support color strings for all colors, ColorAide allows for non-CSS color spaces to be represented via the Level 4 CSS color() function. Essentially, we've adopted the color() function as the universal way in which to serialize color strings.

It should also be noted that color() can be used to describe any color regardless of whether it is supported in the CSS spec in this way or not. For any color that is not explicitly supported in CSS via the color() function, ColorAide will allow using this form if the color space uses a -- prefix for the color space identifier. Check the documentation of the given color space to discover the appropriate CSS identifier name.

>>> Color('color(--hsl 130 40% 75% / 0.5)')\ncolor(--hsl 130 0.4 0.75 / 0.5)\n
"},{"location":"color/#color-instance-inputs","title":"Color Instance Inputs","text":"

If another color instance is passed as the input, a new color object will be created using the color data from the input. This essentially clones the passed object.

>>> c1 = Color('red')\n>>> c2 = Color(c1)\n>>> c1, c2\n(color(srgb 1 0 0 / 1), color(srgb 1 0 0 / 1))\n

You can also use the new method to generate new colors from already instantiated color objects.

>>> color1 = Color(\"red\")\n>>> color1\ncolor(srgb 1 0 0 / 1)\n>>> color1.new(\"blue\")\ncolor(srgb 0 0 1 / 1)\n

Tip

If the Color class has be subclassed, this is an easy way to convert between the different subclasses, assuming the registered color spaces are compatible between the two different Color classes.

"},{"location":"color/#random","title":"Random","text":"

If you'd like to generate a random color, simply call Color.random with a given color space and one will be generated.

>>> [Color.random('srgb') for _ in range(10)]\n[color(srgb 0.02438 0.67719 0.81316 / 1), color(srgb 0.35667 0.04083 0.32678 / 1), color(srgb 0.71755 0.91093 0.74213 / 1), color(srgb 0.60069 0.88372 0.07998 / 1), color(srgb 0.95268 0.8185 0.2335 / 1), color(srgb 0.401 0.16545 0.66271 / 1), color(srgb 0.27553 0.38181 0.14499 / 1), color(srgb 0.98796 0.78747 0.21055 / 1), color(srgb 0.84131 0.563 0.16015 / 1), color(srgb 0.56572 0.39736 0.85305 / 1)]\n

Ranges are based on the color space's defined channel range. For color spaces with defined gamuts, the values will be confined to appropriate ranges. For color space's without defined gamuts, the ranges may be quite arbitrary in some cases. For color spaces with no hard, defined gamut, or gamuts that that far exceed practical usage it is recommend to fit the colors to whatever gamut you'd like, or simply use a target space with a clear defined gamut.

>>> Color.random('lab').fit('srgb')\ncolor(--lab 8.3727 17.942 -41.971 / 1)\n

Lastly, if you'd like to further constrain the limits, you can provide a list of constraints. A constraint should be a sequence of two values specifying the minimum and maximum for the channel. If None is provided, that constraint will be ignored. If the list doesn't have enough values, those missing indexes will be ignored. If the list has too many values, those extra values will be ignored.

>>> Color.random('srgb', limits=[(0.25, 0.75)] * 3)\ncolor(srgb 0.60899 0.5199 0.33739 / 1)\n
"},{"location":"color/#cloning","title":"Cloning","text":"

The clone method is an easy way to duplicate the current color object.

Here we clone the green color object, giving us two.

>>> c1 = Color(\"green\")\n>>> c1\ncolor(srgb 0 0.50196 0 / 1)\n>>> c1.clone()\ncolor(srgb 0 0.50196 0 / 1)\n
"},{"location":"color/#updating","title":"Updating","text":"

A color can be \"updated\" using another color input. When an update occurs, the current color space is updated from the data of the second color, but the color space does not change. Using update is the equivalent of converting the second color to the color space of the first and then updating all the coordinates (including alpha). The input parameters are identical to the new method, so we can use a color object, a color string, dictionary, or even raw data points.

Here we update the color red to the color blue:

>>> Color(\"red\")\ncolor(srgb 1 0 0 / 1)\n>>> Color(\"red\").update(Color(\"blue\"))\ncolor(srgb 0 0 1 / 1)\n

Here we update the sRGB red with the color lch(80% 50 130).

>>> Color(\"red\").update(\"lch(80% 50 130)\")\ncolor(srgb 0.60392 0.8398 0.48396 / 1)\n
"},{"location":"color/#mutating","title":"Mutating","text":"

\"Mutating\" is similar to updating except that it will update the color and the color space from another color. The input parameters are identical to the new method, so we can use a color object, a color string, dictionary, or even raw data points.

In this example, the red color object literally becomes the specified CIELCh color of lch(80% 50 130).

>>> Color(\"red\").mutate(\"lch(80% 50 130)\")\ncolor(--lch 80 50 130 / 1)\n
"},{"location":"color/#converting","title":"Converting","text":"

Colors can be converted to other color spaces as needed. Converting will always return a new color unless the in_place parameter is set to True, in which case, the current color will be mutated to the new converted color and a reference to itself is returned.

For instance, if we had a color yellow, and we needed to work with it in another color space, we could simply call the convert method with the desired color space.

>>> Color('yellow').convert(\"lab\")\ncolor(--lab 97.607 -15.75 93.394 / 1)\n

Notes on Round Trip Accuracy

"},{"location":"color/#color-matching","title":"Color Matching","text":"

As previously mentioned, the Color() object can parse CSS style string inputs. The string matching logic is exposed via the match method. We can simply pass match a string, and, if the string is a valid color, a ColorMatch object will be returned. The ColorMatch object has a simple structure that contains the matched color as a Color object, and the start and end points it was located at.

>>> Color.match(\"red\")\nColorMatch(color=color(srgb 1 0 0 / 1), start=0, end=3)\n

By default it matches at the start of the buffer and returns a color if it finds one. If desired, we can do a fullmatch which requires the entire buffer to match a color.

>>> Color.match(\"red and yellow\")\nColorMatch(color=color(srgb 1 0 0 / 1), start=0, end=3)\n>>> Color.match(\"red and yellow\", fullmatch=True)\n

We can also adjust the start position of the search. In this case, by adjusting the start position to 8 characters later, we will match yellow instead of red.

>>> Color.match(\"red and yellow\", start=8)\nColorMatch(color=color(srgb 1 1 0 / 1), start=8, end=14)\n

A method to find all colors in a buffer is not currently provided as looping through all the color spaces and matching all potential colors on every character is not really efficient. Additionally, some buffers may require additional context that is not available to the match function. If such behavior is desired, it is recommended to apply some additional logic to sniff out areas with high likelihood of having a color.

In the following example, we construct a regular expression to find places within the buffer that potentially have a valid color. As the buffer is an HTML document we also want to incorporate some context to avoid matching HTML entities or color names that are part of a CSS variable.

Once we've crafted our regular expression, we can search the buffer to find locations in the buffer that are likely to be colors. Then we can run Color.match() on those positions within the buffer to see if we find a valid color. This ends up being much more efficient!

>>> import re\n>>> from coloraide import Color\n>>> RE_COLOR_START = re.compile(\n...     r\"\"\"(?ix)\n...     (?:\n...         # CSS functions\n...         \\b(?<![-#&$])(?:color\\((?!\\s*-)|(?:hsla?|(?:ok)?(?:lch|lab)|hwb|rgba?)\\()|\n...         # Color words\n...         \\b(?<![-#&$])[\\w]{3,}(?![(-])\\b|\n...         # Hex codes\n...         (?<![&])\\#\n...     )\n...     \"\"\"\n... )\n>>> text = \"\"\"\n... <html>\n... <head>\n... <style>\n... body {\n...     background-color: red;\n...     color: yellow;\n... }\n... </style>\n... </head>\n... <body>\n... <p>This is a test <span style=\"background-color: #000088; color: lch(75% 50 50)\">test</span></p>\n... </body>\n... </html>\n... \"\"\"\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...         mcolor.color.to_string()\n... \n'rgb(255 0 0)'\n'rgb(255 255 0)'\n'rgb(0 0 136)'\n'lch(75 50 50)'\n
"},{"location":"color/#custom-color-classes","title":"Custom Color Classes","text":"

The Color object was created to be extensible and has implemented various functionalities as plugins. Things like color spaces, distancing algorithms, filters, etc. are all implemented as plugins. In order to keep things light, ColorAide does not register all the of the plugins by default unless the user as imported the ColorAll object.

Additionally, ColorAide has implemented a number of defaults that can be tweaked within the Color class to alter how things are handled.

Creating a custom class allows for a user to change some of the default settings and add or remove plugins to gain access to more color spaces, distancing algorithms, filters, and other functionality.

In general, it is always recommended to subclass the Color object when setting up custom preferences or adding or removing plugins. This prevents modifying the base class which may affect other libraries relying on the module. When Color is subclassed, it is safe to then update global overrides or register and deregister plugins without the worry of affecting the base class.

"},{"location":"color/#override-default-settings","title":"Override Default Settings","text":"

ColorAide has a number of preferences that can be altered in the Color class. Most of these options can be configured on demand when calling into a related function that uses them, but it may be useful to set them up one time on a new Color object.

>>> class Color2(Color):\n...     PRECISION = 3\n... \n>>> Color('rgb(128.12345 0 128.12345)').to_string()\n'rgb(128.12 0 128.12)'\n>>> Color2('rgb(128.12345 0 128.12345)').to_string()\n'rgb(128 0 128)'\n
Properties Defaults Description FIT \"lch-chroma\" The default gamut mapping method used by the Color object. INTERPOLATE \"oklab\" The default color space used for interpolation. DELTA_E \"76\" The default \u2206E algorithm used. This applies to when delta_e() is called without specifying a method or when using color distancing to separate color when using the interpolation method called steps. PRECISION 5 The default precision for string outputs. CHROMATIC_ADAPTATION \"bradford\" Chromatic adaptation method used when converting between two color spaces with different white points. See Chromatic Adaptation for more information. HARMONY \"oklch\" Default color space to use for calculating color harmonies. This should be a cylindrical color space. CONTRAST \"wcag21\" Default contrast algorithm. AVERAGE \"average\" Default color space for averaging. CCT \"robertson-1968 Default CCT method. POWERLESS False Experimental option that controls default powerless interpolation behavior. If True, if a color is considered achromatic, but has an explicit hue, it will be treated as powerless during interpolation. CARRYFORWARD False Experimental option that controls default carrying-forward behavior during interpolation. If True, supported, undefined values will be carried over to the interpolating color space after conversion."},{"location":"color/#plugins","title":"Plugins","text":"

Currently, color spaces, delta E methods, chromatic adaptation, filters, contrast, interpolation, and gamut mapping methods are exposed as plugins. As previously mentioned, Color does not register all plugins, and ColorAll is often more than a user needs by default. Registering exactly what you need is normally the recommend approach when more functionality is required.

While we won't go into a lot of details about creating plugins here, we will go over how to register new plugins and deregister existing plugins. To learn more about creating plugins, checkout the plugin documentation.

Registration is performed by the register method. It can take a single plugin or a list of plugins. Based on the plugin's type, The Color object will determine how to properly register the plugin. If the plugin attempts to overwrite a plugin already registered with the same name (as dictated by the plugin) the operation will fail. If overwrite is set to True, the overwrite will not fail and the new plugin will be registered with the specified name in place of the existing plugin.

>>> from coloraide import Color\n>>> from coloraide.spaces.xyy import xyY\n>>> try:\n...     Color('red').convert('xyy')\n... except:\n...     print('Nope')\n... \nNope\n>>> class Custom(Color): ...\n... \n>>> Custom.register(xyY())\n>>> Custom('red').convert('xyy')\ncolor(--xyy 0.64 0.33 0.21264 / 1)\n

Used in conjunction with default settings override, we can not only change a default \u2206E, but we can alter a \u2206E method's configuration by registering it with different defaults:

>>> Color('red').delta_e('blue', method='cmc')\n108.56925233888809\n>>> from coloraide.distance.delta_e_cmc import DECMC\n>>> class Custom(Color):\n...     DELTA_E = \"cmc\"\n... \n>>> Custom.register(DECMC(l=1, c=1), overwrite=True)\n>>> Custom('red').delta_e('blue')\n109.7597278634398\n

If a deregistration is desired, the deregister method can be used. It takes a string that describes the plugin to deregister: category:name.

Valid categories are space, delta-e, cat, contrast, filter, interpolate, fit, and cct.

If the given plugin is not found, an error will be thrown, but if this notification is found to be unnecessary, silent can be enabled and the there will be no error thrown.

>>> class Custom(Color): ...\n... \n>>> Custom.deregister('space:lab-d65')\n>>> try:\n...     Custom('red').convert('lab-d65')\n... except ValueError:\n...     print('Could not convert to Lab D65 as it is no longer registered')\n... \nCould not convert to Lab D65 as it is no longer registered\n

Use of * with deregister will remove all plugins. Use of category:* will remove all plugins of that category.

"},{"location":"compositing/","title":"Compositing and Blending","text":"

Alpha compositing and blending are tied together under the umbrella of compositing. Each is just an aspect of the overall compositing of colors. Blend is run first, followed by alpha compositing.

ColorAide implements both alpha compositing and blending as described in the Compositing and Blending Level 1 specification. Alpha composting is based on Porter Duff compositing. By default, the compose method uses the normal blend mode and the source-over Porter Duff operator.

"},{"location":"compositing/#blending","title":"Blending","text":"

Blending is the aspect of compositing that calculates the mixing of colors where the source element and backdrop overlap. Conceptually, the colors in the source element (top layer) are blended in place with the backdrop (bottom layer).

There are various blend modes, the most common is the normal blend mode which is the default blending mode for browsers. The normal blend mode simply returns the top layer's color when one is overlaid onto another.

But there are many blend modes that could be used, all of which yield different results. If we were to apply a multiply blend mode, we would get something very different:

When composing, the blend mode can be controlled separately in ColorAide. Here, we again use the multiply example and replicate it in ColorAide. To apply blending in ColorAide, simply call compose with a backdrop color, and the calling color will be used as the source.

Display P3sRGB
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, blend='multiply', space=\"display-p3\")\ncolor(display-p3 0.32281 0.23703 0.54084 / 1)\n
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, blend='multiply', space=\"srgb\")\ncolor(srgb 0.02713 0.18668 0.55765 / 1)\n

Tip

compose() can output the results in any color space you need by setting out_space.

>>> Color('#07c7ed').compose(Color('#fc3d99'), blend='multiply', space='srgb', out_space='hsl')\ncolor(--hsl 221.95 0.90722 0.29239 / 1)\n

Display Differences

As some browsers apply compositing based on the display's current color space, we've provided examples in both sRGB and Display P3 so that the examples can be compared on different displays. Which of the above matches your browser?

ColorAide allows you to blend a source over multiple backdrops quite easily as well. Simply send in a list, and the colors will be blended from right to left with the right most color being on the bottom of the stack, and the base color being used as the source (on the very top).

Display P3sRGB
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c3 = Color('#f5d311')\n>>> c1, c2, c3\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1), color(srgb 0.96078 0.82745 0.06667 / 1))\n>>> c1.compose([c2, c3], blend='multiply', space=\"display-p3\")\ncolor(display-p3 0.3031 0.19729 0.15625 / 1)\n
>>> c1 = Color('#07c7ed')\n>>> c2 = Color('#fc3d99')\n>>> c3 = Color('#f5d311')\n>>> c1, c2, c3\n(color(srgb 0.02745 0.78039 0.92941 / 1), color(srgb 0.98824 0.23922 0.6 / 1), color(srgb 0.96078 0.82745 0.06667 / 1))\n>>> c1.compose([c2, c3], blend='multiply', space=\"srgb\")\ncolor(srgb 0.02606 0.15447 0.03718 / 1)\n

Lastly, if for any reason, it is desired to compose with blending disabled (e.g. just run alpha compositing), then you can simply set blend to False.

multiply is just one of many blend modes that are offered in ColorAide, check out Blend Modes to learn about other blend modes.

"},{"location":"compositing/#alpha-compositing","title":"Alpha Compositing","text":"

Alpha compositing or alpha blending is the process of combining one image with a background to create the appearance of partial or full transparency.

When dealing with layers, there are many possible ways to handle them:

Porter Duff compositing covers all possible configurations of layers. Many of these configurations can be useful for all sorts of operations, such as masking. While this library supports all of them, the most commonly used one is source-over which is used to implement simple alpha compositing to simulate semi-transparent layers on top of each other.

Given two colors, ColorAide can replicate this behavior and determine the resultant color by applying compositing. We will use the demonstration above and replicate the result in the example below. Below we set the source color to rgb(7 199 237 / 0.5) and the backdrop color to #fc3d99 and run it through the compose method. It should be noted that the default blend mode of normal is used in conjunction by default.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, space=\"display-p3\")\ncolor(display-p3 0.63261 0.53855 0.75261 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, space=\"srgb\")\ncolor(srgb 0.50784 0.5098 0.76471 / 1)\n

Display Differences

As some browsers apply compositing based on the display's current color space, we've provided examples in both sRGB and Display P3 so that the examples can be compared on different displays. Which of the above matches your browser?

While the average user will be content with the default alpha compositing, Porter Duff offers many other configurations. If desired, we can change the Porter Duff operator used and apply different composite logic. For instance, in this case we can get the resultant of the backdrop over the source color by setting the operator to destination-over. As the backdrop is fully opaque, we just get the backdrop color unaltered.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, operator='destination-over', space=\"display-p3\")\ncolor(display-p3 0.91078 0.30832 0.59266 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99')\n>>> c1, c2\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 1))\n>>> c1.compose(c2, operator='destination-over', space=\"srgb\")\ncolor(srgb 0.98824 0.23922 0.6 / 1)\n

You can also apply alpha compositing to multiple layers at once. Simply send in a list of colors as the backdrop, and the colors will be composed from right to left with the right most color being on the bottom of the stack and the base color (the source) being on the very top.

Here we are using the normal blend mode and 50% transparency on all the circles with an opaque white background. We will calculate the center color where all three layers overlap.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> bg = Color('white')\n>>> c1, c2, c3, bg\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 0.5), color(srgb 0.96078 0.82745 0.06667 / 0.5), color(srgb 1 1 1 / 1))\n>>> c1.compose([c2, c3, bg], blend='normal', space=\"display-p3\")\ncolor(display-p3 0.64728 0.69051 0.76555 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> bg = Color('white')\n>>> c1, c2, c3, bg\n(color(srgb 0.02745 0.78039 0.92941 / 0.5), color(srgb 0.98824 0.23922 0.6 / 0.5), color(srgb 0.96078 0.82745 0.06667 / 0.5), color(srgb 1 1 1 / 1))\n>>> c1.compose([c2, c3, bg], blend='normal', space=\"srgb\")\ncolor(srgb 0.50588 0.67843 0.74804 / 1)\n

Lastly, if for any reason, it is desired to run compose with alpha compositing disabled (e.g. just run blending), then you can simply set operator to False.

Check out Compositing Operators to learn about the many variations that are supported.

"},{"location":"compositing/#complex-compositing","title":"Complex Compositing","text":"

We've covered alpha compositing and blending and have demonstrated their use with simple two color examples and multi-layered examples, but what about different blend modes mixed with alpha compositing?

In this example, we will consider three circles, each with a unique color: #07c7ed, #fc3d99, and #f5d311. We apply 50% transparency to all the circles and place them on a white background. We then perform a multiply blend on all the circles but isolate them so the multiply blend does not apply to the background. The circles are all represented with CSS. We will now try and replicate the colors with ColorAide.

So in the code below, we work our way from the bottom of the stack to the top. Since the background is isolated from the multiply blending, in each region, we start by performing a normal blend on the bottom circle against the background. We then apply multiply blending on each color that is stacked on top. We've provided both the P3 and sRGB outputs to make it easy to compare in case your browser blends in one instead of the other.

Display P3sRGB
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> cw2 = c2.compose('white', blend='normal', space='display-p3')\n>>> cw3 = c3.compose('white', blend='normal', space='display-p3')\n>>> r1 = c2.compose(cw3, blend='multiply', space='display-p3')\n>>> r2 = c1.compose(cw2, blend='multiply', space='display-p3')\n>>> r3 = c1.compose(cw3, blend='multiply', space='display-p3')\n>>> r1, r2, r3\n(color(display-p3 0.92621 0.59932 0.5132 / 1), color(display-p3 0.64701 0.57853 0.76151 / 1), color(display-p3 0.65654 0.81024 0.61627 / 1))\n>>> c1.compose([c2, cw3], blend='multiply', space='display-p3')\ncolor(display-p3 0.62725 0.53003 0.49076 / 1)\n
>>> c1 = Color('#07c7ed').set('alpha', 0.5)\n>>> c2 = Color('#fc3d99').set('alpha', 0.5)\n>>> c3 = Color('#f5d311').set('alpha', 0.5)\n>>> cw2 = c2.compose('white', blend='normal', space='srgb')\n>>> cw3 = c3.compose('white', blend='normal', space='srgb')\n>>> r1 = c2.compose(cw3, blend='multiply', space='srgb')\n>>> r2 = c1.compose(cw2, blend='multiply', space='srgb')\n>>> r3 = c1.compose(cw3, blend='multiply', space='srgb')\n>>> r1, r2, r3\n(color(srgb 0.97463 0.56615 0.42667 / 1), color(srgb 0.5107 0.55157 0.77176 / 1), color(srgb 0.50365 0.81339 0.51451 / 1))\n>>> c1.compose([c2, cw3], blend='multiply', space='srgb')\ncolor(srgb 0.50069 0.50399 0.41161 / 1)\n

Results may vary depending on the browser, but we can see (ignoring rounding differences) that the colors match up. This was performed on Chrome in macOS using a display that uses display-p3.

"},{"location":"compositing/#blend-modes","title":"Blend Modes","text":""},{"location":"compositing/#normal","title":"Normal","text":"

The blending formula simply selects the source color.

Specified as 'normal'.

"},{"location":"compositing/#multiply","title":"Multiply","text":"

The source color is multiplied by the destination color and replaces the destination. The resultant color is always at least as dark as either the source or destination color. Multiplying any color with black results in black. Multiplying any color with white preserves the original color.

Specified as 'multiply'.

"},{"location":"compositing/#screen","title":"Screen","text":"

Multiplies the complements of the backdrop and source color values, then complements the result. The result color is always at least as light as either of the two constituent colors. Screening any color with white produces white; screening with black leaves the original color unchanged. The effect is similar to projecting multiple photographic slides simultaneously onto a single screen.

Specified as 'screen'.

"},{"location":"compositing/#overlay","title":"Overlay","text":"

Multiplies or screens the colors, depending on the backdrop color value. Source colors overlay the backdrop while preserving its highlights and shadows. The backdrop color is not replaced but is mixed with the source color to reflect the lightness or darkness of the backdrop.

Specified as 'overlay'.

"},{"location":"compositing/#darken","title":"Darken","text":"

Selects the darker of the backdrop and source colors. The backdrop is replaced with the source where the source is darker; otherwise, it is left unchanged.

Specified as 'darken'.

"},{"location":"compositing/#lighten","title":"Lighten","text":"

Selects the lighter of the backdrop and source colors. The backdrop is replaced with the source where the source is lighter; otherwise, it is left unchanged.

Specified as 'lighten'.

"},{"location":"compositing/#color-dodge","title":"Color Dodge","text":"

Brightens the backdrop color to reflect the source color. Painting with black produces no changes.

Specified as 'color-dodge'.

"},{"location":"compositing/#color-burn","title":"Color Burn","text":"

Darkens the backdrop color to reflect the source color. Painting with white produces no change.

Specified as 'color-burn'.

"},{"location":"compositing/#hard-light","title":"Hard Light","text":"

Multiplies or screens the colors, depending on the source color value. The effect is similar to shining a harsh spotlight on the backdrop.

Specified as 'hard-light'.

"},{"location":"compositing/#soft-light","title":"Soft Light","text":"

Darkens or lightens the colors, depending on the source color value. The effect is similar to shining a diffused spotlight on the backdrop.

Specified as 'soft-light'.

"},{"location":"compositing/#difference","title":"Difference","text":"

Subtracts the darker of the two constituent colors from the lighter color. Painting with white inverts the backdrop color; painting with black produces no change.

Specified as 'difference'.

"},{"location":"compositing/#exclusion","title":"Exclusion","text":"

Produces an effect similar to that of the Difference mode but lower in contrast. Painting with white inverts the backdrop color; painting with black produces no change.

Specified as 'exclusion'.

"},{"location":"compositing/#hue","title":"Hue","text":"

Creates a color with the hue of the source color and the saturation and luminosity of the backdrop color.

Specified as 'hue'.

"},{"location":"compositing/#saturation","title":"Saturation","text":"

Creates a color with the saturation of the source color and the hue and luminosity of the backdrop color. Painting with this mode in an area of the backdrop that is a pure gray (no saturation) produces no change.

Specified as 'saturation'.

"},{"location":"compositing/#luminosity","title":"Luminosity","text":"

Creates a color with the luminosity of the source color and the hue and saturation of the backdrop color. This produces an inverse effect to that of the Color mode. This mode is the one you can use to create monochrome \"tinted\" image effects like the ones you can see in different website headers.

Specified as 'luminosity'.

"},{"location":"compositing/#color","title":"Color","text":"

Creates a color with the hue and saturation of the source color and the luminosity of the backdrop color. This preserves the gray levels of the backdrop and is useful for coloring monochrome images or tinting color images.

Specified as 'color'.

"},{"location":"compositing/#compositing-operators","title":"Compositing Operators","text":""},{"location":"compositing/#clear","title":"Clear","text":"

No regions are enabled.

Source Destination Result

Specified as 'clear'.

"},{"location":"compositing/#copy","title":"Copy","text":"

Only the source will be present.

Source Destination Result

Specified as 'copy'.

"},{"location":"compositing/#destination","title":"Destination","text":"

Only the destination will be present.

Source Destination Result

Specified as 'destination'.

"},{"location":"compositing/#source-over","title":"Source Over","text":"

Source is placed over the destination.

Source Destination Result

Specified as 'source-over'.

"},{"location":"compositing/#destination-over","title":"Destination Over","text":"

Destination is placed over the source.

Source Destination Result

Specified as 'destination-over'.

"},{"location":"compositing/#source-in","title":"Source In","text":"

The source that overlaps the destination, replaces the destination.

Source Destination Result

Specified as 'source-in'.

"},{"location":"compositing/#destination-in","title":"Destination In","text":"

Destination which overlaps the source, replaces the source.

Source Destination Result

Specified as 'destination-in'.

"},{"location":"compositing/#source-out","title":"Source Out","text":"

Source is placed, where it falls outside of the destination.

Source Destination Result

Specified as 'source-out'.

"},{"location":"compositing/#destination-out","title":"Destination Out","text":"

Destination is placed, where it falls outside of the source.

Source Destination Result

Specified as 'destination-out'.

"},{"location":"compositing/#source-atop","title":"Source Atop","text":"

Source which overlaps the destination, replaces the destination. Destination is placed elsewhere.

Source Destination Result

Specified as 'source-atop'.

"},{"location":"compositing/#destination-atop","title":"Destination Atop","text":"

Destination which overlaps the source replaces the source. Source is placed elsewhere.

Source Destination Result

Specified as 'destination-atop'.

"},{"location":"compositing/#xor","title":"XOR","text":"

Destination which overlaps the source replaces the source. Source is placed elsewhere.

Source Destination Result

Specified as 'xor'.

"},{"location":"compositing/#lighter","title":"Lighter","text":"

Display the sum of the source image and destination image.

Source Destination Result

Specified as 'lighter'.

"},{"location":"contrast/","title":"Contrast","text":"

ColorAide provides a number of utilities related to luminance and contrast.

"},{"location":"contrast/#relative-luminance","title":"Relative Luminance","text":"

In the CIE XYZ and xyY color spaces, the Y parameter is linear to changes in the volume of light. Specifically this refers to the amount of reflected light where 1.0 is assumed to be a perfect reflector in relation to the reference white.

The luminance method exposes access to this value to make it quick and easy to query the relative luminance.

>>> Color(\"black\").luminance()\n0.0\n>>> Color(\"white\").luminance()\n0.9999999999999999\n>>> Color(\"blue\").luminance()\n0.07219231536073371\n

It should be noted that this luminance is relative to the XYZ D65 color space by default as this is how it is defined in the WCAG 2.1. What this means is that luminance is equivalent to the Y value of XYZ D65. We follow this convention as many people expect it in this format.

Luminance and WCAG 2.1

Luminance as described in the WCAG 2.1 spec is essentially the exact same as what the luminance method returns. The only difference is the lower precision by which they calculate the value:

>>> r, g, b = Color('purple')[:-1]\n>>> r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4\n>>> g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4\n>>> b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4\n>>> l = (0.2126 * r + 0.7152 * g + 0.0722 * b)\n>>> print(l)\n0.06147707043243851\n>>> Color('purple').convert('xyz-d65')['y']\n0.06148383144929487\n

If you'd like to have luminance in relation to a given color's white point, you can set white to None. If you'd like to get the luminance relative to some other white point, you can specify the white point as xy chromaticity points.

>>> from coloraide import cat\n>>> Color('prophoto-rgb', [1, 0, 0]).luminance()\n0.26831828062163154\n>>> Color('prophoto-rgb', [1, 0, 0]).luminance(white=None)\n0.2880711282292934\n>>> Color('prophoto-rgb', [1, 0, 0]).luminance(white=cat.WHITES['2deg']['E'])\n0.2905597743108222\n

New 2.4

The white parameter is new in 2.4.

"},{"location":"contrast/#contrast_1","title":"Contrast","text":"

Chromatic contrast refers to the ability to see the difference of a colored object against a colored background. This can be very important when it comes to visual design and other fields. Determining contrast has been explored in many different ways and, depending on the application, there may be approaches to contrast that are more favorable. In the context of the web, contrast often refers to the ability to see text on a colored background and is of particular importance to those that suffer from visual impairments or disabilities.

It should be noted that as we talk about contrast, we will refer to the colors as the text and background as this is often the context in which such a function is used. As far as ColorAide is concerned, the text is always the calling color and the background is the input parameter. It is important to note this as not all contrast algorithms are symmetrical, and can differ depending which color is referenced as text, and which is referenced as the background.

At this time, ColorAide only offers a handful of contrast approaches, and they can be by using the contrast() method.

>>> Color(\"blue\").contrast(\"red\")\n2.149390533243867\n

To select different contrast methods, simply use the method parameter.

>>> Color(\"blue\").contrast(\"red\", method='wcag21')\n2.149390533243867\n
Methods Symmetrical Description wcag21 WCAG 2.1 contrast ratio. lstar Color difference between two tones in the HCT color space."},{"location":"contrast/#wcag-21-contrast-ratio","title":"WCAG 2.1 Contrast Ratio","text":"

The WCAG 2.1 contrast ratio is registered in Color by default

ColorAide implements the color contrast ratio as outlined in the WCAG 2.1 spec. This is currently, the default contrast method. It is not without fault, but is currently the standard outlined for the web.

The contrast ratio as outlined in the WCAG 2.1 specification is simply the ratio of color luminance from a foreground and background color and is very easy to determine if you are able to acquire the luminance of the two colors.

# Where `l1` is the lighter luminance and `l2` the darker\ncontrast_ratio = (l1 + 0.05) / (l2 + 0.05)\n

This method can be used by specifying wcag21 as the contrast method.

>>> Color(\"blue\").contrast(\"red\", method='wcag21')\n2.149390533243867\n
"},{"location":"contrast/#lstar-lightness-difference","title":"Lstar Lightness Difference","text":"

The Lstar contrast method is not registered in Color by default

Google's Material Design uses a new color space called HCT. It uses the hue and chroma from CAM16 and the tone/lightness from CIELab. For contrast, they determined using tones that are \"far enough apart\" in the HCT color space was a good indication of sufficient contrast. Since HCT tone is exactly the same as CIELab's lightness (also known as L*), we've referred to this approach as Lstar.

Lstar's color difference approach to contrast is quite simple, it's literally the difference between two color's lightness as provided by CIELab. This method does not care which color is text or background.

>>> Color('hct', [30, 20, 70]).contrast(Color('hct', [30, 20, 50]), method='lstar')\n20.000001474145677\n

In order to use this contrast method, the plugin must be registered. This assumes the CIELab color space is currently registered, which it is by default.

from coloraide import Color as Base\nfrom coloraide.contrast.lstar import LstarContrast\n\nclass Color(Base): ...\n\nColor.register(LstarContrast())\n
"},{"location":"distance/","title":"Color Distance and Delta E","text":"

The difference or distance between two colors allows for a quantified analysis of how far apart two colors are from one another. This metric is of particular interest in the field of color science, but it has practical applications in color libraries working with colors.

Usually, color distance is applied to near perceptual uniform color spaces in order to obtain a metric regarding a color's visual, perceptual distance from another color. This can be useful in gamut mapping or even determining that colors are close enough or far enough away from each other.

"},{"location":"distance/#color-distance","title":"Color Distance","text":"

ColorAide provides a simple euclidean color distance function. By default, it evaluates the distance in the CIELab color space, but it can be configured to evaluate in any color space, such as Oklab, etc. It may be less useful in some color spaces compared to others. Some spaces may not be well suited, such as cylindrical spaces. Some spaces might not be as very perceptually uniform as others requiring more complex algorithms.

>>> Color(\"red\").distance(\"blue\", space=\"srgb\")\n1.4142135623730951\n>>> Color(\"red\").distance(\"blue\", space=\"lab\")\n184.0190486209969\n
"},{"location":"distance/#delta-e","title":"Delta E","text":"

The delta_e function gives access to various \u2206E implementations, which are just different algorithms to calculate distance. Some are simply Euclidean distance withing a certain color space, some are far more complex.

If no method is specified, the default implementation is \u2206E*ab (CIE76) which uses a simple Euclidean distancing algorithm on the CIELab color space. It is fast, but not as accurate as later iterations of the algorithm as CIELab is not actually as perceptually uniform as it was thought when CIELab was originally developed.

>>> Color(\"red\").delta_e(\"blue\")\n176.3084955965824\n

When method is set, the specified \u2206E algorithm will be used instead. For instance, below we use \u2206E*00 which is a more complex algorithm that accounts for the CIELab's weakness in perceptually uniformity. It does come at the cost of being a little slower.

>>> Color(\"red\").delta_e(\"blue\", method=\"2000\")\n52.878195285926445\n

Distancing and Symmetry

It should be noted that not all distancing algorithms are symmetrical. Some are order dependent.

"},{"location":"distance/#delta-e-cie76","title":"Delta E CIE76","text":"

The \u2206Eab distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*ab\u00a0(CIE76) 76 space='lab-d65'

One of the first approaches to color distancing and is actually just Euclidean distancing in the CIELab color space.

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"76\")\n176.3084955965824\n>>> Color(\"red\").delta_e(\"blue\", method=\"76\", space='lab')\n184.0190486209969\n
"},{"location":"distance/#delta-e-cmc-1984","title":"Delta E CMC (1984)","text":"

The \u2206Ecmc distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*cmc\u00a0(CMC\u00a0l:c\u00a0(1984)) cmc l=2, c=1, space='lab-d65'

Delta E CMC is based on the CIELCh color space. The CMC calculation mathematically defines an ellipsoid around the standard color with semi-axis corresponding to hue, chroma and lightness.

Parameter Acceptability Perceptibility l 2 1 c 1 1

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"cmc\")\n108.56925233888809\n>>> Color(\"red\").delta_e(\"blue\", method=\"cmc\", space='lab')\n114.2301281201658\n
"},{"location":"distance/#delta-e-cie94","title":"Delta E CIE94","text":"

The \u2206E*94 distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*94\u00a0(CIE94) 94 kl=1, k1=0.045, k2=0.015, space='lab-d65'

The 1976 definition was extended to address perceptual non-uniformities, while retaining the CIELab color space, by the introduction of application-specific weights derived from an automotive paint test's tolerance data.

Parameter Graphic\u00a0Arts Textiles kl 1 2 k1 0.045 0.048 k2 0.015 0.014

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"94\")\n70.57699903580162\n>>> Color(\"red\").delta_e(\"blue\", method=\"94\", space='lab')\n73.82677591958294\n
"},{"location":"distance/#delta-e-ciede2000","title":"Delta E CIEDE2000","text":"

The \u2206E*00 distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E*00\u00a0(CIEDE2000) 2000 kl=1, kc=1, kh=1, space='lab-d65'

Since the 1994 definition did not adequately resolve the perceptual uniformity issue, the CIE refined their definition, adding five corrections:

  • A hue rotation term (RT), to deal with the problematic blue region (hue angles in the neighborhood of 275\u00b0)
  • Compensation for neutral colors (the primed values in the LCh differences)
  • Compensation for lightness (SL)
  • Compensation for chroma (SC)
  • Compensation for hue (SH)

Note

By default, Lab D65 is used for color distancing. In the print industry, it is common for Lab D50 to be used. If Lab D50 is desired, simply specify it as the space color space. space must be a CIE Lab color space.

>>> Color(\"red\").delta_e(\"blue\", method=\"2000\")\n52.878195285926445\n>>> Color(\"red\").delta_e(\"blue\", method=\"2000\", space='lab')\n55.79977339019778\n
"},{"location":"distance/#delta-e-hyab","title":"Delta E HyAB","text":"

The \u2206EHyAB distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206EHyAB\u00a0(HyAB) hyab space=\"lab-d65\"

A combination of a Euclidean metric in hue and chroma with a city\u2010block metric to incorporate lightness differences. It can be used on any Lab like color space, the default being CIELab D65.

"},{"location":"distance/#delta-e-ok","title":"Delta E OK","text":"

The \u2206Eok distancing algorithm is registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Eok ok scalar=1

A color distancing algorithm that performs Euclidean distancing in the Oklab color space. This is used in the OkLCh Chroma gamut mapping algorithm. The scalar parameter allows you to scale the result up if desired.

"},{"location":"distance/#delta-e-itp","title":"Delta E ITP","text":"

The \u2206Eitp distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Eitp\u00a0(ICtCp) itp scalar=720

Various algorithms are designed for and perform decently in the SDR range, but \u2206Eitp aims to provide good distancing in the HDR range using the ICtCp color space (must be registered in order to use \u2206Eitp). It was determined that a scalar of 240 was more comparable to the average \u2206E*00 result from the JND data set and 720 equates them to a JND.

Both the ICtCp color space and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_itp import DEITP\nfrom coloraide.spaces.ictcp import ICtCp\n\nclass Color(Base): ...\n\nColor.register([ICtCp(), DEITP()])\n
"},{"location":"distance/#delta-e-z","title":"Delta E Z","text":"

The \u2206Ez distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Ez\u00a0(Jzazbz) jz

Performs Euclidean distancing in the Jzazbz color space, useful for the HDR range.

Both the Jzazbz color space and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_z import DEZ\nfrom coloraide.spaces.jzazbz import Jzazbz\n\nclass Color(Base): ...\n\nColor.register([Jzazbz(), DEZ()])\n
"},{"location":"distance/#delta-e-99o","title":"Delta E 99o","text":"

The \u2206E99o distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206E99o\u00a0(DIN99o) 99o

\u2206E99o performs Euclidean distancing in the DIN99o color space.

Both the DIN99o color space and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_99o import DE99o\nfrom coloraide.spaces.din99o import DIN99o\n\nclass Color(Base): ...\n\nColor.register([DIN99o(), DE99o()])\n
"},{"location":"distance/#delta-e-cam16","title":"Delta E CAM16","text":"

The \u2206Ecam16 distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206Ecam16 cam16 model='ucs'

The CAM16 UCS uniform color space applies an additional nonlinear transformation to lightness and colorfulness so that a color difference metric \u0394E can be based more closely on Euclidean distance. This algorithm performs distancing using the CAM16 UCS color space. If desired model can be changed to use the SCD or LCD model for \"small\" and \"large\" distancing respectively

Parameter Default Small Large model ucs scd lcd

The one or more of the CAM16 (UCS/SCD/LCD) color spaces and the \u2206E algorithm must be registered to use.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_cam16 import DECAM16\nfrom coloraide.spaces.cam16_ucs import CAM16UCS, CAM16SCD, CAM16LCD\n\nclass Color(Base): ...\n\nColor.register([CAM16UCS(), CAM16SCD(), CAM16LCD(), DECAM16()])\n
"},{"location":"distance/#delta-e-hct","title":"Delta E HCT","text":"

The \u2206Ehct distancing algorithm is not registered in Color by default

Delta\u00a0E Symmetrical Name Parameters \u2206EHCT hct

This takes the HCT color space C and H components (CAM16's M and h) and converts them to CAM16 UCS M and h, and applies Euclidean distancing on them along with the T component (CIELab's L*). This is necessary for the HCT Chroma gamut mapping approach.

from coloraide import Color as Base\nfrom coloraide.distance.delta_e_hct import DEHCT\nfrom coloraide.spaces.HCT import HCT\n\nclass Color(Base): ...\n\nColor.register([HCT(), DEHCT()])\n
"},{"location":"distance/#finding-closest-color","title":"Finding Closest Color","text":"

ColorAide implements a simple way to find the closest color, given a list of colors, to another color. The method is called closest and takes a list of colors that are to be compared to the calling color object. The first color with the smallest distance between the calling color object and itself will be considered the nearest/closest color.

Consider the following example. Here we provide a list of colors to compare against red. After comparing all the colors, the closest ends up being maroon.

>>> Color('red').closest(['pink', 'yellow', 'green', 'blue', 'purple', 'maroon'])\ncolor(srgb 0.50196 0 0 / 1)\n

The default distancing method is used if one is not supplied, but others can be used:

>>> Color('red').closest(['pink', 'yellow', 'green', 'blue', 'purple', 'maroon'], method='2000')\ncolor(srgb 0.50196 0 0 / 1)\n
"},{"location":"distance/#configuring-delta-e-defaults","title":"Configuring Delta E Defaults","text":"

A number of distancing algorithms have configurable features that can be set on demand. If you'd like to have these options set by default, you create a custom class and register the the plugins with the defaults of your choice.

In this example, we will configure \u2206E*00 to use CIE Lab D50 instead of D65 by default.

>>> from coloraide import Color as Base\n>>> from coloraide.distance.delta_e_2000 import DE2000\n>>> class Color(Base):\n...     ...\n... \n>>> Color.register(DE2000(space='lab'), overwrite=True)\n>>> Color('red').delta_e('blue', method='2000')\n55.79977339019778\n>>> Color('red').delta_e('blue', method='2000', space='lab-d65')\n52.878195285926445\n
"},{"location":"filters/","title":"Filters","text":"

ColorAide implements a number of filters with each filter being provided as a plugin. Filters simply apply some logic to transform a color in some specific way. Filters can be used to lighten colors, adjust saturation, or completely change the color. Filters can even be used to simulate things like color vision deficiencies.

"},{"location":"filters/#w3c-filter-effects","title":"W3C Filter Effects","text":"

The W3C Filter Effects Module Level 1 specification outline a number of filters for use in SVG and CSS. ColorAide implements all the filters that directly apply to colors. By default, filters are applied in the Linear sRGB color space, but can be applied in sRGB if requested. All other color spaces will throw an error.

NormalBrightnessSaturateContrastOpacityInvertHue RotateSepiaGrayscale

To apply a specific filter in ColorAide, just call the filter() method with the name of the filter you wish to use. If an amount is not provided, the default according to the W3C spec will be used instead.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('brightness', 0.5).clip() for c in colors])\n[color(srgb-linear 0.5 0 0 / 1), color(srgb-linear 0.5 0.07796 0 / 1), color(srgb-linear 0.5 0.27286 0 / 1), color(srgb-linear 0.5 0.5 0 / 1), color(srgb-linear 0.04542 0.20186 0 / 1), color(srgb-linear 0 0.04579 0.04542 / 1), color(srgb-linear 0 0 0.5 / 1), color(srgb-linear 0.01595 0 0.20539 / 1), color(srgb-linear 0.11038 0.01225 0.19066 / 1), color(srgb-linear 0.4275 0.11161 0.4275 / 1)]\n>>> Steps([c.filter('saturate', 0.5).clip() for c in colors])\n[color(srgb-linear 0.6065 0.1065 0.1065 / 1), color(srgb-linear 0.66224 0.24021 0.16224 / 1), color(srgb-linear 0.8016 0.57446 0.3016 / 1), color(srgb-linear 0.964 0.964 0.464 / 1), color(srgb-linear 0.19943 0.35587 0.15401 / 1), color(srgb-linear 0.03601 0.0818 0.08143 / 1), color(srgb-linear 0.036 0.036 0.536 / 1), color(srgb-linear 0.03413 0.01818 0.22357 / 1), color(srgb-linear 0.15637 0.05825 0.23666 / 1), color(srgb-linear 0.62914 0.31325 0.62914 / 1)]\n>>> Steps([c.filter('contrast', 0.8).clip() for c in colors])\n[color(srgb-linear 0.9 0.1 0.1 / 1), color(srgb-linear 0.9 0.22474 0.1 / 1), color(srgb-linear 0.9 0.53658 0.1 / 1), color(srgb-linear 0.9 0.9 0.1 / 1), color(srgb-linear 0.17267 0.42298 0.1 / 1), color(srgb-linear 0.1 0.17326 0.17267 / 1), color(srgb-linear 0.1 0.1 0.9 / 1), color(srgb-linear 0.12552 0.1 0.42862 / 1), color(srgb-linear 0.2766 0.1196 0.40506 / 1), color(srgb-linear 0.78399 0.27858 0.78399 / 1)]\n>>> Steps([c.filter('opacity', 0.5).clip() for c in colors])\n[color(srgb-linear 1 0 0 / 0.5), color(srgb-linear 1 0.15593 0 / 0.5), color(srgb-linear 1 0.54572 0 / 0.5), color(srgb-linear 1 1 0 / 0.5), color(srgb-linear 0.09084 0.40373 0 / 0.5), color(srgb-linear 0 0.09158 0.09084 / 0.5), color(srgb-linear 0 0 1 / 0.5), color(srgb-linear 0.0319 0 0.41077 / 0.5), color(srgb-linear 0.22076 0.0245 0.38133 / 0.5), color(srgb-linear 0.85499 0.22323 0.85499 / 0.5)]\n>>> Steps([c.filter('invert', 1).clip() for c in colors])\n[color(srgb-linear 0 1 1 / 1), color(srgb-linear 0 0.84407 1 / 1), color(srgb-linear 0 0.45428 1 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.90916 0.59627 1 / 1), color(srgb-linear 1 0.90842 0.90916 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.9681 1 0.58923 / 1), color(srgb-linear 0.77924 0.9755 0.61867 / 1), color(srgb-linear 0.14501 0.77677 0.14501 / 1)]\n>>> Steps([c.filter('hue-rotate', 90).clip() for c in colors])\n[color(srgb-linear 0 0.356 0 / 1), color(srgb-linear 0 0.48932 0 / 1), color(srgb-linear 0 0.82259 0.20639 / 1), color(srgb-linear 0 1 0.856 / 1), color(srgb-linear 0 0.37753 0.52519 / 1), color(srgb-linear 0.09084 0.05913 0.14404 / 1), color(srgb-linear 1 0 0.144 / 1), color(srgb-linear 0.41077 0 0.04084 / 1), color(srgb-linear 0.38133 0.01908 0 / 1), color(srgb-linear 0.85499 0.31483 0 / 1)]\n>>> Steps([c.filter('sepia', 1).clip() for c in colors])\n[color(srgb-linear 0.393 0.349 0.272 / 1), color(srgb-linear 0.51291 0.45597 0.35526 / 1), color(srgb-linear 0.81266 0.72337 0.56342 / 1), color(srgb-linear 1 1 0.806 / 1), color(srgb-linear 0.34617 0.30866 0.2403 / 1), color(srgb-linear 0.08759 0.07808 0.0608 / 1), color(srgb-linear 0.189 0.168 0.131 / 1), color(srgb-linear 0.09017 0.08014 0.06249 / 1), color(srgb-linear 0.17767 0.15791 0.12308 / 1), color(srgb-linear 0.66927 0.59517 0.46377 / 1)]\n>>> Steps([c.filter('grayscale', 1).clip() for c in colors])\n[color(srgb-linear 0.2126 0.2126 0.2126 / 1), color(srgb-linear 0.32412 0.32412 0.32412 / 1), color(srgb-linear 0.6029 0.6029 0.6029 / 1), color(srgb-linear 0.9278 0.9278 0.9278 / 1), color(srgb-linear 0.30806 0.30806 0.30806 / 1), color(srgb-linear 0.07205 0.07205 0.07205 / 1), color(srgb-linear 0.0722 0.0722 0.0722 / 1), color(srgb-linear 0.03644 0.03644 0.03644 / 1), color(srgb-linear 0.09199 0.09199 0.09199 / 1), color(srgb-linear 0.40315 0.40315 0.40315 / 1)]\n

Tip

filter() can output the results in any color space you need by setting out_space.

>>> Color('#07c7ed').filter('grayscale', 1, out_space='hsl')\ncolor(--hsl none 0 0.71528 / 1)\n
"},{"location":"filters/#color-vision-deficiency-simulation","title":"Color Vision Deficiency Simulation","text":"

Color blindness or color vision deficiency (CVD) affects approximately 1 in 12 men (8%) and 1 in 200 women. CVD affects millions of people in the world, and many people have no idea that they are color blind and not seeing the full spectrum that others see.

CVD simulation allows those who do not suffer with one of the many different variations of color blindness, to simulate what someone with a CVD would see. Keep in mind that these are just approximations, and that a given type of CVD can be quite different from person to person in severity.

The human eye has 3 types of cones that are used to perceive colors. Each of these cones can become deficient, either through genetics, or other means. Each type of cone is responsible for perceiving different wavelengths of light. A CVD occurs when one or more of these cones are missing or not functioning properly. There are severe cases where one of the three cones will not perceive color at all, and there are others where the cones may just be less sensitive.

"},{"location":"filters/#dichromacy","title":"Dichromacy","text":"

Dichromacy is a type of CVD that has the characteristics of essentially causing the person to only have two functioning cones for perceiving colors. This essentially flattens the color spectrum into a 2D plane. Protanopia describes the CVD where the cone responsible for long wavelengths does not function, deuteranopia describes the CVD affecting the cone responsible for processing medium wavelengths, and tritanopia describes deficiencies with the cone responsible for short wavelengths.

NormalProtanopiaDeuteranopiaTritanopia

One misconception is that people with CVD have a color blindness for just red and green or something similar as that can often be how it is described, and while the statement is true that certain people with CVD may have trouble with red and green, they often can have trouble with other colors as well.

The LMS color space was created to mimic the response of the human eye. Each channel represents one of the 3 cones with each cone responsible for seeing light waves of different frequencies: long (L), medium (M), and short (S). Protanopia represents deficiencies with the L cone, deuteranopia with the M cone, and tritanopia with the S cone. Any color whose properties only vary in the properties specific to a person's deficient cone(s) will have the potential to cause confusion for that person.

Consider the example below. We generate 3 different color series, each specifically targeting a specific deficiency. This is done by generating a series of colors that have all properties equal except that they have variance in a different cone response. The first row varies only with the L cone response, the second only with the M cone response, and the third only with the S cone response. We then apply the filters for protanopia, deuteranopia, and tritanopia. We can see that while many of the colors are altered, the row that targets the deficient cone specific to the CVD all appear to be of the same color making it difficult to distinguish between any of them.

NormalProtanopiaDeuteranopiaTritanopia
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.clip() for c in confusing_colors])\n[color(srgb 0 0.73785 0.0565 / 1), color(srgb 0.48453 0.72034 0.0466 / 1), color(srgb 0.66519 0.70225 0.03524 / 1), color(srgb 0.79774 0.68354 0.02349 / 1), color(srgb 0.90633 0.66414 0.01175 / 1), color(srgb 1 0.64398 0 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.clip() for c in confusing_colors])\n[color(srgb 1 0.40107 0.70629 / 1), color(srgb 0.90666 0.5039 0.70182 / 1), color(srgb 0.7985 0.58535 0.69731 / 1), color(srgb 0.66664 0.65439 0.69277 / 1), color(srgb 0.48742 0.71511 0.68818 / 1), color(srgb 0.04172 0.76977 0.68356 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.clip() for c in confusing_colors])\n[color(srgb 0.18039 0.5451 0.34118 / 1), color(srgb 0.26982 0.51394 0.56225 / 1), color(srgb 0.33367 0.4802 0.70642 / 1), color(srgb 0.38543 0.44317 0.81991 / 1), color(srgb 0.42983 0.40182 0.91581 / 1), color(srgb 0.46916 0.35444 1 / 1)]\n
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.filter('protan').clip() for c in confusing_colors])\n[color(srgb-linear 0.44718 0.44718 0.00253 / 1), color(srgb-linear 0.44632 0.44632 0.00253 / 1), color(srgb-linear 0.44545 0.44545 0.00252 / 1), color(srgb-linear 0.44459 0.44459 0.00252 / 1), color(srgb-linear 0.44372 0.44372 0.00252 / 1), color(srgb-linear 0.44286 0.44286 0.00251 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.filter('protan').clip() for c in confusing_colors])\n[color(srgb-linear 0.23099 0.23099 0.46046 / 1), color(srgb-linear 0.28318 0.28318 0.45292 / 1), color(srgb-linear 0.33538 0.33538 0.44537 / 1), color(srgb-linear 0.38758 0.38758 0.43782 / 1), color(srgb-linear 0.43978 0.43978 0.43027 / 1), color(srgb-linear 0.49197 0.49197 0.42273 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.filter('protan').clip() for c in confusing_colors])\n[color(srgb-linear 0.23224 0.23224 0.09438 / 1), color(srgb-linear 0.20829 0.20829 0.27557 / 1), color(srgb-linear 0.18435 0.18435 0.45676 / 1), color(srgb-linear 0.16041 0.16041 0.63795 / 1), color(srgb-linear 0.13646 0.13646 0.81914 / 1), color(srgb-linear 0.11252 0.11252 1 / 1)]\n
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.filter('deutan').clip() for c in confusing_colors])\n[color(srgb-linear 0.35631 0.35631 0.0158 / 1), color(srgb-linear 0.39626 0.39626 0.00984 / 1), color(srgb-linear 0.43622 0.43622 0.00387 / 1), color(srgb-linear 0.47617 0.47617 0 / 1), color(srgb-linear 0.51612 0.51612 0 / 1), color(srgb-linear 0.55607 0.55607 0 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.filter('deutan').clip() for c in confusing_colors])\n[color(srgb-linear 0.38725 0.38725 0.43764 / 1), color(srgb-linear 0.38833 0.38833 0.43756 / 1), color(srgb-linear 0.38941 0.38941 0.43748 / 1), color(srgb-linear 0.3905 0.3905 0.43739 / 1), color(srgb-linear 0.39158 0.39158 0.43731 / 1), color(srgb-linear 0.39266 0.39266 0.43723 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.filter('deutan').clip() for c in confusing_colors])\n[color(srgb-linear 0.1906 0.1906 0.10046 / 1), color(srgb-linear 0.17799 0.17799 0.28 / 1), color(srgb-linear 0.16539 0.16539 0.45953 / 1), color(srgb-linear 0.15278 0.15278 0.63907 / 1), color(srgb-linear 0.14018 0.14018 0.8186 / 1), color(srgb-linear 0.12757 0.12757 0.99814 / 1)]\n
>>> confusing_colors = confusion_line(Color('orange'), 'l')\n>>> Steps([c.filter('tritan').clip() for c in confusing_colors])\n[color(srgb-linear 0.09504 0.41129 0.56924 / 1), color(srgb-linear 0.27752 0.40204 0.46424 / 1), color(srgb-linear 0.46584 0.38712 0.3939 / 1), color(srgb-linear 0.66496 0.36167 0.3878 / 1), color(srgb-linear 0.86409 0.33623 0.38171 / 1), color(srgb-linear 1 0.31078 0.37561 / 1)]\n>>> confusing_colors = confusion_line(Color('hotpink'), 'm')\n>>> Steps([c.filter('tritan').clip() for c in confusing_colors])\n[color(srgb-linear 0.96312 0.16951 0.23789 / 1), color(srgb-linear 0.77356 0.24403 0.28965 / 1), color(srgb-linear 0.584 0.31855 0.34142 / 1), color(srgb-linear 0.39444 0.39306 0.39318 / 1), color(srgb-linear 0.22435 0.44862 0.56064 / 1), color(srgb-linear 0.05436 0.50409 0.7287 / 1)]\n>>> confusing_colors = confusion_line(Color('seagreen'), 's')\n>>> Steps([c.filter('tritan').clip() for c in confusing_colors])\n[color(srgb-linear 0.06253 0.22391 0.30451 / 1), color(srgb-linear 0.06359 0.22288 0.30244 / 1), color(srgb-linear 0.06464 0.22186 0.30038 / 1), color(srgb-linear 0.0657 0.22083 0.29831 / 1), color(srgb-linear 0.06675 0.2198 0.29624 / 1), color(srgb-linear 0.06781 0.21877 0.29417 / 1)]\n

By default, ColorAide uses the Brettel 1997 method to simulate tritanopia as it is the only option that has decent accuracy for tritanopia. Vi\u00e9not, Brettel, and Mollon 1999 approach is used to simulate protanopia and deuteranopia as it is not only faster than Brettel, but handles extreme case a little better. Machado 2009 has it strengths as well which we will cover in Anomalous Trichromacy.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('protan').clip() for c in colors])\n[color(srgb-linear 0.11238 0.11238 0.00401 / 1), color(srgb-linear 0.25079 0.25079 0.00338 / 1), color(srgb-linear 0.59678 0.59678 0.00182 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.36856 0.36856 0 / 1), color(srgb-linear 0.08129 0.08129 0.09047 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.00358 0.00358 0.4109 / 1), color(srgb-linear 0.04655 0.04655 0.38211 / 1), color(srgb-linear 0.29423 0.29423 0.85752 / 1)]\n>>> Steps([c.filter('deutan').clip() for c in colors])\n[color(srgb-linear 0.29275 0.29275 0 / 1), color(srgb-linear 0.40303 0.40303 0 / 1), color(srgb-linear 0.67871 0.67871 0 / 1), color(srgb-linear 1 1 0 / 1), color(srgb-linear 0.31213 0.31213 0.00699 / 1), color(srgb-linear 0.06477 0.06477 0.09289 / 1), color(srgb-linear 0 0 1 / 1), color(srgb-linear 0.00934 0.00934 0.41006 / 1), color(srgb-linear 0.08195 0.08195 0.37694 / 1), color(srgb-linear 0.40818 0.40818 0.84088 / 1)]\n>>> Steps([c.filter('tritan').clip() for c in colors])\n[color(srgb-linear 1 0 0.07589 / 1), color(srgb-linear 1 0.12293 0.20142 / 1), color(srgb-linear 1 0.46132 0.5152 / 1), color(srgb-linear 1 0.85569 0.88089 / 1), color(srgb-linear 0.16172 0.33473 0.42114 / 1), color(srgb-linear 0.00588 0.08585 0.12579 / 1), color(srgb-linear 0 0.1232 0.24795 / 1), color(srgb-linear 0 0.05257 0.08987 / 1), color(srgb-linear 0.17036 0.07355 0.08189 / 1), color(srgb-linear 0.7694 0.30654 0.34642 / 1)]\n

If desired, any of the three available methods can be used.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('tritan', method='brettel').clip() for c in colors])\n[color(srgb-linear 1 0 0.07589 / 1), color(srgb-linear 1 0.12293 0.20142 / 1), color(srgb-linear 1 0.46132 0.5152 / 1), color(srgb-linear 1 0.85569 0.88089 / 1), color(srgb-linear 0.16172 0.33473 0.42114 / 1), color(srgb-linear 0.00588 0.08585 0.12579 / 1), color(srgb-linear 0 0.1232 0.24795 / 1), color(srgb-linear 0 0.05257 0.08987 / 1), color(srgb-linear 0.17036 0.07355 0.08189 / 1), color(srgb-linear 0.7694 0.30654 0.34642 / 1)]\n>>> Steps([c.filter('tritan', method='vienot').clip() for c in colors])\n[color(srgb-linear 1 0 0 / 1), color(srgb-linear 1 0.13398 0.13398 / 1), color(srgb-linear 1 0.46891 0.46891 / 1), color(srgb-linear 1 0.85924 0.85924 / 1), color(srgb-linear 0.14923 0.3469 0.3469 / 1), color(srgb-linear 0.00011 0.09147 0.09147 / 1), color(srgb-linear 0 0.14076 0.14076 / 1), color(srgb-linear 0 0.05782 0.05782 / 1), color(srgb-linear 0.16915 0.07473 0.07473 / 1), color(srgb-linear 0.76363 0.31216 0.31216 / 1)]\n>>> Steps([c.filter('tritan', method='machado').clip() for c in colors])\n[color(srgb-linear 1 0 0.00473 / 1), color(srgb-linear 1 0.06673 0.11254 / 1), color(srgb-linear 1 0.42955 0.38203 / 1), color(srgb-linear 1 0.8524 0.6961 / 1), color(srgb-linear 0.08307 0.36867 0.27955 / 1), color(srgb-linear 0 0.09865 0.09092 / 1), color(srgb-linear 0 0.1476 0.3039 / 1), color(srgb-linear 0 0.05813 0.12498 / 1), color(srgb-linear 0.20711 0.06178 0.13387 / 1), color(srgb-linear 0.90348 0.26694 0.41821 / 1)]\n
"},{"location":"filters/#anomalous-trichromacy","title":"Anomalous Trichromacy","text":"

While Dichromacy is probably the more severe case with only two functional cones, a more common CVD type is anomalous trichromacy. In this case, a person will have three functioning cones, but not all of the cones function with full sensitivity. Sometimes, the sensitivity can be so low, that their ability to perceive color may be close to someone with dichromacy.

While dichromacy may be considered a severity 1, a given case of anomalous trichromacy could be anywhere between 0 and 1, where 0 would be no CVD.

Like dichromacy, the related deficiencies are named in a similar manner: protanomaly (reduced red sensitivity), deuteranomaly (reduced green sensitivity), and tritanomaly (reduced blue sensitivity).

NormalProtanomaly Severity 0.5Protanomaly Severity 0.7Protanomaly Severity 0.9

To represent anomalous trichromacy, ColorAide leans on the Machado 2009 approach which has a more nuanced approach to handling severity levels below 1. This research associated with this method did not really focus on tritanopia though, and Brettel is still a better choice for tritanopia. Instead of relying on the Machado approach for tritanomaly, we instead just use linear interpolation between the severity 1 results and the severity 0 (no CVD) results.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('protan', 0.3).clip() for c in colors])\n[color(srgb-linear 0.63032 0.06918 0 / 1), color(srgb-linear 0.70293 0.20796 0 / 1), color(srgb-linear 0.88443 0.5549 0 / 1), color(srgb-linear 1 0.95923 0 / 1), color(srgb-linear 0.24525 0.36562 0 / 1), color(srgb-linear 0.03392 0.08521 0.09141 / 1), color(srgb-linear 0 0.04077 1 / 1), color(srgb-linear 0 0.01895 0.41633 / 1), color(srgb-linear 0.11396 0.05262 0.3851 / 1), color(srgb-linear 0.56082 0.29269 0.85987 / 1)]\n>>> Steps([c.filter('protan', 0.5).clip() for c in colors])\n[color(srgb-linear 0.45806 0.09279 0 / 1), color(srgb-linear 0.56403 0.22475 0 / 1), color(srgb-linear 0.82893 0.55464 0 / 1), color(srgb-linear 1 0.9391 0 / 1), color(srgb-linear 0.31598 0.35011 0 / 1), color(srgb-linear 0.04973 0.08304 0.09151 / 1), color(srgb-linear 0 0.0609 1 / 1), color(srgb-linear 0 0.02798 0.42051 / 1), color(srgb-linear 0.06528 0.06444 0.38853 / 1), color(srgb-linear 0.42566 0.32032 0.86561 / 1)]\n>>> Steps([c.filter('protan', 0.9).clip() for c in colors])\n[color(srgb-linear 0.20388 0.11298 0 / 1), color(srgb-linear 0.3583 0.23687 0 / 1), color(srgb-linear 0.74433 0.54658 0 / 1), color(srgb-linear 1 0.90752 0 / 1), color(srgb-linear 0.41835 0.33104 0 / 1), color(srgb-linear 0.07305 0.08116 0.09129 / 1), color(srgb-linear 0 0.09248 1 / 1), color(srgb-linear 0 0.04159 0.42961 / 1), color(srgb-linear 0 0.07967 0.39681 / 1), color(srgb-linear 0.22933 0.35303 0.88092 / 1)]\n

The Brettel and Vi\u00e9not approach can be used for severities below 1 as well, but, like Brettel with tritanopia, they will employ simple linear interpolation between a severity 1 case ans the actual color. It is probably debatable as to whether this approach is sufficient or not.

"},{"location":"filters/#usage-details","title":"Usage Details","text":"

To use filters, a filter name must be given, followed by an optional amount. If an amount is omitted, suitable default will be used. The exact range a given filter accepts varies depending on the filter. If a value exceeds the filter range , the value will be clamped.

Filters Name Default Brightness brightness 1 Saturation saturate 1 Contrast contrast 1 Opacity opacity 1 Invert invert 1 Hue\u00a0rotation hue-rotate 0 Sepia sepia 1 Grayscale grayscale 1 Protan protan 1 Deutan deutan 1 Tritan tritan 1

All of the filters that are supported allow filtering in the Linear sRGB color space and will do so by default. Additionally, the W3C filter effects also support filtering in the sRGB color space. The CVD filters are specifically designed to be applied in the Linear sRGB space, and cannot be used in any other color space.

>>> inputs = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet']\n>>> colors = Color.steps(inputs, steps=10, space='srgb')\n>>> Steps(colors)\n[color(srgb 1 0 0 / 1), color(srgb 1 0.43137 0 / 1), color(srgb 1 0.76471 0 / 1), color(srgb 1 1 0 / 1), color(srgb 0.33333 0.66797 0 / 1), color(srgb 0 0.33464 0.33333 / 1), color(srgb 0 0 1 / 1), color(srgb 0.19608 0 0.6732 / 1), color(srgb 0.50719 0.16993 0.65098 / 1), color(srgb 0.93333 0.5098 0.93333 / 1)]\n>>> Steps([c.filter('sepia', 1, space='srgb-linear').clip() for c in colors])\n[color(srgb-linear 0.393 0.349 0.272 / 1), color(srgb-linear 0.51291 0.45597 0.35526 / 1), color(srgb-linear 0.81266 0.72337 0.56342 / 1), color(srgb-linear 1 1 0.806 / 1), color(srgb-linear 0.34617 0.30866 0.2403 / 1), color(srgb-linear 0.08759 0.07808 0.0608 / 1), color(srgb-linear 0.189 0.168 0.131 / 1), color(srgb-linear 0.09017 0.08014 0.06249 / 1), color(srgb-linear 0.17767 0.15791 0.12308 / 1), color(srgb-linear 0.66927 0.59517 0.46377 / 1)]\n>>> Steps([c.filter('sepia', 1, space='srgb').clip() for c in colors])\n[color(srgb 0.393 0.349 0.272 / 1), color(srgb 0.72473 0.64492 0.50235 / 1), color(srgb 0.98106 0.87359 0.68035 / 1), color(srgb 1 1 0.806 / 1), color(srgb 0.64467 0.57456 0.44736 / 1), color(srgb 0.32034 0.28556 0.22236 / 1), color(srgb 0.189 0.168 0.131 / 1), color(srgb 0.20429 0.18153 0.14152 / 1), color(srgb 0.45304 0.40295 0.31398 / 1), color(srgb 0.93524 0.83226 0.64837 / 1)]\n

Processing Lots of Colors

One logical application for filters is to apply them directly to images. If you are performing these operations on millions of pixels, you may notice that ColorAide, with all of its convenience, may not always be the fastest. There is a cost due to the overhead of convenience and a cost due to the pure Python approach as well. With that said, there are tricks that can dramatically make things much faster in most cases!

functools.lru_cache is your friend in such cases. We actually process all the images on this page with ColorAide to demonstrate the filters. The key to making it a quick and painless process was to cache repetitive operations. When processing images, it is highly likely that you will be performing the same operations on thousands of identical pixels. Caching the work you've already done can speed this process up exponentially.

There are certainly some images that could be constructed in such a way to elicit a worse case scenario where the cache would not be able to compensate as well, but for most images, caching dramatically reduces processing time.

We can crawl the pixels in a file, and using a simple function like below, we will only process a pixel once (at least until our cache fills and we start having to overwrite existing colors).

@lru_cache(maxsize=1024 * 1024)\ndef apply_filter(name, amount, space, method, p, fit):\n\"\"\"Apply filter.\"\"\"\n\n    has_alpha = len(p) > 3\n    color = Color('srgb', [x / 255 for x in p[:3]], p[3] / 255 if has_alpha else 1)\n    if method is not None:\n        # This is a CVD filter that allows specifying the method\n        color.filter(name, amount, space=space, in_place=True, method=method)\n    else:\n        # General filter.\n        color.filter(name, amount, space=space, in_place=True)\n    # Fit the color back into the color gamut and return the results\n    return tuple([int(x * 255) for x in color.fit(method=fit)[:3 if has_alpha else -1]])\n

When processing a 4608x2456 image (15,925,248 pixels) during our testing, it turned a ~7 minute process into a ~25 second process*. Using gamut mapping opposed to simple clipping only increases time by to about ~56 seconds. The much smaller images shown on this page process much, much faster.

The full script can be viewed here.

* Tests were performed using the Pillow library. Results may vary depending on the size of the image, pixel configuration, number of unique pixels, etc. Cache size can be tweaked to optimize the results.

"},{"location":"gamut/","title":"Gamut Mapping","text":"

Many color spaces are designed in such a way that they can only represent colors accurately within a specific range. This range in which a color can accurately be represented is known as the color gamut. While some color spaces are theoretically unbounded, there are many that are designed with distinct ranges.

The sRGB and Display P3 color spaces are both RGB color spaces, but they actually can represent a different amount of colors. Display P3 has a wider gamut and allows for greener greens and redder reds, etc. In the image below, we show four different RGB color spaces, each with varying different gamut sizes. Display P3 contains all the colors in sRGB and extends it even further. Rec. 2020, another RGB color space, is even wider. ProPhoto is so wide that it contains colors that the human eye can't even see.

In order to visually represent a color from a wider gamut color space, such as Display P3, in a more narrow color space, such as sRGB, a suitable color within the more narrow color space must must be selected and be shown in its place. This selecting of a suitable replacement is called gamut mapping.

ColorAide defines a couple methods to help identify when a color is outside the gamut bounds of a color space and to help find a suitable, alternative color that is within the gamut.

"},{"location":"gamut/#checking-gamut","title":"Checking Gamut","text":"

When dealing with colors, it can be important to know whether a color is within its own gamut. The in_gamut function allows for comparing the current color's specified values against the color space's gamut.

Let's assume we have a color rgb(30% 105% 0%). The color is out of gamut due to the green channel exceeding the channel's limit of 100%. When we execute in_gamut, we can see that the color is not in its own gamut.

>>> Color(\"rgb(30% 105% 0%)\").in_gamut()\nFalse\n

On the other hand, some color spaces do not have a limit. CIELab is one such color space. Sometimes limits will be placed on the color space channels for practicality, but theoretically, there are no bounds. When we check a CIELab color, we will find that it is always considered in gamut.

>>> Color(\"lab(200% -20 40 / 1)\").in_gamut()\nTrue\n

While checking CIELab's own gamut isn't very useful, we can test it against a different color space's gamut. By simply passing in the name of a different color space, the current color will be converted to the provided space and then will run in_gamut on the new color. You could do this manually, but using in_gamut in this manner can be very convenient. In the example below, we can see that the CIELab color of lab(200% -20 40 / 1) is outside the narrow gamut of sRGB.

>>> Color(\"lab(200% -20 40 / 1)\").in_gamut('srgb')\nFalse\n
"},{"location":"gamut/#tolerance","title":"Tolerance","text":"

Generally, ColorAide does not round off values in order to guarantee the best possible values for round tripping, but due to limitations of floating-point arithmetic and precision of conversion algorithms, there can be edge cases where colors don't round trip perfectly. By default, in_gamut allows for a tolerance of 0.000075 to account for such cases where a color is \"close enough\". If desired, this \"tolerance\" can be adjusted.

Let's consider CIELab with a D65 white point. The sRGB round trip through CIELab D65 for white does not perfectly convert back to the original color. This is due to the perils of floating point arithmetic.

>>> Color('color(srgb 1 1 1)').convert('lab-d65')[:]\n[100.0, 0.0, 0.0, 1.0]\n>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb')[:]\n[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]\n

We can see that when using a tolerance of zero, and gamut checking in sRGB, that the color is considered out of gamut. This makes sense as the round trip through CIELab D65 and back is so very close, but ever so slightly off. Depending on what you are doing, this may not be an issue up until you are ready to finalize the color, so sometimes it may be desirable to have some tolerance, and other times not.

>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb')[:]\n[0.9999999999999999, 0.9999999999999999, 0.9999999999999997, 1.0]\n>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb').in_gamut()\nTrue\n>>> Color('color(srgb 1 1 1)').convert('lab-d65').convert('srgb').in_gamut(tolerance=0)\nTrue\n

On the topic of tolerance, lets consider some color models that do not handle out of gamut colors very well. There are some color models that are alternate representations of an existing color space. For instance, the cylindrical spaces HSL, HSV, and HWB are just different color models for the sRGB color space. They are are essentially the sRGB color space, just with cylindrical coordinates that isolate certain attributes of the color space: saturation, whiteness, blackness, etc. So their gamut is exactly the same as the sRGB space, because they are the sRGB color space. So it stands to reason that simply using the sRGB gamut check for them should be sufficient, and if we are using strict tolerance, this would be true.

>>> Color('rgb(255 255 255)').in_gamut('srgb', tolerance=0)\nTrue\n>>> Color('hsl(0 0% 100%)').in_gamut('srgb', tolerance=0)\nTrue\n>>> Color('color(--hsv 0 0% 100%)').in_gamut('srgb', tolerance=0)\nTrue\n>>> Color('rgb(255.05 255 255)').in_gamut('srgb', tolerance=0)\nFalse\n>>> Color('hsl(0 0% 100.05%)').in_gamut('srgb', tolerance=0)\nFalse\n>>> Color('color(--hsv 0 0% 100.05%)').in_gamut('srgb', tolerance=0)\nFalse\n

But when we are not using a strict threshold, and we check one of these models only using the sRGB gamut, there are some cases where these cylindrical colors can exhibit coordinates wildly outside of the model's range but still very close to the sRGB gamut.

In this example, we have an sRGB color that is extremely close to being in gamut, but when we convert it to HSL, we can see wildly large saturation.

>>> hsl = Color('color(srgb 1 1.000002 1)').convert('hsl')\n>>> hsl.to_string(fit=False)\n'hsl(120 -100% 100%)'\n>>> hsl.in_gamut('srgb')\nTrue\n

This happens because these cylindrical color models do not represent colors in a very sane way when lightness exceeds the SDR range of 0 - 1. They are simply not designed to extend past such limits in a sane way. So even a slightly out of gamut sRGB color could translate to a value way outside the cylindrical color model's boundaries.

For this reason, gamut checks in the HSL, HSV, or HWB models apply tolerance checks on the color's coordinates in the sRGB color space and the respective cylindrical model ensuring we have coordinates that are close to the color's actual gamut and reasonably close to the cylindrical model's constraints as well.

So, when using HSL as the gamut check, we can see that it ensures the color is not only very close to the sRGB gamut, but that it is also very close the color model's constraints.

>>> hsl = Color('color(srgb 0.9999999999994 1.0000000000002 0.9999999999997)').convert('hsl')\n>>> hsl\ncolor(--hsl none 2.0006 1 / 1)\n>>> hsl.in_gamut('hsl')\nFalse\n

If the Cartesian check is the only desired check, and the strange cylindrical values that are returned are not a problem, srgb can always be specified. tolerance=0 can also be used to constrain the check to values exactly in the gamut.

HSL has a very tight conversion to and from sRGB, so when an sRGB color is precisely in gamut, it will remain in gamut throughout the conversion to and from HSL, both forwards and backwards. On the other hand, there may be color models that have a looser conversion algorithm. There may be cases where it may be beneficial to increase the threshold.

"},{"location":"gamut/#gamut-mapping-colors","title":"Gamut Mapping Colors","text":"

Gamut mapping is the process of taking a color that is out of gamut and adjusting it such that it fits within the gamut. There are various ways to map an out of bound color to an in bound color, each with their own pros and cons. ColorAide offers two methods related to gamut mapping: clip() and fit(). clip() is a dedicated function that performs the speedy, yet naive, approach of simply truncating a color channel's value to fit within the specified gamut, and fit() is a method that allows you to do more advanced gamut mapping approaches that, while slower, generally yield better results.

While clipping won't always yield the best results, clipping is still very important and can be used to trim channel noise after certain mathematical operations or even used in other gamut mapping algorithms if used carefully. For this reason, clip has its own dedicated method for quick access: clip().

>>> Color('rgb(270 30 120)').clip()\ncolor(srgb 1 0.11765 0.47059 / 1)\n

The fit() method, is the generic gamut mapping method that exposes access to all the different gamut mapping methods available. By default, fit() uses a more advanced method of gamut mapping that tries to preserve hue and lightness, hue being the attribute the human eye is most sensitive to. If desired, a user can also specify any currently registered gamut mapping algorithm via the method parameter.

>>> Color('rgb(270 30 120)').fit()\ncolor(srgb 1 0.18505 0.47435 / 1)\n>>> Color('rgb(270 30 120)').fit(method='clip')\ncolor(srgb 1 0.11765 0.47059 / 1)\n

Gamut mapping can also be used to indirectly fit colors in another gamut. For instance, fitting a Display P3 color into an sRGB gamut.

>>> c1 = Color('color(display-p3 1 1 0)')\n>>> c1.in_gamut('srgb')\nFalse\n>>> c1.fit('srgb')\ncolor(display-p3 0.99859 0.9923 0.32854 / 1)\n>>> c1.in_gamut()\nTrue\n

This can also be done with clip().

>>> Color('color(display-p3 1 1 0)').clip('srgb')\ncolor(display-p3 1 1 0.3309 / 1)\n

Indirectly Gamut Mapping a Color Space

When indirectly gamut mapping in another color space, results may vary depending on what color space you are in and what color space you are using to fit the color. The operation may not get the color precisely in gamut. This is because we must convert the color to the gamut mapping space, apply the gamut mapping, and then convert it back to the original color. The process will be subject to any errors that occur in the round trip to and from the targeted space. This is mainly mentioned as fitting in one color space and round tripping back may not give exact results and, in some cases, exceed \"in gamut\" thresholds.

There are actually many different ways to gamut map a color. Some are computationally expensive, some are quite simple, and many do really good in some cases and not so well in others. There is probably no perfect gamut mapping method, but some are better than others.

"},{"location":"gamut/#clip","title":"Clip","text":"

The clip gamut mapping is registered in Color by default and cannot be unregistered

Clipping is a simple and naive approach to gamut mapping. If the color space is bounded by a gamut, clip will compare each channel's value against the bounds for that channel set the value to the limit it exceeds.

Clip can be performed via fit by using the method name clip or by using the clip() method.

>>> c = Color('srgb', [2, 1, 1.5])\n>>> c.fit(method='clip')\ncolor(srgb 1 1 1 / 1)\n>>> c = Color('srgb', [2, 1, 1.5])\n>>> c.clip()\ncolor(srgb 1 1 1 / 1)\n

Clipping is unique to all other clipping methods in that it has its own dedicated method clip() method and that its method name clip is reserved. While not always the best approach for gamut mapping in general, clip is very important to some other gamut mapping and has specific cases where its speed and simplicity are of great value.

"},{"location":"gamut/#lch-chroma","title":"LCh Chroma","text":"

The lch-chroma gamut mapping is registered in Color by default

LCh Chroma uses a combination of chroma reduction and MINDE in the CIELCh color space to bring a color into gamut. By reducing chroma in the CIELCh color space, LCh Chroma can hold hue and lightness in the LCh color space relatively constant. This is currently the default method used.

Note

As most colors in ColorAide use a D65 white point by default, LCh D65 is used as the gamut mapping color space.

The algorithm generally works by performing both clipping and chroma reduction. Using bisection, the chroma is reduced and then the chroma reduced color is clipped. Using \u2206E2000, the distance between the chroma reduced color and the clipped chroma reduced color is measured. If the resultant distance falls within the specified threshold, the clipped color is returned.

Computationally, LCh Chroma is slower to compute than clipping, but generally provides better results. LCh, is not necessarily the best perceptual color space available, but it is generally well understood color space that has been available a long time. It does suffer from a purple shift when dealing with blue colors, but generally can generally colors far out of gamut in a reasonable manner.

While CSS has currently proposed LCh Chroma reduction to be done with OkLCh, and we do offer an OkLCh variant, we currently still use CIELCh as the default until OkLCh can be evaluated more fully.

LCh Chroma is the default gamut mapping algorithm by default, unless otherwise changed, and can be performed by simply calling fit() or by calling fit(method='lch-chroma').

>>> c = Color('srgb', [2, -1, 0])\n>>> c.fit(method='clip')\ncolor(srgb 1 0 0 / 1)\n>>> c = Color('srgb', [2, -1, 0])\n>>> c.fit(method='clip')\ncolor(srgb 1 0 0 / 1)\n
"},{"location":"gamut/#oklch-chroma","title":"OkLCh Chroma","text":"

The lch-chroma gamut mapping is registered in Color by default

The CSS CSS Color Level 4 specification currently recommends using OkLCh as the gamut mapping color space. OkLCh Chroma is performed exactly like LCh Chroma except that it uses the perceptually uniform OkLCh color space as the LCh color space of choice.

OkLCh has the advantage of doing a better job at holding hues uniform than CIELCh.

>>> c = Color('srgb', [2, -1, 0])\n>>> c.fit(method='oklch-chroma')\ncolor(srgb 1 0.60354 0.66617 / 1)\n

OkLCh is a very new color space to be used in the field of gamut mapping. While CIELCh is not perfect, its weakness are known. OkLCh does seem to have certain quirks of its own, and may have more that have yet to be discovered. While we have not made oklch-chroma our default yet, we have exposed the algorithm so users can begin exploring it.

"},{"location":"gamut/#hct-chroma","title":"HCT Chroma","text":"

The hct-chroma gamut mapping is not registered in Color by default

Much like the other LCh chroma reduction algorithms, HCT Chroma performs gamut mapping exactly like LCh Chroma with the exception that it uses the HCT color space as the working LCh color space.

Google's Material Design uses a new color space called HCT. It uses the hue and chroma from CAM16 (JMh) space and the tone/lightness from the CIELab space. HCT takes advantage of the good hue preservation of CAM16 and has the better lightness predictability of CIELab. Using these characteristics, the color space is adept at generating tonal palettes with predictable lightness. This makes it easier to construct UIs with decent contrast. But to do this well, you must work in HCT and gamut map in HCT. For this reason, the HCT Chroma gamut mapping method was added.

HCT Chroma is computationally the most expensive gamut mapping method that is offered. Since the color space used is based on the already computationally expensive CAM16 color space, and is made more expensive by blending that color space with CIELab, it is not the most performant approach, but when used in conjunction with the HCT color space, it can allow creating good tonal palettes:

>>> c = Color('hct', [325, 24, 50])\n>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit='hct-chroma') for tone in tones])\n['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']\n

To HCT Chroma plugin is not registered by default, but can be added by subclassing Color. You must register the \u2206Ehct distancing algorithm and the HCT color space as well.

from coloraide import Color as Base\nfrom coloraide.gamut.fit_hct_chroma import HCTChroma\nfrom coloraide.distance.delta_e_hct import DEHCT\nfrom coloraide.spaces.hct import HCT\n\nclass Color(Base): ...\n\nColor.register([HCT(), DEHCT(), HCTChroma()])\n
"},{"location":"gamut/#why-not-just-clip","title":"Why Not Just Clip?","text":"

In the past, clipping has been the default way in which out of gamut colors have been handled in web browsers. It is fast, and has generally been fine as most browsers have been constrained to using sRGB. But as modern browsers begin to adopt more wide gamut monitors such as Display P3, and CSS grows to support an assortment of wide and ultra wide color spaces, representing the best intent of an out of gamut color becomes even more important.

ColorAide currently uses a default gamut mapping algorithm that performs gamut mapping in the CIELCh color space using chroma reduction coupled with minimum \u2206E (MINDE). This approach is meant to preserve enough of the important attributes of the out of gamut color as is possible, mostly preserving both lightness and hue, hue being the attribute that people are most sensitive to. MINDE is used to abandon chroma reduction and clip the color when the color is very close to being in gamut. MINDE also allows us to catch cases where the geometry of the color space's gamut is such that we may slip by higher chroma options resulting in undesirable, aggressive chroma reduction. While CIELCh is not a perfect color space, and we may use a different color space in the future, this method is generally more accurate that using clipping alone.

Below we have an example of using chroma reduction with MINDE. It can be noted that chroma is reduced until we are very close to being in gamut. The MINDE helps us catch the peak of the yellow shape as, otherwise, we would have continued reducing chroma until we were at a very chroma reduced, pale yellow.

One might see some cases of clipping and think it does a fine job and question why any of this complexity is necessary. In order to demonstrate the differences in gamut mapping vs clipping, see the example below. We start with the color color(display-p3 1 1 0) and interpolate with it in the CIELCh color space reducing just the lightness. This will leave both chroma and hue intact. The Interactive playground below automatically gamut maps the color previews to sRGB, but we'll control the method being used by providing two different Color objects: one that uses lch-chroma (the default) for gamut mapping, and one that uses clip. Notice how clipping, the bottom color set, clips these dark colors and makes them reddish. This is a very undesirable outcome.

>>> yellow = Color('color(display-p3 1 1 0)')\n>>> lightness_mask = Color('lch(0% none none)')\n>>> Row([c.fit('srgb') for c in Color.steps([yellow, lightness_mask], steps=10, space='lch')])\n[color(--lch 97.084 94.208 99.059 / 1), color(--lch 86.627 85.37 97.728 / 1), color(--lch 75.806 76.349 97.429 / 1), color(--lch 64.98 67.345 97.418 / 1), color(--lch 54.144 58.334 97.439 / 1), color(--lch 43.284 49.307 97.535 / 1), color(--lch 32.387 40.256 97.872 / 1), color(--lch 21.425 29.815 98.42 / 1), color(--lch 10.15 15.125 97.724 / 1), color(--lch 0 0 none / 1)]\n>>> yellow = Color('color(display-p3 1 1 0)')\n>>> lightness_mask = Color('lch(0% none none)')\n>>> Row([c.clip('srgb') for c in Color.steps([yellow, lightness_mask], steps=10, space='lch')])\n[color(--lch 97.607 94.712 99.572 / 1), color(--lch 86.823 85.836 100.4 / 1), color(--lch 76.017 76.983 101.52 / 1), color(--lch 65.223 68.164 102.85 / 1), color(--lch 54.47 59.371 104.26 / 1), color(--lch 43.793 50.569 105.45 / 1), color(--lch 33.274 41.659 105.35 / 1), color(--lch 23.119 31.812 100.43 / 1), color(--lch 13.922 21.135 76.154 / 1), color(--lch 4.9924 15.753 29.476 / 1)]\n

There are times when clipping is simply preferred. It is fast, and if you are just trimming noise off channels, it is very useful, but if the idea is to present an in gamut color that tries to preserve as much of the intent of the original color as possible, other methods may be desired. There are no doubt better gamut methods available than what ColorAide offers currently, and more may be added in the future, but ColorAide can also be extended using 3rd party plugins as well.

"},{"location":"gamut/#pointers-gamut","title":"Pointer's Gamut","text":"

New 2.4

The Pointer\u2019s gamut is (an approximation of) the gamut of real surface colors as can be seen by the human eye, based on the research by Michael R. Pointer (1980). What this means is that every color that can be reflected by the surface of an object of any material should be is inside the Pointer\u2019s gamut. This does not include, however, those that do not occur naturally, such as neon lights, etc.

While in the above image, it may appear that most of sRGB is in the gamut, it is important to note that the image is showing the maximum range of the gamut. The actual boundary will be different at different luminance levels.

The gamuts previously discussed are bound by a color space's limits, but the Pointer's gamut applies to colors more generally and was created from observed data via research. Because it doesn't quite fit with the color space gamut API, ColorAide exposes two special functions to test if a color is in the Pointer's gamut and to fit a color to the gamut.

To test if a color is within the gamut, simply call in_pointer_gamut():

>>> Color('red').in_pointer_gamut()\nFalse\n>>> Color('orange').in_pointer_gamut()\nTrue\n

ColorAide also provides a way to fit a color to the Pointer's gamut. The original gamut's data is described in LCh using illuminant C. Using this color space, we can estimate the chroma limit for any color based on it's lightness and hue. We can then reduce the chroma, preserving the lightness and hue. The image below shows the out of Pointer's gamut color red (indicated by the x) which is clamped to the Pointer's gamut by reducing the chroma (indicated by the dot).

ColorAide provides the fit_pointer_gamut() method to perform this \"fitting\" of the color.

>>> color = Color('red')\n>>> color\ncolor(srgb 1 0 0 / 1)\n>>> color.in_pointer_gamut()\nFalse\n>>> color.fit_pointer_gamut()\ncolor(srgb 0.95687 0.18251 0.09074 / 1)\n>>> color.in_pointer_gamut()\nTrue\n

Tip

Much like in_gamut(), in_pointer_gamut() allows adjusting tolerance as well via the tolerance parameter.

"},{"location":"harmonies/","title":"Color Harmonies","text":"

In color theory, color harmony refers to the property that certain aesthetically pleasing color combinations have. Modern day color theory probably starts with the first color wheel created by Isaac Newton. Based on his observations of light with prisms, he formed one the first color wheels. From there, many others built upon this work, sometimes with opposing ideas.

The original color wheel, while inspired by what was observed by light, was created based on experiments with pigments as well. As most know, in paint, red, yellow, and blue are considered primary colors. Newton thought this translated to light as well and stated they were also the primary colors of light. While this isn't actually true, his work was very important in reshaping how people viewed color.

Over time, the color wheel was refined. The traditional model, which we will call an RYB color model, defined 12 colors that made up the wheel: the primary colors, the secondary colors, and the tertiary colors. The secondary colors are created by evenly mixing the primary colors, and the tertiary colors are created by evenly mixing those primary colors with the secondary colors.

The idea of color harmonies originates from the idea that colors, based on their relative position on the wheel, can form more pleasing color combinations.

"},{"location":"harmonies/#which-color-space-is-best-for-color-harmonies","title":"Which Color Space is Best for Color Harmonies?","text":"

As we know, these days, there are many color spaces out there: subtractive models, additive models, perceptually uniform models, high dynamic range models, etc. Many color spaces trying to solve specific issues based on the knowledge at the time.

It should be noted, that the idea of primary colors stems from the idea that there are a set of pure colors from which all colors can be made from. If you've spent any time with paint, you will know that not all colors can be made from red, yellow, and blue. There are colors like cyan and magenta that cannot be made with the traditional primary colors. The early work that helped create the first color wheels was done with the limited paints that was available at the time, and the color harmony concepts were built upon the early RYB color model.

In modern TVs and monitors, the RYB color model is not used. Paint has subtractive properties, but light has additive properties. Electronic screens create all their colors with light based methods that mix red, green, and blue lights. In addition, the human eye perceives colors using red, green, and blue light as well. This is As far as light is concerned, the primary colors are red, green, and blue.

In reality, we could create a color wheel from any of the various color spaces out there and end up with different results. If we were to compose a color wheel based on the common sRGB color space, we could base it off the 3 primary colors of light. Starting with red (0\u02da), we could extract the colors at evenly spaced degrees, 30\u02da to be exact. This would give us our 12 colors for the sRGB color space.

>>> Steps([Color('hsl', [x, 1, 0.5]) for x in range(0, 360, 30)])\n[color(--hsl 0 1 0.5 / 1), color(--hsl 30 1 0.5 / 1), color(--hsl 60 1 0.5 / 1), color(--hsl 90 1 0.5 / 1), color(--hsl 120 1 0.5 / 1), color(--hsl 150 1 0.5 / 1), color(--hsl 180 1 0.5 / 1), color(--hsl 210 1 0.5 / 1), color(--hsl 240 1 0.5 / 1), color(--hsl 270 1 0.5 / 1), color(--hsl 300 1 0.5 / 1), color(--hsl 330 1 0.5 / 1)]\n

From this we can construct an sRGB color wheel.

This is different from the RYB color wheel we showed earlier, and more accurate in relation to how light works, but does it yield better harmonies for colors?

The sRGB color space is additive, just like light, but pigments are subtractive. We can use CMY to generate a subtractive wheel with a far greater range that red, green blue creates by use magenta, yellow, and cyan. But does this create better harmonies?

>>> Steps(Color('magenta').harmony('wheel', space='cmy'))\n[color(--cmy 0 1 0 / 1), color(--cmy 0 1 0.5 / 1), color(--cmy 0 1 1 / 1), color(--cmy 0 0.5 1 / 1), color(--cmy 0 0 1 / 1), color(--cmy 0.5 0 1 / 1), color(--cmy 1 0 1 / 1), color(--cmy 1 0 0.5 / 1), color(--cmy 1 0 0 / 1), color(--cmy 1 0.5 0 / 1), color(--cmy 1 1 0 / 1), color(--cmy 0.5 1 0 / 1)]\n

If we were to select the perceptually uniform OkLCh color space, and seed it with red's lightness and chroma, we'd get the wheel below.

>>> Steps(Color('red').harmony('wheel', space='oklch'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 59.234 / 1), color(--oklch 0.62796 0.25768 89.234 / 1), color(--oklch 0.62796 0.25768 119.23 / 1), color(--oklch 0.62796 0.25768 149.23 / 1), color(--oklch 0.62796 0.25768 179.23 / 1), color(--oklch 0.62796 0.25768 209.23 / 1), color(--oklch 0.62796 0.25768 239.23 / 1), color(--oklch 0.62796 0.25768 269.23 / 1), color(--oklch 0.62796 0.25768 299.23 / 1), color(--oklch 0.62796 0.25768 329.23 / 1), color(--oklch 0.62796 0.25768 359.23 / 1)]\n

This produces colors with visually more uniform lightness, does that mean these are better?

The truth is, what is better or even harmonious can be largely subjective, and everyone has reasons for selecting certain color spaces for a specific task.

Many artists swear by the limited, classical color wheel, others are fine with using the RGB color wheel as it is easy to work with in CSS via the HSL color space, and there are still others that are more interested in perceptually uniform color spaces that aim for more consistent hues and predictable lightness.

As far as ColorAide is concerned, we've chosen to use OkLCh as the color space in which we work in. This is based mainly on the fact that it keeps hue more consistent than some other options, and it allows us to support a wider gamut than options like HSL.

>>> Steps(Color.steps(['black', 'blue', 'white'], steps=11, space='oklch'))\n[color(--oklch 0 0 264.05 / 1), color(--oklch 0.0904 0.06264 264.05 / 1), color(--oklch 0.18081 0.12529 264.05 / 1), color(--oklch 0.27121 0.18793 264.05 / 1), color(--oklch 0.36161 0.25057 264.05 / 1), color(--oklch 0.45201 0.31321 264.05 / 1), color(--oklch 0.56161 0.25057 264.05 / 1), color(--oklch 0.67121 0.18793 264.05 / 1), color(--oklch 0.78081 0.12529 264.05 / 1), color(--oklch 0.8904 0.06264 264.05 / 1), color(--oklch 1 0 264.05 / 1)]\n>>> Steps(Color.steps(['black', 'blue', 'white'], steps=11, space='hsl'))\n[color(--hsl 240 0 0 / 1), color(--hsl 240 0.2 0.1 / 1), color(--hsl 240 0.4 0.2 / 1), color(--hsl 240 0.6 0.3 / 1), color(--hsl 240 0.8 0.4 / 1), color(--hsl 240 1 0.5 / 1), color(--hsl 240 0.8 0.6 / 1), color(--hsl 240 0.6 0.7 / 1), color(--hsl 240 0.4 0.8 / 1), color(--hsl 240 0.2 0.9 / 1), color(--hsl 240 0 1 / 1)]\n>>> Steps(Color.steps(['black', 'blue', 'white'], steps=11, space='lch'))\n[color(--lch 0 0 301.36 / 1), color(--lch 5.9137 26.24 301.36 / 1), color(--lch 11.827 52.481 301.36 / 1), color(--lch 17.741 78.721 301.36 / 1), color(--lch 23.655 104.96 301.36 / 1), color(--lch 29.568 131.2 301.36 / 1), color(--lch 43.655 104.96 301.36 / 1), color(--lch 57.741 78.721 301.36 / 1), color(--lch 71.827 52.481 301.36 / 1), color(--lch 85.914 26.24 301.36 / 1), color(--lch 100 0 301.36 / 1)]\n

While OkLCh is the default, we understand that there are many reasons to use other spaces, so use what you like, we won't judge . If you are a color theory purist, you can use the classical RYB model.

>>> Steps(Color('red').harmony('complement'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 209.23 / 1)]\n>>> Steps(Color('ryb', [1, 0, 0]).harmony('complement', space='ryb'))\n[color(--ryb 1 0 0 / 1), color(--ryb 0 1 1 / 1)]\n

RYB Model

The RYB model has a more limited color gamut than sRGB as the red, yellow and blue primaries cannot make all colors. Additionally, the red, yellow, and blue primaries are not the same as the ones in sRGB, so when using RYB to generate harmonies, make sure you are working directly within RYB to ensure you are not out of gamut.

Tip

harmony() can output the results in any color space you need by setting out_space.

>>> Steps(Color('red').harmony('complement', out_space='srgb'))\n[color(srgb 1 0 0 / 1), color(srgb -0.56631 0.66342 0.85808 / 1)]\n
"},{"location":"harmonies/#supported-harmonies","title":"Supported Harmonies","text":"

ColorAide currently supports 7 theorized color harmonies: monochromatic, complementary, split complementary, analogous, triadic, square, and rectangular. By default, all color harmonies are calculated with the perceptually uniform OkLCh color space, but other color spaces can be used if desired.

"},{"location":"harmonies/#monochromatic","title":"Monochromatic","text":"

The monochromatic harmony pairs various tints and shades of a color together to create pleasing color schemes.

>>> Steps(Color('red').harmony('mono'))\n[color(--oklch 0.37677 0.15461 29.234 / 1), color(--oklch 0.50236 0.20615 29.234 / 1), color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.70236 0.20615 29.234 / 1), color(--oklch 0.77677 0.15461 29.234 / 1)]\n

Achromatic Colors

Pure white and black will not be included in a monochromatic color harmony unless the color is achromatic.

"},{"location":"harmonies/#complementary","title":"Complementary","text":"

Complementary harmonies use a dyad of colors at opposite ends of the color wheel.

>>> Steps(Color('red').harmony('complement'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 209.23 / 1)]\n
"},{"location":"harmonies/#split-complementary","title":"Split Complementary","text":"

Split Complementary is similar to complementary, but actually uses a triad of colors. Instead of just choosing one complement, it splits and chooses two colors on the opposite side that are close, but not adjacent.

>>> Steps(Color('red').harmony('split'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 239.23 / 1), color(--oklch 0.62796 0.25768 -180.77 / 1)]\n
"},{"location":"harmonies/#analogous","title":"Analogous","text":"

Analogous harmonies consists of 3 adjacent colors.

>>> Steps(Color('red').harmony('analogous'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 59.234 / 1), color(--oklch 0.62796 0.25768 -0.76612 / 1)]\n
"},{"location":"harmonies/#triadic","title":"Triadic","text":"

Triadic draws an equilateral triangle between 3 colors on the color wheel. For instance, the primary colors have triadic harmony.

>>> Steps(Color('red').harmony('triad'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 149.23 / 1), color(--oklch 0.62796 0.25768 269.23 / 1)]\n
"},{"location":"harmonies/#tetradic-square","title":"Tetradic Square","text":"

Tetradic color harmonies refer to a group of four colors. One tetradic color harmony can be found by drawing a square between four colors on the color wheel.

>>> Steps(Color('red').harmony('square'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 119.23 / 1), color(--oklch 0.62796 0.25768 209.23 / 1), color(--oklch 0.62796 0.25768 299.23 / 1)]\n
"},{"location":"harmonies/#tetradic-rectangular","title":"Tetradic Rectangular","text":"

The rectangular tetradic harmony is very similar to the square tetradic harmony except that it draws a rectangle between four colors instead of a square.

>>> Steps(Color('red').harmony('rectangle'))\n[color(--oklch 0.62796 0.25768 29.234 / 1), color(--oklch 0.62796 0.25768 59.234 / 1), color(--oklch 0.62796 0.25768 209.23 / 1), color(--oklch 0.62796 0.25768 239.23 / 1)]\n
"},{"location":"harmonies/#others","title":"Others","text":"

If you have a particular configuration that you are after that is not covered under the defaults, you can use harmony to calculate your own. Simply use the wheel harmony that can generate a color wheel of any size. Simply use a color to seed the wheel, specify the space in which to generate the wheel. Optionally, provide the desired number of colors in the color wheel via the count argument. We can generate a wheel for any color (assuming the color space can properly handle the color). We can even generate an extended color wheel if so desired.

>>> Steps(Color('ryb', [1, 0, 0]).harmony('wheel', space='ryb', count=48))\n[color(--ryb 1 0 0 / 1), color(--ryb 1 0.125 0 / 1), color(--ryb 1 0.25 0 / 1), color(--ryb 1 0.375 0 / 1), color(--ryb 1 0.5 0 / 1), color(--ryb 1 0.625 0 / 1), color(--ryb 1 0.75 0 / 1), color(--ryb 1 0.875 0 / 1), color(--ryb 1 1 0 / 1), color(--ryb 0.875 1 0 / 1), color(--ryb 0.75 1 0 / 1), color(--ryb 0.625 1 0 / 1), color(--ryb 0.5 1 0 / 1), color(--ryb 0.375 1 0 / 1), color(--ryb 0.25 1 0 / 1), color(--ryb 0.125 1 0 / 1), color(--ryb 0 1 0 / 1), color(--ryb 0 1 0.125 / 1), color(--ryb 0 1 0.25 / 1), color(--ryb 0 1 0.375 / 1), color(--ryb 0 1 0.5 / 1), color(--ryb 0 1 0.625 / 1), color(--ryb 0 1 0.75 / 1), color(--ryb 0 1 0.875 / 1), color(--ryb 0 1 1 / 1), color(--ryb 0 0.875 1 / 1), color(--ryb 0 0.75 1 / 1), color(--ryb 0 0.625 1 / 1), color(--ryb 0 0.5 1 / 1), color(--ryb 0 0.375 1 / 1), color(--ryb 0 0.25 1 / 1), color(--ryb 0 0.125 1 / 1), color(--ryb 0 0 1 / 1), color(--ryb 0.125 0 1 / 1), color(--ryb 0.25 0 1 / 1), color(--ryb 0.375 0 1 / 1), color(--ryb 0.5 0 1 / 1), color(--ryb 0.625 0 1 / 1), color(--ryb 0.75 0 1 / 1), color(--ryb 0.875 0 1 / 1), color(--ryb 1 0 1 / 1), color(--ryb 1 0 0.875 / 1), color(--ryb 1 0 0.75 / 1), color(--ryb 1 0 0.625 / 1), color(--ryb 1 0 0.5 / 1), color(--ryb 1 0 0.375 / 1), color(--ryb 1 0 0.25 / 1), color(--ryb 1 0 0.125 / 1)]\n
"},{"location":"harmonies/#changing-the-default-harmony-color-space","title":"Changing the Default Harmony Color Space","text":"

New 2.7

Non-cylindrical space support was added in 2.7.

If you'd like to change the Color() class's default harmony color space, it can be done with class override. Simply derive a new Color() class from the original and override the HARMONY property with the name of a suitable color space. Color spaces must be either a cylindrical space, a Lab-like color space, or what we will call a regular, rectangular space. By \"regular\" we mean a normal 3 channel color space usually with a range of [0, 1]. Afterwards, all color harmony calculations will use the specified color space unless overridden via the method's space parameter.

>>> class Custom(Color):\n...     HARMONY = 'hsl'\n... \n>>> Steps(Custom('red').harmony('split'))\n[color(--hsl 0 1 0.5 / 1), color(--hsl 210 1 0.5 / 1), color(--hsl -210 1 0.5 / 1)]\n

Warning

Remember that every color space is different. Some may rotate hues in a different direction and some may just not be very compatible for extracting harmonies from.

Additionally, a color space may not handle colors beyond its gamut well, for such color spaces, it is important to work within that spaces gamut opposed to picking colors outside of the gamut and relying on gamut mapping.

"},{"location":"interpolation/","title":"Color Interpolation","text":"

Interpolation is a type of estimation that finds new data points based on the range of a discrete set of known data points. When used in the context of color, it is finding one or more colors that reside between any two given colors. This is often used to simulate mixing colors, creating gradients, or even create color palettes.

ColorAide provides a number of useful utilities based on interpolation.

"},{"location":"interpolation/#linear-interpolation","title":"Linear Interpolation","text":"

Linear interpolation is registered in Color by Default

One of the most common, and easiest ways to interpolate data between two points is to use linear interpolation. An easy way of thinking about this concept is to imagine drawing a straight line that connects two colors within a color space. We could then navigate along that line and return colors at different points to simulate mixing colors at various percentages or return the whole range and create a continuous, smooth gradient.

To further illustrate this point, the example below shows a slice of the Oklab color space at a lightness of 70%. On this 2D plane, we select two colors: oklab(0.7 0.15 0.1) and oklab(0.7 -0.03 -0.12). 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: oklab(0.7 0.06 -0.01).

Interpolation performed at 50%

The interpolate method allows a user to create a linear interpolation function using two or more colors. By default, a returned interpolation function accepts numerical input in the domain of [0, 1] and will cause a new color between the specified colors to be 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 discrete, steps, mix, etc.

As an example, below we create an interpolation between rebeccapurple and lch(85% 100 85). We then step through values of 0.0, 0.1, 0.2, etc. This returns colors at various positions on the line that connects the two colors, 0 returning rebeccapurple and 1 returning lch(85% 100 85).

>>> i = Color.interpolate([\"rebeccapurple\", \"lch(85% 100 85)\"], space='lch')\n>>> [i(x / 10).to_string() for x in range(10 + 1)]\n['lch(32.393 61.244 308.86)', 'lch(37.653 65.119 322.47)', 'lch(42.914 68.995 336.09)', 'lch(48.175 72.87 349.7)', 'lch(53.436 76.746 3.3143)', 'lch(58.696 80.622 16.929)', 'lch(63.957 84.497 30.543)', 'lch(69.218 88.373 44.157)', 'lch(74.479 92.249 57.771)', 'lch(79.739 96.124 71.386)', 'lch(85 100 85)']\n

If we create enough steps, we can create a gradient.

>>> i = Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 85)\"],\n...     space='lch'\n... )\n
"},{"location":"interpolation/#piecewise-interpolation","title":"Piecewise Interpolation","text":"

Piecewise interpolation takes the idea of linear interpolation and then applies it to multiple colors. As drawing a straight line through a series of points greater than two can be difficult to achieve, piecewise interpolation creates straight lines between each color in a chain of colors.

When the interpolate method receives more that two colors, the interpolation will utilize piecewise interpolation and interpolation will be broken up between each pair of colors. The function, just like when interpolating between two colors, still operates by default in the domain of [0, 1], only it will now apply to the entire range of colors.

Piecewise interpolation simply breaks up a series of data points into segments in order to apply interpolation individually on each segment.

>>> Color.interpolate(['black', 'red', 'white'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bff010>\n

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 at these locations.

"},{"location":"interpolation/#continuous-interpolation","title":"Continuous Interpolation","text":"

Continuous interpolation is registered in Color by Default

In this document, we use the term \"continuous\" in two ways when talking about interpolation: continuous vs discrete and the interpolation method whose literal name is continuous.

The interpolate method only creates continuous interpolations, meaning that for any point along the interpolation line, you will get a unique color. Continuous interpolation in this sense directly contrasts with with discrete interpolation which provides quantized color results where multiple inputs are associated with a limited set of colors along the interpolation line.

The continuous interpolation method is simply a piecewise, linear interpolation method that interpolates defined channels continuously across one more undefined channels.

Normal, piecewise interpolation only considers a single segments under interpolation at a time. When channels are undefined, the undefined channel on one end of a segment will adopt the value of the other defined channel on the other side of the segment, and if that other channel is also undefined, then any color interpolated between the two will also have no defined value for the channel. This approach to interpolation never considers any context beyond the segment it is looking at.

The continuous interpolation method is a linear piecewise approach created for ColorAide that will actually interpolate through undefined channels, using context from all the colors to be interpolated. What this means is that if you have multiple colors, and one or more of the colors have the same channel undefined, the colors with that channel defined will have those values interpolated across the undefined gaps across all the segments. This is probably better illustrated with an example.

In this example, we have 3 colors. The end colors both define lightness, but the middle color is undefined. We can see when we use normal, linear piecewise interpolation that we get a discontinuity. But with continuous linear interpolation, we get a smooth interpolation of the lightness through the undefined channel.

>>> colors = [\n...     Color('oklab', [0, 0, 0]),\n...     Color('oklab', [NaN, -0.03246, -0.31153]),\n...     Color('oklab', [1, 0, 0])\n... ]\n>>> Color.interpolate(colors, space='oklab', method='linear')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b77ed0>\n>>> Color.interpolate(colors, space='oklab', method='continuous')\n<coloraide.interpolate.continuous.InterpolatorContinuous object at 0x112bcf950>\n

Now, if have colors on the side that are not between two defined colors, all those colors will adopt the defined value of the one that is defined. This time we have a single color with all components defined, but all the colors to the left are missing the lightness. All colors with the undefined lightness will assume the lightness of the defined color.

>>> colors = [\n...     Color('oklab', [NaN, 0.22486, 0.12585]),\n...     Color('oklab', [NaN, -0.1403, 0.10768]),\n...     Color('oklab', [0.45201, -0.03246, -0.31153])\n... ]\n>>> Color.interpolate(colors, space='oklab', method='linear')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c69950>\n>>> Color.interpolate(colors, space='oklab', method='continuous')\n<coloraide.interpolate.continuous.InterpolatorContinuous object at 0x112bfc810>\n
"},{"location":"interpolation/#cubic-spline-interpolation","title":"Cubic Spline Interpolation","text":"

Linear interpolation is nice because it is easy to implement, and due to its straight forward nature, pretty fast. With that said, it doesn't always have the smoothest transitions. It turns out that there are other piecewise ways to interpolate that can yield smoother results.

Inspired by some efforts seen on the web and in the great JavaScript library Culori, ColorAide implements a number of spline based interpolation methods.

Because splines require taking into account more than two colors at a time, all spline based interpolation methods are built off of the continuous interpolation approach of handling undefined values.

"},{"location":"interpolation/#b-spline","title":"B-Spline","text":"

B-Spline interpolation is registered in Color by Default

B-spline is a piecewise spline similar to Bezier curves. It utilizes \"control points\" that help shape the interpolation path through a series of colors. Like Bezier Curves, the path does not pass through the control points, but it is clamped at the start and end. Essentially, the interpolation path passes through both end colors and bends that path along the way towards the other colors being used as control points.

It can be used by specifying bspline as the interpolation method.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='bspline')\n<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x112c8f0d0>\n
"},{"location":"interpolation/#natural","title":"Natural","text":"

Natural interpolation is registered in Color by Default

The \"natural\" spline is the same as the B-spline approach except an algorithm is applied that uses the colors as data points and calculates new control points such that the interpolation passes through all the data points. This means that the path will pass through all the colors. The resultant spline has the continuity and properties of a natural spline, hence the name.

One down side is that it can overshoot or undershoot a bit, and can occasionally cause the interpolation path to pass out of gamut if interpolating on an edge.

It can be used by specifying natural as the interpolation method.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='natural')\n<coloraide.interpolate.bspline_natural.InterpolatorNaturalBSpline object at 0x112895e50>\n
"},{"location":"interpolation/#monotone","title":"Monotone","text":"

Monotone interpolation is registered in Color by Default

The \"monotone\" spline is a piecewise interpolation spline that passes through all its data points and helps to preserve monotonicity. As far as we are concerned, the important thing to note is that it greatly reduces any overshoot or undershoot in the interpolation.

>>> Color.interpolate(['red', 'green', 'blue', 'orange'], method='monotone')\n<coloraide.interpolate.monotone.InterpolatorMonotone object at 0x1124df190>\n
"},{"location":"interpolation/#catmull-rom","title":"Catmull-Rom","text":"

Catmull-Rom interpolation is not registered in Color by Default

Lastly, the Catmull-Rom spline is another \"interpolating\" spline that passes through all of its data points, similar to the \"natural\" spline, but it but does not share the same continuity and properties of a \"natural\" spline.

Much like the \"natural\" spline, it can overshoot or undershoot.

Catmull-Rom is not registered by default, but can be registered as shown below and then used by specifying catrom as the interpolation method.

>>> from coloraide import Color\n>>> from coloraide.interpolate.catmull_rom import CatmullRom\n>>> class Custom(Color): ...\n... \n>>> Custom.register(CatmullRom())\n>>> Custom.interpolate(['red', 'green', 'blue', 'orange'], method='catrom')\n<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x112c39e50>\n
"},{"location":"interpolation/#discrete-interpolation","title":"Discrete Interpolation","text":"

New 2.5

So far, we've only shown examples of continuous interpolation methods. To clarify, we are using \"continuous\" in a slightly different way than we discussed earlier. When we say \"continuous\" here, we simply mean that the colors in the interpolation smoothly transition from one color to the other. But when creating charts or graphs, some times you'd like to categorize data such that a range of values correspond to a specific color. For this, we can use discrete, which like intrpolate, returns an interpolation object, but the the ranges will be discrete.

By default, ranges are calculated directly form the input colors. So if you had three colors, the interpolation would be broken up into 3 ranges. Compare this with the the \"continuous\" interpolation we methods we showed earlier.

>>> Color.discrete(['red', 'green', 'blue'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b8fbd0>\n>>> Color.interpolate(['red', 'green', 'blue'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bcff90>\n

If we specify step, we can create a larger or smaller color scale using the input colors to interpolate the new color scale. And we can use any of the aforementioned interpolation methods to help generate this new discrete scale.

>>> Color.discrete(['red', 'green', 'blue'], steps=5, method='catrom')\n<coloraide.interpolate.catmull_rom.InterpolatorCatmullRom object at 0x112ba3f10>\n

What makes this really useful is if you combine it with custom domains to process data. By default, the domain is [0, 1], but we can change this to directly correlate the data with our quantized color samples. For instance, let's use a series of discrete colors to represent temperature. Additionally, let's use domain to associate a temperature ranges with the given colors. Now when we input a temperature value, it will align with our discrete color scale.

>>> i = Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'], domain=[-32, 32, 60, 85, 95])\n>>> i(-32)\ncolor(--oklab 0.45201 -0.03246 -0.31153 / 1)\n>>> i(40)\ncolor(--oklab 0.51975 -0.1403 0.10768 / 1)\n>>> i(87)\ncolor(--oklab 0.79269 0.05661 0.16138 / 1)\n>>> i(100)\ncolor(--oklab 0.62796 0.22486 0.12585 / 1)\n>>> i\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1116b5350>\n

Additionally, color scales can be limited using the padding parameter.

>>> Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1124ece10>\n>>> Color.discrete(['blue', 'green', 'yellow', 'orange', 'red'], padding=[0.25, 0])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c489d0>\n

As discrete() is built on steps(), it can take all the same arguments. Check out steps() to learn more.

"},{"location":"interpolation/#hue-interpolation","title":"Hue Interpolation","text":"

In interpolation, hues are handled special allowing us to control the way in which hues are evaluated. By default, the shortest angle between two hues is targeted for interpolation, but the hue option allows us to redefine this behavior in a number of interesting ways: shorter, longer, increasing, decreasing, and specified. Below, we can see how the interpolation varies using shorter vs longer (interpolate between the longest angle).

>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask(\"hue\", invert=True)],\n...     space=\"lch\"\n... )\n>>> i(0.2477).to_string()\n'lch(52 58.1 351.59)'\n>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask(\"hue\", invert=True)],\n...     space=\"lch\",\n...     hue=\"longer\"\n... )\n>>> i(0.2477).to_string()\n'lch(52 58.1 80.761)'\n

To help visualize the different hue methods, consider the following evaluation between rebeccapurple and lch(85% 85 805). Below we will demonstrate each of the different hue evaluations. To learn more, check out the CSS level 4 specification which describes each one.

Hue Specified

The specified fix-up was at one time specified in the CSS Color Level 4 specification, but is no longer mentioned there. While CSS no longer supports this hue fix-up, we still do. specified simply does not apply any hue fix-up and will use hues as specified, hence the name.

Interpolating Multiple Colors

The algorithm has been tweaked in order to calculate fix-ups of multiple hues such that they are all relative to each other. This is a requirement for interpolation methods that use cubic splines that evaluate many hues at the same time as opposed to linear, piecewise interpolation that only evaluates two hues at any given time.

shorterlongerincreasingdecreasingspecified
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"shorter\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112be5b50>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"longer\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1117e75d0>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"increasing\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e7f90>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"decreasing\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c07f90>\n
>>> Color.interpolate(\n...     [\"rebeccapurple\", \"lch(85% 100 805)\"],\n...     space='lch',\n...     hue=\"specified\"\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112883910>\n
"},{"location":"interpolation/#interpolating-with-alpha","title":"Interpolating with Alpha","text":"

Interpolating color channels is pretty straight forward and uses traditional linear interpolation logic, but when introducing transparency to a color, interpolation uses a concept known as premultiplication which alters the normal interpolation process.

Premultiplication is a technique that tends to produce better results when two colors have differing transparency. It essentially accounts for the transparency and uses it to weight how may a given color channel will contribute to the interpolation. A more transparent color's channels will naturally contribute less.

Consider the following example. Normally, when transitioning to a \"transparent\" color, the colors will be more gray during the transition. This is because transparent is actually black. But when using premultiplication, the transition looks just as one would expect as the transparent color's channels are weighted less due to the high transparency.

>>> Color.interpolate(['white', 'transparent'], space='srgb', premultiplied=False)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112883910>\n>>> Color.interpolate(['white', 'transparent'], space='srgb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b97610>\n

As a final example, below we have an opaque orange and a blue that is quite transparent. Logically, the blue shouldn't have as big an affect on the overall color as it is so faint, and yet, in the un-premultiplied example, when mixing the colors equally, we see that the resultant color is also equally influenced by the hue of both colors. In the premultiplied example, we see that orange is still quite dominant at 50% as it is fully opaque.

>>> Color('orange').mix(Color('blue').set('alpha', 0.25), space='srgb', premultiplied=False)\ncolor(srgb 0.5 0.32353 0.5 / 0.625)\n>>> Color('orange').mix(Color('blue').set('alpha', 0.25), space='srgb')\ncolor(srgb 0.8 0.51765 0.2 / 0.625)\n

If we interpolate it, we can see the difference in transition.

>>> Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb', premultiplied=False)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1117e5510>\n>>> Color.interpolate(['orange', Color('blue').set('alpha', 0.25)], space='srgb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e4a90>\n

There may be some cases where it is desired to use no premultiplication in alpha blending. One could simply be that you need to mimic the same behavior of a system that does not use premultiplied interpolation. If so, simply set premultiplied to False as shown above.

"},{"location":"interpolation/#mixing","title":"Mixing","text":"

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 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.

>>> Color(\"red\").mix(Color(\"blue\"))\ncolor(--oklab 0.53998 0.0962 -0.09284 / 1)\n

By default, colors are mixed at 50%, but the percentage can be controlled. Here we mix the color blue into the color red at 20%. With blue at 20% and red at 80%, this gives us a more reddish color.

>>> Color(\"red\").mix(Color(\"blue\"), 0.2)\ncolor(--oklab 0.59277 0.1734 0.03837 / 1)\n

As with all interpolation based functions, if needed, a different color space can be specified with the space parameter or even a different interpolation method via method. mix accepts all the same parameters used in interpolate, though concepts like stops and hints are not allowed with mixing.

>>> Color(\"red\").mix(Color(\"blue\"), space=\"hsl\", method='bspline')\ncolor(--hsl -60 1 0.5 / 1)\n

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 color afterwards.

>>> Color(\"red\").mix(\"blue\", 0.2)\ncolor(--oklab 0.59277 0.1734 0.03837 / 1)\n

Mixing will always return a new color unless in_place is set True.

"},{"location":"interpolation/#steps","title":"Steps","text":"

Interpolation Options

Any options not consumed by mix will be passed to the underlying interpolation function. This includes options 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. Just provide two or more colors, and specify how many steps are wanted.

>>> Color.steps([\"red\", \"blue\"], steps=10)\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.60841 0.19627 0.07725 / 1), color(--oklab 0.58886 0.16768 0.02865 / 1), color(--oklab 0.56931 0.13909 -0.01995 / 1), color(--oklab 0.54976 0.1105 -0.06854 / 1), color(--oklab 0.53021 0.08191 -0.11714 / 1), color(--oklab 0.51066 0.05332 -0.16574 / 1), color(--oklab 0.49111 0.02473 -0.21433 / 1), color(--oklab 0.47156 -0.00387 -0.26293 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1)]\n

If desired, multiple colors can be provided, and steps will be returned for all the interpolated segments. When interpolating multiple colors, piecewise interpolation is used (which is covered in more detail later).

>>> Color.steps([\"red\", \"orange\", \"yellow\", \"green\"], steps=10)\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.68287 0.16878 0.13769 / 1), color(--oklab 0.73778 0.1127 0.14954 / 1), color(--oklab 0.79269 0.05661 0.16138 / 1), color(--oklab 0.85112 0.01395 0.17378 / 1), color(--oklab 0.90955 -0.02871 0.18617 / 1), color(--oklab 0.96798 -0.07137 0.19857 / 1), color(--oklab 0.81857 -0.09435 0.16827 / 1), color(--oklab 0.66916 -0.11732 0.13797 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1)]\n

Steps can also be configured to return colors based on a maximum Delta E distance. This means you can ensure the distance between all colors is no greater than a certain value.

In this example, we specify the color color(display-p3 0 1 0) and interpolate steps between red. The result gives us an array of colors, where the distance between any two colors should be no greater than the Delta E result of 10.

>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.4504 0.99903 -0.32673 / 1), color(srgb -0.37655 0.97943 -0.33694 / 1), color(srgb -0.27847 0.95946 -0.34286 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.23528 0.91833 -0.34574 / 1), color(srgb 0.34809 0.89715 -0.34401 / 1), color(srgb 0.42823 0.87552 -0.34098 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.54849 0.83088 -0.33349 / 1), color(srgb 0.59727 0.80784 -0.32909 / 1), color(srgb 0.64097 0.7843 -0.32423 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.71679 0.73568 -0.3131 / 1), color(srgb 0.74999 0.71057 -0.30681 / 1), color(srgb 0.78053 0.6849 -0.30002 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.83451 0.6318 -0.28486 / 1), color(srgb 0.85826 0.60433 -0.27644 / 1), color(srgb 0.87999 0.57619 -0.26744 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.9177 0.51777 -0.24752 / 1), color(srgb 0.9338 0.48735 -0.2365 / 1), color(srgb 0.9481 0.456 -0.22467 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.97145 0.38994 -0.19821 / 1), color(srgb 0.98055 0.35476 -0.18326 / 1), color(srgb 0.98796 0.31761 -0.16684 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 0.99777 0.23405 -0.12767 / 1), color(srgb 1.0002 0.18378 -0.10158 / 1), color(srgb 1.0009 0.11978 -0.06417 / 1), color(srgb 1 0 0 / 1)]\n

max_steps can be used to limit the results of max_delta_e in case result balloons to an unexpected size. Obviously, this affects the Delta E between the colors inversely. It should be noted that steps are injected equally between every color when satisfying a max Delta E limit in order to avoid shifting the midpoint. In some cases, in order to satisfy both the max_delta_e and the max_steps requirement, the number of steps may even be clipped such that they are less than the max_steps limit. max_steps is set to 1000 by default, but can be set to None if no limit is desired.

>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     max_steps=10\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 1 0 0 / 1)]\n

When specifying a max_delta_e, steps will function as a minimum required steps and will push the delta even smaller if the required steps is greater than the calculated steps via the maximum Delta E limit.

>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     steps=50\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.47276 1.0057 -0.32192 / 1), color(srgb -0.42946 0.99307 -0.33039 / 1), color(srgb -0.37992 0.98024 -0.33661 / 1), color(srgb -0.32082 0.96725 -0.341 / 1), color(srgb -0.24447 0.9541 -0.34385 / 1), color(srgb -0.1198 0.94078 -0.34542 / 1), color(srgb 0.15996 0.92729 -0.34592 / 1), color(srgb 0.26558 0.91362 -0.3455 / 1), color(srgb 0.33665 0.89976 -0.34431 / 1), color(srgb 0.3931 0.88572 -0.34248 / 1), color(srgb 0.44104 0.8715 -0.34036 / 1), color(srgb 0.48324 0.85707 -0.33806 / 1), color(srgb 0.52118 0.84244 -0.33557 / 1), color(srgb 0.55582 0.82761 -0.33289 / 1), color(srgb 0.58776 0.81258 -0.33002 / 1), color(srgb 0.61745 0.79733 -0.32696 / 1), color(srgb 0.64519 0.78187 -0.32371 / 1), color(srgb 0.67123 0.76619 -0.32025 / 1), color(srgb 0.69575 0.75029 -0.31659 / 1), color(srgb 0.7189 0.73416 -0.31273 / 1), color(srgb 0.74079 0.7178 -0.30866 / 1), color(srgb 0.76151 0.7012 -0.30437 / 1), color(srgb 0.78113 0.68437 -0.29987 / 1), color(srgb 0.79972 0.66729 -0.29515 / 1), color(srgb 0.81733 0.64995 -0.2902 / 1), color(srgb 0.834 0.63236 -0.28502 / 1), color(srgb 0.84977 0.6145 -0.2796 / 1), color(srgb 0.86467 0.59636 -0.27393 / 1), color(srgb 0.87872 0.57794 -0.26801 / 1), color(srgb 0.89194 0.55922 -0.26182 / 1), color(srgb 0.90434 0.54018 -0.25536 / 1), color(srgb 0.91596 0.52082 -0.2486 / 1), color(srgb 0.92679 0.50111 -0.24154 / 1), color(srgb 0.93686 0.48103 -0.23415 / 1), color(srgb 0.94616 0.46054 -0.22641 / 1), color(srgb 0.95471 0.43961 -0.2183 / 1), color(srgb 0.96252 0.41819 -0.20979 / 1), color(srgb 0.96959 0.39623 -0.20082 / 1), color(srgb 0.97593 0.37364 -0.19136 / 1), color(srgb 0.98155 0.35032 -0.18134 / 1), color(srgb 0.98644 0.32615 -0.17067 / 1), color(srgb 0.99062 0.30093 -0.15926 / 1), color(srgb 0.99409 0.27439 -0.14694 / 1), color(srgb 0.99685 0.24615 -0.13353 / 1), color(srgb 0.99891 0.21556 -0.11841 / 1), color(srgb 1.0002 0.18152 -0.10034 / 1), color(srgb 1.0009 0.14177 -0.07755 / 1), color(srgb 1.0008 0.09013 -0.04535 / 1), color(srgb 1 0 0 / 1)]\n

steps uses the color class's default \u2206E method to calculate max \u2206E, the current default \u2206E being \u2206E*ab. While using something like \u2206E*00 is far more accurate, it is a much more expensive operation. If desired, the class's default \u2206E can be changed via subclassing the color object and and changing DELTA_E class variable or by manually specifying the method via the delta_e parameter.

\u2206E*ab.\u2206E*00
>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     delta_e=\"76\"\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.4504 0.99903 -0.32673 / 1), color(srgb -0.37655 0.97943 -0.33694 / 1), color(srgb -0.27847 0.95946 -0.34286 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.23528 0.91833 -0.34574 / 1), color(srgb 0.34809 0.89715 -0.34401 / 1), color(srgb 0.42823 0.87552 -0.34098 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.54849 0.83088 -0.33349 / 1), color(srgb 0.59727 0.80784 -0.32909 / 1), color(srgb 0.64097 0.7843 -0.32423 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.71679 0.73568 -0.3131 / 1), color(srgb 0.74999 0.71057 -0.30681 / 1), color(srgb 0.78053 0.6849 -0.30002 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.83451 0.6318 -0.28486 / 1), color(srgb 0.85826 0.60433 -0.27644 / 1), color(srgb 0.87999 0.57619 -0.26744 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.9177 0.51777 -0.24752 / 1), color(srgb 0.9338 0.48735 -0.2365 / 1), color(srgb 0.9481 0.456 -0.22467 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.97145 0.38994 -0.19821 / 1), color(srgb 0.98055 0.35476 -0.18326 / 1), color(srgb 0.98796 0.31761 -0.16684 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 0.99777 0.23405 -0.12767 / 1), color(srgb 1.0002 0.18378 -0.10158 / 1), color(srgb 1.0009 0.11978 -0.06417 / 1), color(srgb 1 0 0 / 1)]\n
>>> Color.steps(\n...     [Color(\"display-p3\", [0, 1, 0]), \"red\"],\n...     space=\"lch\",\n...     out_space=\"srgb\",\n...     max_delta_e=10,\n...     delta_e=\"2000\"\n... )\n[color(srgb -0.5116 1.0183 -0.31067 / 1), color(srgb -0.37655 0.97943 -0.33694 / 1), color(srgb -0.09291 0.9391 -0.34554 / 1), color(srgb 0.34809 0.89715 -0.34401 / 1), color(srgb 0.49308 0.85343 -0.33745 / 1), color(srgb 0.59727 0.80784 -0.32909 / 1), color(srgb 0.6806 0.76025 -0.3189 / 1), color(srgb 0.74999 0.71057 -0.30681 / 1), color(srgb 0.80865 0.65865 -0.29271 / 1), color(srgb 0.85826 0.60433 -0.27644 / 1), color(srgb 0.89978 0.54736 -0.25782 / 1), color(srgb 0.9338 0.48735 -0.2365 / 1), color(srgb 0.96064 0.4236 -0.21195 / 1), color(srgb 0.98055 0.35476 -0.18326 / 1), color(srgb 0.99369 0.27779 -0.14854 / 1), color(srgb 1.0002 0.18378 -0.10158 / 1), color(srgb 1 0 0 / 1)]\n

And much like interpolate, we can use stops and hints and any of the other supported interpolate features as well.

>>> Color.steps(['orange', stop('purple', 0.25), 'green'], method='bspline', steps=10)\n[color(--oklab 0.79269 0.05661 0.16138 / 1), color(--oklab 0.63434 0.09861 0.05147 / 1), color(--oklab 0.51731 0.10434 -0.01701 / 1), color(--oklab 0.48698 0.08246 -0.02298 / 1), color(--oklab 0.47842 0.05764 -0.01527 / 1), color(--oklab 0.4775 0.02611 0.00011 / 1), color(--oklab 0.48271 -0.01079 0.02163 / 1), color(--oklab 0.49251 -0.05172 0.04775 / 1), color(--oklab 0.50536 -0.09534 0.07695 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1)]\n
"},{"location":"interpolation/#masking","title":"Masking","text":"

If desired, we can mask off specific channels that we do not wish to interpolate. Masking works by cloning the color and setting the specified channels as undefined (internally set to NaN). When interpolating, if one color's channel has a NaN, the other color's channel will be used as the result, keeping that channel at a constant value. If both colors have a NaN for the same channel, then NaN will be returned.

Magic Behind NaN

There are times when NaN values can happen naturally, such as with achromatic colors with hues. To learn more, check out Undefined Handling/NaN Handling.

In the following example, we have a base color of lch(52% 58.1 22.7) which we then interpolate with lch(56% 49.1 257.1). We then mask off the second color's channels except for hue. Applying this logic, we will end up with a range of colors that maintains the same lightness and chroma as the first color, but with different hues. We can see as we step through the colors that only the hue is interpolated.

>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask(['lightness', 'chroma', 'alpha'])],\n...     space=\"lch\"\n... )\n>>> [i(x/10).to_string() for x in range(10)]\n['lch(52 58.1 22.7)', 'lch(52 58.1 10.14)', 'lch(52 58.1 357.58)', 'lch(52 58.1 345.02)', 'lch(52 58.1 332.46)', 'lch(52 58.1 319.9)', 'lch(52 58.1 307.34)', 'lch(52 58.1 294.78)', 'lch(52 58.1 282.22)', 'lch(52 58.1 269.66)']\n

You can also create inverted masks. An inverted mask will mask all except the specified channel.

>>> i = Color.interpolate(\n...     [\"lch(52% 58.1 22.7)\", Color(\"lch(56% 49.1 257.1)\").mask('hue', invert=True)],\n...     space=\"lch\"\n... )\n>>> [i(x/10).to_string() for x in range(10)]\n['lch(52 58.1 22.7)', 'lch(52 58.1 10.14)', 'lch(52 58.1 357.58)', 'lch(52 58.1 345.02)', 'lch(52 58.1 332.46)', 'lch(52 58.1 319.9)', 'lch(52 58.1 307.34)', 'lch(52 58.1 294.78)', 'lch(52 58.1 282.22)', 'lch(52 58.1 269.66)']\n
"},{"location":"interpolation/#easing-functions","title":"Easing Functions","text":"

When interpolating, whether using linear interpolation or something like B-Spline interpolation, the transitioning between colors is always linear in time, even if the path to those colors is not. For example, if you are interpolating between 2 colors and you request a 0.5 point on that line, it will always be in the middle. This is because, no matter how crooked the path, the rate of change on that path is always linear.

By default, ColorAide uses linear transitions when interpolating, but there are times that a different, more dynamic transition may be desired. This can be achieved by using the progress parameter on any of the interpolation related functions provided by ColorAide.

progress accepts an easing function that takes a single time input and returns a new time input. This allows for a user to augment the rate of change when transitioning from one color to another. Inputs are almost always between 0 - 1 unless extrapolate is enabled and the user has manually input a range beyond 0 - 1. Even a change in domain will not affect the range as once the domain is accounted for, internally the domain [0, 1] is used.

ColorAide provides 5 basic easing functions out of the box along with cubic_bezier which is used to create all of the aforementioned easing function except linear, which simply returns what is given as an input.

Create Your Own Cubic Bezier Easings Online: https://cubic-bezier.com

More Common Cubic Bezier Easings

The following were all acquired from from https://matthewlein.com/tools/ceaser.js.

ease_in_quad = cubic_bezier(0.550, 0.085, 0.680, 0.530)\nease_in_cubic = cubic_bezier(0.550, 0.055, 0.675, 0.190)\nease_in_quart = cubic_bezier(0.895, 0.030, 0.685, 0.220)\nease_in_quint = cubic_bezier(0.755, 0.050, 0.855, 0.060)\nease_in_sine = cubic_bezier(0.470, 0.000, 0.745, 0.715)\nease_in_expo = cubic_bezier(0.950, 0.050, 0.795, 0.035)\nease_in_circ = cubic_bezier(0.600, 0.040, 0.980, 0.335)\nease_in_back = cubic_bezier(0.600, -0.280, 0.735, 0.045)\n\nease_out_quad = cubic_bezier(0.250, 0.460, 0.450, 0.940)\nease_out_cubic = cubic_bezier(0.215, 0.610, 0.355, 1.000)\nease_out_quart = cubic_bezier(0.165, 0.840, 0.440, 1.000)\nease_out_quint = cubic_bezier(0.230, 1.000, 0.320, 1.000)\nease_out_sine = cubic_bezier(0.390, 0.575, 0.565, 1.000)\nease_out_expo = cubic_bezier(0.190, 1.000, 0.220, 1.000)\nease_out_circ = cubic_bezier(0.075, 0.820, 0.165, 1.000)\nease_out_back = cubic_bezier(0.175, 0.885, 0.320, 1.275)\n\nease_in_out_quad = cubic_bezier(0.455, 0.030, 0.515, 0.955)\nease_in_out_cubic = cubic_bezier(0.645, 0.045, 0.355, 1.000)\nease_in_out_quart = cubic_bezier(0.770, 0.000, 0.175, 1.000)\nease_in_out_quint = cubic_bezier(0.860, 0.000, 0.070, 1.000)\nease_in_out_sine = cubic_bezier(0.445, 0.050, 0.550, 0.950)\nease_in_out_expo = cubic_bezier(1.000, 0.000, 0.000, 1.000)\nease_in_out_circ = cubic_bezier(0.785, 0.135, 0.150, 0.860)\nease_in_out_back = cubic_bezier(0.680, -0.550, 0.265, 1.550)\n
LinearEaseEase InEase OutEase In/OutCubic Bezier

Here, we are using the default \"ease in\" and \"ease out\" easing functions provided by ColorAide.

>>> from coloraide import ease_in, ease_out\n>>> Color.interpolate(\n...     [\"green\", \"blue\"],\n...     progress=ease_in\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c66510>\n>>> Color.interpolate(\n...     [\"green\", \"blue\"]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c77bd0>\n>>> Color.interpolate(\n...     [\"green\", \"blue\"],\n...     progress=ease_out\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c835d0>\n

Additionally, easing functions can be injected inline which allows a user to control how easing is performed between specific sub-interpolations within piecewise interpolation.

>>> Color.interpolate([\"red\", \"green\", ease_out, \"blue\"])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128f7e50>\n

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 an exponential \"ease in\" to alpha while allowing all other channels to interpolate normally.

>>> ease_in_expo = cubic_bezier(0.950, 0.050, 0.795, 0.035)\n>>> Color.interpolate(\n...     [\"lch(50% 50 0)\", \"lch(90% 50 260 / 0.5)\"],\n...     progress={\n...         'alpha': ease_in_expo\n...     }\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c3a150>\n

We can also set all the channels to an easing function via all and then override specific channels. In this case, we exponentially \"ease out\" on all channels except the red channel, which we then force to be linear.

>>> ease_out_expo = cubic_bezier(0.190, 1.000, 0.220, 1.000)\n>>> Color.interpolate(\n...     [\"color(srgb 0 1 1)\", \"color(srgb 1 0 0)\"],\n...     progress={\n...         'all': ease_out_expo,\n...         'r': linear\n...     },\n...     space='srgb'\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b96410>\n
"},{"location":"interpolation/#color-stops-and-hints","title":"Color Stops and Hints","text":"

Color stops are the position where the transition to and from a color starts and ends. By default, color stops are evenly distributed within the domain of [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.

>>> from coloraide import stop\n>>> Color.interpolate(['orange', 'purple', 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e60d0>\n>>> Color.interpolate(['orange', stop('purple', 0.25), 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128ef010>\n

Color stops follow the rules as laid out in the CSS spec.

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.

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.

>>> from coloraide import hint\n>>> Color.interpolate(['orange', 'purple', 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1124e37d0>\n>>> Color.interpolate(['orange', hint(0.75), 'purple', 'green'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128fe810>\n
"},{"location":"interpolation/#padding","title":"Padding","text":"

New 2.6

Particularly when interpolating a color scale, it can be useful to \"resize\" the area of the color scale being evaluated. This can generally be done using the padding parameter. Consider the following example using the ColorBrewer scale OrRd.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.interpolate(scale, space='srgb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c062d0>\n>>> Color.discrete(scale, space='srgb', steps=5)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c74390>\n>>> Color.interpolate(scale, space='srgb', padding=0.25)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112cb5ad0>\n>>> Color.discrete(scale, space='srgb', steps=5, padding=0.25)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112cb56d0>\n

Padding can be applied to both sides by specifying a single number, or it can be controlled per side by sending in a sequence of two values.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[0.25, 0])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128c3610>\n

Negative padding is allowed as well.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[-0.25, 0])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128e5a10>\n

If the result extends past the limits, extrapolate needs to be enabled or the values will be clamped to the ends.

>>> scale = ['#fff7ec', '#fee8c8', '#fdd49e', '#fdbb84', '#fc8d59', '#ef6548', '#d7301f', '#b30000', '#7f0000']\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[1, 1])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c5fb90>\n>>> Color.discrete(scale, space='srgb', steps=5, padding=[1, 1], extrapolate=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c5ea90>\n
"},{"location":"interpolation/#domains","title":"Domains","text":"

By default, interpolation has an input domain of [0, 1]. This domain applies to an entire interpolation, even ones that span multiple colors. Generally, this is sufficient and can be used to generate color scales, mixes, and steps in any way that a user needs. When generating colors that should align with data, custom domains can be quite helpful.

For instance, associating colors with temperature.

>>> i = Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n>>> i(-32)\ncolor(--oklab 0.45201 -0.03246 -0.31153 / 1)\n>>> i(47)\ncolor(--oklab 0.75988 -0.10337 0.15637 / 1)\n>>> i(89)\ncolor(--oklab 0.7268 0.12391 0.14717 / 1)\n>>> i\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112894910>\n

It should be noted that you are not constrained to provide the exact same amount of domain values as you have colors and can have differing amounts, but if you want to align specific colors to certain data points, then it helps.

>>> Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bfc710>\n>>> Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bb0210>\n

Lastly, domains must be specified in ascending order of values. If a value decreases in magnitude, it will assume the value that comes right before it. This means you cannot put a domain in reverse. If you need to reverse the order, just flip the color order and setup the domain accordingly.

>>> i = Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 20, 95]\n... )\n>>> i.domain\n<bound method Interpolator.domain of <coloraide.interpolate.linear.InterpolatorLinear object at 0x112c32d90>>\n>>> i\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c32d90>\n

Custom domains are most useful when working with discrete or interpolate directly, but you can use it in other methods like steps as well. As steps does not take data point inputs like interpolate, we do not need to use the temperature data as an input except to set the domain, but the steps will be generated with the same alignment relative to the domain range.

>>> Color.interpolate(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bc5710>\n>>> Color.discrete(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     domain=[-32, 32, 60, 85, 95]\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d1f490>\n>>> Color.steps(\n...     ['blue', 'green', 'yellow', 'orange', 'red'],\n...     steps=11,\n...     domain=[-32, 32, 60, 85, 95]\n... )\n[color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.46546 -0.05386 -0.22834 / 1), color(--oklab 0.4789 -0.07526 -0.14516 / 1), color(--oklab 0.49234 -0.09666 -0.06197 / 1), color(--oklab 0.50578 -0.11806 0.02122 / 1), color(--oklab 0.51922 -0.13946 0.1044 / 1), color(--oklab 0.71505 -0.11027 0.14728 / 1), color(--oklab 0.91836 -0.079 0.18851 / 1), color(--oklab 0.90067 -0.02222 0.18429 / 1), color(--oklab 0.81162 0.04279 0.1654 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1)]\n

Wile you can technically feed domain into mix, it is probably not as useful. It will respect the domain alignment, but mix always accepts a percentage of [0, 1], regardless of the underlying domain.

"},{"location":"interpolation/#extrapolation","title":"Extrapolation","text":"

By default, ColorAide clamps the entire progress of an interpolation to always be within the domain ([0, 1] by default). In most cases, this is more what most user expects and why this is the default. It should be noted that this does not affect easing functions, as the clamping is done prior to any easing function calls.

If it is desired to extrapolate past 0 and 1, extrapolate can set to True on all interpolation methods.

>>> Color('red').mix('blue', 0.5)\ncolor(--oklab 0.53998 0.0962 -0.09284 / 1)\n>>> Color('red').mix('blue', -0.5, extrapolate=True)\ncolor(--oklab 0.71593 0.35352 0.34453 / 1)\n

As a larger example, we can purposely interpolate over a range with values beyond 0 and 1. Here we extended the range to -0.5 and 1.5.

>>> offset, factor = 0.25, 1.5\n>>> i = Color.interpolate(['red', 'blue'])\n>>> Ramp([i((r * factor / 100) - offset) for r in range(101)])\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.62708 0.22358 0.12366 / 1), color(--oklab 0.62444 0.21972 0.1171 / 1), color(--oklab 0.6218 0.21586 0.11054 / 1), color(--oklab 0.61916 0.212 0.10398 / 1), color(--oklab 0.61652 0.20814 0.09742 / 1), color(--oklab 0.61388 0.20428 0.09086 / 1), color(--oklab 0.61124 0.20042 0.0843 / 1), color(--oklab 0.6086 0.19656 0.07774 / 1), color(--oklab 0.60596 0.1927 0.07117 / 1), color(--oklab 0.60332 0.18884 0.06461 / 1), color(--oklab 0.60068 0.18498 0.05805 / 1), color(--oklab 0.59805 0.18112 0.05149 / 1), color(--oklab 0.59541 0.17726 0.04493 / 1), color(--oklab 0.59277 0.1734 0.03837 / 1), color(--oklab 0.59013 0.16954 0.03181 / 1), color(--oklab 0.58749 0.16568 0.02525 / 1), color(--oklab 0.58485 0.16182 0.01869 / 1), color(--oklab 0.58221 0.15796 0.01213 / 1), color(--oklab 0.57957 0.1541 0.00557 / 1), color(--oklab 0.57693 0.15024 -0.00099 / 1), color(--oklab 0.57429 0.14638 -0.00755 / 1), color(--oklab 0.57165 0.14252 -0.01411 / 1), color(--oklab 0.56901 0.13866 -0.02067 / 1), color(--oklab 0.56638 0.1348 -0.02723 / 1), color(--oklab 0.56374 0.13094 -0.0338 / 1), color(--oklab 0.5611 0.12708 -0.04036 / 1), color(--oklab 0.55846 0.12322 -0.04692 / 1), color(--oklab 0.55582 0.11936 -0.05348 / 1), color(--oklab 0.55318 0.1155 -0.06004 / 1), color(--oklab 0.55054 0.11164 -0.0666 / 1), color(--oklab 0.5479 0.10778 -0.07316 / 1), color(--oklab 0.54526 0.10392 -0.07972 / 1), color(--oklab 0.54262 0.10006 -0.08628 / 1), color(--oklab 0.53998 0.0962 -0.09284 / 1), color(--oklab 0.53735 0.09234 -0.0994 / 1), color(--oklab 0.53471 0.08848 -0.10596 / 1), color(--oklab 0.53207 0.08462 -0.11252 / 1), color(--oklab 0.52943 0.08076 -0.11908 / 1), color(--oklab 0.52679 0.0769 -0.12564 / 1), color(--oklab 0.52415 0.07304 -0.1322 / 1), color(--oklab 0.52151 0.06918 -0.13877 / 1), color(--oklab 0.51887 0.06532 -0.14533 / 1), color(--oklab 0.51623 0.06146 -0.15189 / 1), color(--oklab 0.51359 0.05761 -0.15845 / 1), color(--oklab 0.51095 0.05375 -0.16501 / 1), color(--oklab 0.50832 0.04989 -0.17157 / 1), color(--oklab 0.50568 0.04603 -0.17813 / 1), color(--oklab 0.50304 0.04217 -0.18469 / 1), color(--oklab 0.5004 0.03831 -0.19125 / 1), color(--oklab 0.49776 0.03445 -0.19781 / 1), color(--oklab 0.49512 0.03059 -0.20437 / 1), color(--oklab 0.49248 0.02673 -0.21093 / 1), color(--oklab 0.48984 0.02287 -0.21749 / 1), color(--oklab 0.4872 0.01901 -0.22405 / 1), color(--oklab 0.48456 0.01515 -0.23061 / 1), color(--oklab 0.48192 0.01129 -0.23717 / 1), color(--oklab 0.47928 0.00743 -0.24374 / 1), color(--oklab 0.47665 0.00357 -0.2503 / 1), color(--oklab 0.47401 -0.00029 -0.25686 / 1), color(--oklab 0.47137 -0.00415 -0.26342 / 1), color(--oklab 0.46873 -0.00801 -0.26998 / 1), color(--oklab 0.46609 -0.01187 -0.27654 / 1), color(--oklab 0.46345 -0.01573 -0.2831 / 1), color(--oklab 0.46081 -0.01959 -0.28966 / 1), color(--oklab 0.45817 -0.02345 -0.29622 / 1), color(--oklab 0.45553 -0.02731 -0.30278 / 1), color(--oklab 0.45289 -0.03117 -0.30934 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1)]\n>>> i = Color.interpolate(['red', 'blue'], extrapolate=True)\n>>> Ramp([i((r * factor / 100) - offset) for r in range(101)])\n[color(--oklab 0.67194 0.28919 0.23519 / 1), color(--oklab 0.6693 0.28533 0.22863 / 1), color(--oklab 0.66666 0.28147 0.22207 / 1), color(--oklab 0.66402 0.27761 0.21551 / 1), color(--oklab 0.66138 0.27375 0.20895 / 1), color(--oklab 0.65875 0.26989 0.20239 / 1), color(--oklab 0.65611 0.26603 0.19583 / 1), color(--oklab 0.65347 0.26217 0.18927 / 1), color(--oklab 0.65083 0.25831 0.1827 / 1), color(--oklab 0.64819 0.25445 0.17614 / 1), color(--oklab 0.64555 0.2506 0.16958 / 1), color(--oklab 0.64291 0.24674 0.16302 / 1), color(--oklab 0.64027 0.24288 0.15646 / 1), color(--oklab 0.63763 0.23902 0.1499 / 1), color(--oklab 0.63499 0.23516 0.14334 / 1), color(--oklab 0.63235 0.2313 0.13678 / 1), color(--oklab 0.62971 0.22744 0.13022 / 1), color(--oklab 0.62708 0.22358 0.12366 / 1), color(--oklab 0.62444 0.21972 0.1171 / 1), color(--oklab 0.6218 0.21586 0.11054 / 1), color(--oklab 0.61916 0.212 0.10398 / 1), color(--oklab 0.61652 0.20814 0.09742 / 1), color(--oklab 0.61388 0.20428 0.09086 / 1), color(--oklab 0.61124 0.20042 0.0843 / 1), color(--oklab 0.6086 0.19656 0.07774 / 1), color(--oklab 0.60596 0.1927 0.07117 / 1), color(--oklab 0.60332 0.18884 0.06461 / 1), color(--oklab 0.60068 0.18498 0.05805 / 1), color(--oklab 0.59805 0.18112 0.05149 / 1), color(--oklab 0.59541 0.17726 0.04493 / 1), color(--oklab 0.59277 0.1734 0.03837 / 1), color(--oklab 0.59013 0.16954 0.03181 / 1), color(--oklab 0.58749 0.16568 0.02525 / 1), color(--oklab 0.58485 0.16182 0.01869 / 1), color(--oklab 0.58221 0.15796 0.01213 / 1), color(--oklab 0.57957 0.1541 0.00557 / 1), color(--oklab 0.57693 0.15024 -0.00099 / 1), color(--oklab 0.57429 0.14638 -0.00755 / 1), color(--oklab 0.57165 0.14252 -0.01411 / 1), color(--oklab 0.56901 0.13866 -0.02067 / 1), color(--oklab 0.56638 0.1348 -0.02723 / 1), color(--oklab 0.56374 0.13094 -0.0338 / 1), color(--oklab 0.5611 0.12708 -0.04036 / 1), color(--oklab 0.55846 0.12322 -0.04692 / 1), color(--oklab 0.55582 0.11936 -0.05348 / 1), color(--oklab 0.55318 0.1155 -0.06004 / 1), color(--oklab 0.55054 0.11164 -0.0666 / 1), color(--oklab 0.5479 0.10778 -0.07316 / 1), color(--oklab 0.54526 0.10392 -0.07972 / 1), color(--oklab 0.54262 0.10006 -0.08628 / 1), color(--oklab 0.53998 0.0962 -0.09284 / 1), color(--oklab 0.53735 0.09234 -0.0994 / 1), color(--oklab 0.53471 0.08848 -0.10596 / 1), color(--oklab 0.53207 0.08462 -0.11252 / 1), color(--oklab 0.52943 0.08076 -0.11908 / 1), color(--oklab 0.52679 0.0769 -0.12564 / 1), color(--oklab 0.52415 0.07304 -0.1322 / 1), color(--oklab 0.52151 0.06918 -0.13877 / 1), color(--oklab 0.51887 0.06532 -0.14533 / 1), color(--oklab 0.51623 0.06146 -0.15189 / 1), color(--oklab 0.51359 0.05761 -0.15845 / 1), color(--oklab 0.51095 0.05375 -0.16501 / 1), color(--oklab 0.50832 0.04989 -0.17157 / 1), color(--oklab 0.50568 0.04603 -0.17813 / 1), color(--oklab 0.50304 0.04217 -0.18469 / 1), color(--oklab 0.5004 0.03831 -0.19125 / 1), color(--oklab 0.49776 0.03445 -0.19781 / 1), color(--oklab 0.49512 0.03059 -0.20437 / 1), color(--oklab 0.49248 0.02673 -0.21093 / 1), color(--oklab 0.48984 0.02287 -0.21749 / 1), color(--oklab 0.4872 0.01901 -0.22405 / 1), color(--oklab 0.48456 0.01515 -0.23061 / 1), color(--oklab 0.48192 0.01129 -0.23717 / 1), color(--oklab 0.47928 0.00743 -0.24374 / 1), color(--oklab 0.47665 0.00357 -0.2503 / 1), color(--oklab 0.47401 -0.00029 -0.25686 / 1), color(--oklab 0.47137 -0.00415 -0.26342 / 1), color(--oklab 0.46873 -0.00801 -0.26998 / 1), color(--oklab 0.46609 -0.01187 -0.27654 / 1), color(--oklab 0.46345 -0.01573 -0.2831 / 1), color(--oklab 0.46081 -0.01959 -0.28966 / 1), color(--oklab 0.45817 -0.02345 -0.29622 / 1), color(--oklab 0.45553 -0.02731 -0.30278 / 1), color(--oklab 0.45289 -0.03117 -0.30934 / 1), color(--oklab 0.45025 -0.03503 -0.3159 / 1), color(--oklab 0.44762 -0.03889 -0.32246 / 1), color(--oklab 0.44498 -0.04275 -0.32902 / 1), color(--oklab 0.44234 -0.04661 -0.33558 / 1), color(--oklab 0.4397 -0.05047 -0.34214 / 1), color(--oklab 0.43706 -0.05433 -0.3487 / 1), color(--oklab 0.43442 -0.05819 -0.35527 / 1), color(--oklab 0.43178 -0.06205 -0.36183 / 1), color(--oklab 0.42914 -0.06591 -0.36839 / 1), color(--oklab 0.4265 -0.06977 -0.37495 / 1), color(--oklab 0.42386 -0.07363 -0.38151 / 1), color(--oklab 0.42122 -0.07749 -0.38807 / 1), color(--oklab 0.41858 -0.08135 -0.39463 / 1), color(--oklab 0.41595 -0.08521 -0.40119 / 1), color(--oklab 0.41331 -0.08907 -0.40775 / 1), color(--oklab 0.41067 -0.09293 -0.41431 / 1), color(--oklab 0.40803 -0.09679 -0.42087 / 1)]\n

Lastly, it is important to note that this affects stops as well, mainly stops applied to interpolation endpoints. When an endpoint is moved inwards via a color stop, the end range of the interpolation is clamped, extending the star and end color. But when extrapolation is enabled, a color stop on an endpoint essentially moves the start and end interpolation. And since there are no other colors on either end to interpolate with, extrapolation occurs.

>>> Color.interpolate([stop('red', 0.25), stop('blue', 0.75)])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128784d0>\n>>> Color.interpolate([stop('red', 0.25), stop('blue', 0.75)], extrapolate=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b8d9d0>\n
"},{"location":"interpolation/#null-handling","title":"Undefined/NaN Handling","text":"

Color spaces that have hue coordinates often have rules about when the hue is considered relevant. For instance, in the HSL color space, if saturation is zero, the hue is essentially powerless. This is because the color is \"without color\" or achromatic; therefore, the hue can have no affect on the actual color.

ColorAide will generally respect the values a user provides, so if an achromatic HSL color is given a hue of 270 degrees, ColorAide will accept it, but the hue will not affect the color in any meaningful way.

During conversions, such context is lost, and if an achromatic color is converted to the color space like HSL, the resultant color will have a hue that is noted as undefined. This is simply because there is no good hue for achromatic colors as they play no part in the color. Any hue is actually incorrect as achromatic colors have no real hue. Instead, colors will be returned with a value that represents that the hue is missing or undefined, or maybe better worded, could not be defined.

Many libraries, like d3-color, chroma.js, and color.js, represent null hues with NaN (not a number). This is usually done to make color interpolation easier. Some, like d3-color, are a bit more liberal with NaN and will target special cases that are above and beyond the normal rules to help ensure good interpolation. For instance, they not only mark hue undefined on HSL colors when saturation is zero, but they'll mark saturation as NaN when lightness indicates \"black\" or \"white\".

ColorAide also uses NaN, or in Python float('nan'), to represent undefined channels. In certain situations, when a hue is deemed undefined, the hue value will be set to coloraide.NaN, which is just a constant containing float('nan').

When performing linear interpolation, where only two color's channels are ever being evaluated together at a given time, if one color's channel has a NaN, the other color's channel will be used as the result. If both colors have a NaN for the same channel, then NaN will be returned.

Continuous NaN Handling

NaN handling is a bit different for the Continuous and Cubic Spline interpolation approaches. Linear only evaluates colors at a given time, while the others will take into consideration more than two colors. Because the context is much wider and more complicated, NaN values will often get context from both sides.

Notice that in this example, because white's saturation is zero, the hue is undefined. Because the hue is undefined, when the color is mixed with a second color (green), the hue of the second color is used.

>>> color = Color('white').convert('hsl')\n>>> color[:-1]\n[nan, 0.0, 1.0]\n>>> color2 = Color('green').convert('hsl')\n>>> color2[:-1]\n[120.0, 1.0, 0.25098039215686274]\n>>> color.mix(color2, space=\"hsl\")\ncolor(--hsl 120 0.5 0.62549 / 1)\n

But if we manually set the hue to 0 instead of NaN, we can see that the mixing goes quite differently.

>>> color = Color('white').convert('hsl').set('hue', 0)\n>>> color[:-1]\n[0.0, 0.0, 1.0]\n>>> color2 = Color('green').convert('hsl')\n>>> color2[:-1]\n[120.0, 1.0, 0.25098039215686274]\n>>> color.mix(color2, space=\"hsl\")\ncolor(--hsl 60 0.5 0.62549 / 1)\n

Technically, any channel can be set to NaN. And there are various ways to do this. The Color Manipulation documentation goes into the details of how these Nan values naturally occur and the various ways a user and manipulate them.

"},{"location":"interpolation/#carrying-forward","title":"Carrying-Forward","text":"

Experimental

This feature is provided to give parity with CSS behavior. As the spec is still in flux, behavior is subject to change or feature could be removed entirely. Use at your own risk.

CSS introduces the concept of carrying-forward undefined channels of like color spaces during conversion to the interpolating color space. The idea is to provide a sane handling to users who specified undefined channels for interpolation, but did not account for the conversion to the interpolating color space.

If a color has undefined channels, and is converting to a like color space, after conversion the new color will have the same undefined channels, assuming the channels support carrying-forward. The example below demonstrates the concept.

>>> rgb = Color('srgb', [0.5, NaN, 0.8])\n>>> p3 = rgb.convert('display-p3').set('green', NaN)\n>>> rgb, p3\n(color(srgb 0.5 none 0.8 / 1), color(display-p3 0.45659 none 0.76952 / 1))\n

ColorAide, by default, expects the user to be aware that undefined values are lost if conversion is required for interpolation. This is mainly because the intent of the color can be changed during this process, but some users may find the automatic carrying-forward more convenient. For this reason, ColorAide has implemented carrying-forward as an optional feature via the carryforward option.

In this example, interpolating without carrying-forward results in an interpolation between a purplish color and white. Using carrying-forward, we get a purplish color with an undefined green channel. The green channel takes on the white's green channel giving us an interpolation between a more greenish color and white.

>>> Color.interpolate(['color(srgb 0.5 none 0.8)', 'white'], space='display-p3')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bfd750>\n>>> Color.interpolate(['color(srgb 0.5 none 0.8)', 'white'], space='display-p3', carryforward=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bcd5d0>\n

Depending on the color space, carrying-forward may have better or worse results.

The following table shows channel components supported for carryforward. Spaces may use different names for their channels, but if they are derived from the related space classes, their channels are supported. For instance, xyz is derived from RGBish, so x, y, and z is treated like super saturated r, g, and b.

Space\u00a0Type Channel\u00a0Equivalents RGBish r, g, b LABish l LCHish l, c, h HSLish h, s, l HSVish h, s, v Cylindrical h

Carrying-forward is applied within categories.

Category Components Reds r Greens g Blues b Lightness l Colorfulness c, s Hue h Opponent a a Opponent b b Value v"},{"location":"interpolation/#powerless-hues","title":"Powerless Hues","text":"

Experimental

This feature is provided to give parity with CSS behavior. As the spec is still in flux, behavior is subject to change or feature could be removed entirely. Use at your own risk.

Normally, ColorAide respects the user's explicitly defined hues. This gives the user power to do things like masking off all channels but the hue to interpolate only the hue.

>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112683790>\n

But when doing this, a user must explicitly define the hue as achromatic if they want the hue to be ignored. Conversions of achromatic colors to a cylindrical space will, in most cases, have the hue automatically set to undefined.

>>> Color.interpolate(['oklch(1 0 0)', 'oklch(0.75 0.2 180)'], space='oklch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bedad0>\n>>> Color.interpolate(['oklch(1 0 None)', 'oklch(0.75 0.2 180)'], space='oklch')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128c6750>\n

CSS has the concept of powerless hues which causes explicitly defined hues to be powerless (or act as undefined) when a color is considered achromatic. This means a user never has to think about achromatic hues, so even if the erroneously define a hue, they will automatically be treated as undefined when interpolating. ColorAide implements this behavior via the powerless option.

>>> Color.interpolate(['oklch(1 0 0)', 'oklch(0.75 0.2 180)'], space='oklch', powerless=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112682e50>\n>>> Color.interpolate(['oklch(1 0 None)', 'oklch(0.75 0.2 180)'], space='oklch', powerless=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c80c50>\n

The one downside is that control over the hue will be diminished to some degree as ColorAide will no longer respect a user's explicit hue if the color is determined to be achromatic.

>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bec850>\n>>> Color.interpolate(['oklch(none none 0)', 'oklch(0.75 0.2 360)'], space='oklch', hue='specified', powerless=True)\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112bb2910>\n
"},{"location":"manipulation/","title":"Manipulating Colors","text":"

Once a Color object is created, you have access to all the color channels. Color channels can be read individually or extracted all at once. Getting and setting color channels is flexible and easy, allowing for intuitive access.

"},{"location":"manipulation/#accessing-coordinates","title":"Accessing Coordinates","text":"

There are various ways to get and set the current values of color coordinates. Colors can be accessed by channel name or numerical index directly. We can also manipulate colors within different color spaces.

"},{"location":"manipulation/#access-by-channel-name","title":"Access By Channel Name","text":"

One of the more intuitive ways to access color values is by channel name. Each color space defines the name of each of the available channels. alpha is the one channel name that is always constant no matter the color space.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color['r']\n1.0\n>>> color['g']\n0.6470588235294118\n>>> color['b']\n0.0\n>>> color['alpha']\n1.0\n

Some channels may be also be recognized using an alias. Check the color space's documentation to learn the recognized channel names and aliases.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color['red'] = 0\n>>> color['green'] = 0\n>>> color['blue'] = 1\n>>> color\ncolor(srgb 0 0 1 / 1)\n
"},{"location":"manipulation/#access-by-index","title":"Access By Index","text":"

Color channels can also be read or set by index. Channels are always in logical order. This means, for instance, an RGB color space will have its channel in the order of r, g, b, and alpha. Thealpha channel always being the last channel in any color space. Check out the color space's documentation to learn more about available channels and the order in which they are stored.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color[0]\n1.0\n>>> color[1]\n0.6470588235294118\n>>> color[2]\n0.0\n>>> color[3]\n1.0\n

Because a Color object essentially operates similar to a list, negative values are also allowed.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color[-1] = 0.5\n>>> color\ncolor(srgb 1 0.64706 0 / 0.5)\n
"},{"location":"manipulation/#access-by-iteration","title":"Access By Iteration","text":"

Color objects can also be treated as an iterable object. This allows us to simply loop through the values.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> [c for c in color]\n[1.0, 0.6470588235294118, 0.0, 1.0]\n
"},{"location":"manipulation/#access-by-slicing","title":"Access By Slicing","text":"

As previously mentioned, Color objects operate very similar to lists, and as such, can also be read or set via slicing.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color[:-1]\n[1.0, 0.6470588235294118, 0.0]\n>>> color[:-1] = [0, 0, 1]\n>>> color\ncolor(srgb 0 0 1 / 1)\n
"},{"location":"manipulation/#access-by-type","title":"Access by Type","text":"

New 2.0

When dealing with colors, you have two types of channels: color channels and an alpha channel. These values can be accessed and separated by slicing as mentioned earlier, but some convenience functions have been added to make this easier. coords() and alpha() will retrieve the color channels and the alpha channel respectively.

>>> color = Color(\"srgb\", [1, 0, 1], 0.5)\n>>> color\ncolor(srgb 1 0 1 / 0.5)\n>>> color.alpha()\n0.5\n>>> color.coords()\n[1.0, 0.0, 1.0]\n

In addition, both of these functions offer a special parameter nans that controls whether undefined values are returned as specified or whether they are resolved to defined values.

>>> color = Color(\"hsl\", [NaN, 0, 0.75], 0.5)\n>>> color\ncolor(--hsl none 0 0.75 / 0.5)\n>>> color.coords()\n[nan, 0.0, 0.75]\n>>> color.coords(nans=False)\n[0.0, 0.0, 0.75]\n
"},{"location":"manipulation/#access-by-functions","title":"Access By Functions","text":"

Colors can also be accessed and modified in more advanced ways with special access functions get() and set().

get() provides access to any channel via the channel name for a given color space, but what sets it apart from other channel access methods is that it can indirectly access channels in other color spaces as well.

>>> color = Color(\"pink\")\n>>> color\ncolor(srgb 1 0.75294 0.79608 / 1)\n>>> color.get('red')\n1.0\n>>> color.get('oklch.hue')\n7.085489349755127\n

Like get(), set() is a method that allows for the setting of any color channel via the color channel names. The value can be set via numerical values or functions with more complex logic.

>>> color = Color(\"pink\")\n>>> color\ncolor(srgb 1 0.75294 0.79608 / 1)\n>>> color.set('blue', 0.5)\ncolor(srgb 1 0.75294 0.5 / 1)\n>>> color.set('green', lambda g: g * 1.3)\ncolor(srgb 1 0.97882 0.5 / 1)\n

Since set() returns a reference to the current color object, we can also chain multiple set() operations.

>>> color = Color('black')\n>>> color\ncolor(srgb 0 0 0 / 1)\n>>> color.set('red', 1).set('green', 1)\ncolor(srgb 1 1 0 / 1)\n

Even more interesting is that, like get(), you can modify a channel in another color space indirectly.

>>> color = Color(\"orange\")\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color.set('oklab.lightness', 0.50)\ncolor(srgb 0.61518 0.28886 -0.22143 / 1)\n

When getting/setting a color channel in a different color space than the current color space, the underlying color must be converted to the target color space in order to access the channel. When doing this to get/set multiple channels, this can be a bit inefficient. In order to make such operations more efficient, both get() and set() allow for bulk operations. When performing bulk channel operations, the channels operations are performed in the order they are specified; therefore, it is important to group together channels of the same color space to ensure they are accessed with a single conversion.

To get multiple channels, simply provide a list of channels.

>>> color = Color('orange')\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color.get(['oklch.lightness', 'oklch.hue', 'alpha'])\n[0.7926884361521512, 70.66991620195026, 1.0]\n

To set multiple channels, pass a single dictionary containing the channel names and values.

>>> color = Color('orange')\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color.set(\n...     {\n...         'oklch.lightness': lambda l: l - l * 0.25,\n...         'oklch.hue': 270\n...     }\n... )\ncolor(srgb 0.34573 0.45438 0.89059 / 1)\n

Indirect Channel Modifications

Indirect channel modification is very useful, but keep in mind that it may give you access to color spaces that are incompatible due to gamut size. Additionally, the feature converts the color to the target color space, modifies it, and then converts it back making it susceptible to any possible round trip errors.

New in 1.5: Getting/Setting Multiple Channels

"},{"location":"manipulation/#undefined-values","title":"Undefined Values","text":"

Colors can sometimes have undefined channels. This can actually happen in a number of ways. In almost all cases, undefined values are generated or manually inserted in order to help out with interpolation.

  1. Hues can naturally become undefined if the color is achromatic.

    >>> color = Color('white').convert('hsl')\n>>> color[:]\n[nan, 0.0, 1.0, 1.0]\n
  2. When specifying raw data, channels can be explicitly set to undefined, and when an insufficient amount of channel data is provided, the missing channels will be assumed as undefined, the exception is the alpha channel which is assumed to be 1 unless explicitly defined or explicitly set as undefined.

    >>> Color('srgb', [1])[:]\n[1.0, nan, nan, 1.0]\n>>> Color('srgb', [1, 0, 0], NaN)[:]\n[1.0, 0.0, 0.0, nan]\n
  3. Undefined values can also occur when a user specifies a channel with the none keyword in CSS syntax.

    >>> from coloraide import NaN\n>>> color = Color(\"srgb\", [0.3, NaN, 0.4])\n>>> color[:]\n[0.3, nan, 0.4, 1.0]\n>>> color = Color('rgb(30% none 40%)')\n>>> color[:]\n[0.3, nan, 0.4, 1.0]\n
  4. Lastly, a user can use the mask method which is a quick way to set one or multiple channels as undefined. Additionally, it returns a clone leaving the original untouched by default.

    >>> Color('white')[:]\n[1.0, 1.0, 1.0, 1.0]\n>>> Color('white').mask(['red', 'green'])[:]\n[nan, nan, 1.0, 1.0]\n

    The alpha channel can also be masked:

    >>> Color('white').mask('alpha')[-1]\nnan\n

    You can also do inverse masks, or masks that apply to every channel not specified.

    >>> c = Color('white').mask('blue', invert=True)\n>>> c[:]\n[nan, nan, 1.0, nan]\n
"},{"location":"manipulation/#checking-for-undefined-values","title":"Checking for Undefined Values","text":"

As previously mentioned, a color channel can be undefined for a number of reasons. And in cases such as interpolation, undefined values can even be useful. On the other hand, sometimes knowing there is an undefined value or being able to ignore it can be useful.

Undefined values are represented as the float value NaN. And since NaN values are not numbers \u2013 hence the name \"not a number\" \u2013 they don't quite work the same as normal numbers. They don't contribute to math operations like add, multiply, and divide. Any math operation performed with a NaN will simply yield NaN. NaN values are essentially infectious.

At first glance, the behavior of NaN values can seem confusing, but it is actually pretty intuitive. If we define a color with an undefined channel, and try to add to that value, what should we get? In reality, if the value is undefined, how could we possibly add to it? The only sane answer is to return NaN again.

>>> color = Color('color(srgb 1 none 1)')\n>>> color['green'] + 0.5\nnan\n

Because a NaN (or undefined value) may cause surprising results, it can be useful to check if a hue (or any channel) is undefined before applying certain operations where such a value may be undesirable, especially if the color potentially came from an unknown source. To make checking for undefined values easy, the convenience function is_nan has been made available. You can simply give is_nan the property you wish to check, and it will return either True or False.

>>> Color('hsl(none 0% 100%)').is_nan('hue')\nTrue\n

This is equivalent to using the math library and comparing the value directly:

>>> import math\n>>> math.isnan(Color('hsl(none 0% 100%)')['hue'])\nTrue\n
"},{"location":"manipulation/#forcing-defined-values","title":"Forcing Defined Values","text":"

Another way to deal with NaN values is to just ignore them. get(), set(), coords(), and alpha() all can use the nans option to ensure read operations return a defined value.

>>> c = Color('srgb', [])\n>>> c\ncolor(srgb none none none / 1)\n>>> c.get('red', nans=False)\n0.0\n>>> c.set('green', lambda x: x + 3, nans=False)\ncolor(srgb none 3 none / 1)\n

set()

In the context of set(), nans specifically ensures that when a callback function is provided that the input value is transformed into a real value opposed to an undefined value.

We can also use normalize() to just set all channels to defined values, but keep mind, when an achromatic color has a real hue, they will then influence interpolation results if interpolating in that same color space.

>>> c = Color('srgb', [])\n>>> c.normalize(nans=False)\ncolor(srgb 0 0 0 / 1)\n
"},{"location":"manipulation/#how-are-undefined-values-resolved","title":"How are Undefined Values Resolved?","text":"

ColorAide will resolve undefined values when necessary. Resolving undefined values may be needed to compute color distance, convert a color, serialize a color, or various other reasons.

Normally, an undefined value defaults to 0 when forced to be defined, but there are a few cases where this may not always be true.

  1. When a color is achromatic the hue becomes meaningless in most cylindrical color spaces. This makes sense as achromatic colors have no hues, but this is also because the algorithms usually work out this way. When chroma is small enough, it usually makes the hue mathematically insignificant. In these cases, when a hue must be defined, we will generally assume 0 as an arbitrary default, but there are some color spaces who have algorithms where the hue actually becomes more important for precise conversions.

    The color spaces CAM16 JMh and HCT are color models that allow you to set the viewing environment. One of the options determines whether the eye is adapted to the illuminant or not. If not adapted, which is our default for both CAM16 JMh and HCT, you can get an achromatic response where grayscale colors lean heavily into one specific hue. Additionally, achromatic chroma may grow to a value much greater than 0 as lightness increases. If we were to use 0 as a default for chroma and/or hue, we'd actually not convert back to a real achromatic color.

    We can see in the example below that using 0 for an undefined hue in CAM16 JMh will not convert gray back to sRGB properly, but using the one calculated for the color space gets us much closer.

    >>> srgb = Color('gray')\n>>> srgb\ncolor(srgb 0.50196 0.50196 0.50196 / 1)\n>>> jmh = srgb.convert('cam16-jmh')\n>>> jmh.coords(nans=False)\n[43.042092459543426, 1.4670107518796203, 209.53509867238978]\n>>> jmh.convert('srgb')\ncolor(srgb 0.50196 0.50196 0.50196 / 1)\n>>> jmh.set('hue', 0).convert('srgb')\ncolor(srgb 0.51857 0.49597 0.49766 / 1)\n

    For color spaces with more dynamic achromatic response, ColorAide will resolve undefined hues and chroma with real values that are neutral for that color's given lightness. This doesn't just apply to cylindrical spaces either. This behavior can be seen in non cylindrical spaces as well, like the Lab form of CAM16.

    >>> Color('gray').convert('cam16')\ncolor(--cam16 43.042 -1.2764 -0.72317 / 1)\n>>> Color('cam16', [43.042, NaN, NaN]).normalize(nans=False)\ncolor(--cam16 43.042 -1.2763 -0.72315 / 1)\n

    The selected values may not always perfectly precise, but they are much better than blindly assuming zero.

    There are a number of spaces that benefit from this approach: Jzazbz/JzCzhz, IPT, IgPgTg.

  2. Most of the time, if you set all color channels to undefined, when resolved, the color will be black (or white in the case of CMYK). Unfortunately, using 0 for undefined channels in some color spaces can create colors outside the viewable gamut. One such example is ACEScct (a logarithmic encoding of ACES) which has a greater value than zero for black. In this case, setting undefined channels to zero will cause nonsense colors. In this specific case, we use ACEScct's value for black instead of 0 for more a more practical default.

    >>> aces = Color('black').convert('acescct')\n>>> aces\ncolor(--acescct 0.07291 0.07291 0.07291 / 1)\n>>> aces.mask(['alpha'], invert=True, in_place=True)\ncolor(--acescct none none none / 1)\n>>> aces.coords(nans=False)\n[0.0729055341958355, 0.0729055341958355, 0.0729055341958355]\n>>> aces.in_gamut()\nTrue\n>>> aces[:] = [0] * 3\n>>> aces.in_gamut()\nFalse\n

    New 2.3

    ACEScc, another color space that performs a logarithmic encoding of ACES, will also resolve undefined channels to a non-zero value, not because zero is out of gamut, but to ensure consistency across all ACES color spaces which resolve to zero or non-zero equivalent values.

"},{"location":"manipulation/#achromatic-colors","title":"Achromatic Colors","text":"

An achromatic color is a color without any real hue. Essentially, it is devoid of color leaving only shades of gray. Different color spaces represent achromatic colors in different ways.

ColorAide has some special handling of achromatic colors and a few ways to test if a color is achromatic.

"},{"location":"manipulation/#checking-for-achromatic-colors","title":"Checking For Achromatic Colors","text":"

New 2.0

ColorAide generally respects an input color's defined channels, but during conversion, or if normalize() is called, cylindrical color spaces will have their hue set to undefined if the color is achromatic (or very close to achromatic). One easy way to check for achromatic colors is simply to check if the hue is undefined with is_nan().

>>> c = Color('gray').convert('hsl')\n>>> c.is_nan('hue')\nTrue\n

Unfortunately, this assumes that the hue has not been manually altered and this doesn't work with non cylindrical colors. Luckily, ColorAide has a universal way to check if any color is achromatic by using is_achromatic().

>>> color1 = Color('orange')\n>>> color1\ncolor(srgb 1 0.64706 0 / 1)\n>>> color1.is_achromatic()\nFalse\n>>> color2 = Color('gray').convert('lab')\n>>> color2\ncolor(--lab 53.585 0 0 / 1)\n>>> color2.is_achromatic()\nTrue\n>>> color3 = Color('darkgray').convert('hsl').set('hue', 270)\n>>> color3\ncolor(--hsl 270 0 0.66275 / 1)\n>>> color3.is_achromatic()\nTrue\n

is_achromatic() tries to use a reasonable threshold to determine achromatic colors. The method used is usually specific to the color space as it is fastest to test in the color space being evaluated.

"},{"location":"manipulation/#normalizing-achromatic-colors","title":"Normalizing Achromatic Colors","text":"

When ColorAide converts to a cylindrical color, if the color is achromatic, the hue will get set as undefined. This is mainly because when a color gets very close to achromatic, the hues can become nonsensical. Many cylindrical spaces, as chroma (or saturation) approaches zero, the color approaches being achromatic. And as chroma gets smaller, the impact of the hue becomes smaller and smaller. In these cases, when we get very close to achromatic, we don't care what the hue is, so it gets set as undefined. Additionally, having hue set to undefined allows us to interpolate achromatic colors in a sane way, see Interpolation for more info.

There are times that a color can be defined such that it is not in this normalized achromatic state. We can manually define a color not in this state, and we can also force a color out of this state.

Here we can disable the normalization when converting. We can do this with convert() and update()

>>> Color('white').convert('lch')\ncolor(--lch 100 0 none / 1)\n>>> Color('white').convert('lch', norm=False)\ncolor(--lch 100 0 0 / 1)\n

We can also remove the normalization by setting nans to False when using normalize().

>>> Color('white').convert('lch').normalize(nans=False)\ncolor(--lch 100 0 0 / 1)\n

If we want to force a color back into this normalized state, we can just call normalize() without any parameters. Normalize will remove any existing undefined channels and set achromatic hues to undefined.

>>> c = Color('lch', [1, 0, 0])\n>>> c\ncolor(--lch 1 0 0 / 1)\n>>> c.normalize()\ncolor(--lch 1 0 none / 1)\n
"},{"location":"playground/","title":"Playground","text":"Notebook"},{"location":"strings/","title":"String Output","text":"

ColorAide supports serializing colors in the same formats that it accepts as inputs. This includes all CSS formats for the associated color spaces, and if a color space is not supported in CSS, the color(space ...) format. ColorAide exposes various options to allow users to serialize in the form they most prefer.

"},{"location":"strings/#convert-to-strings","title":"Convert to Strings","text":"

Colors can be serialized to strings by using the to_string method. The color class will convert the current color into one of the many of CSS formats supported for the given color space.

>>> Color(\"srgb\", [0.5, 0, 1], 0.3).to_string()\n'rgb(127.5 0 255 / 0.3)'\n

There are a number of options that are common among all color spaces, but there are also some color space specific options. We will only cover the color spaces shipped with ColorAide. It is possible to write a color space plugin that uses very different options.

"},{"location":"strings/#common-options","title":"Common Options","text":"

All color spaces support the following parameters.

"},{"location":"strings/#alpha","title":"Alpha","text":"

alpha is set to None by default and controls whether the alpha channel is shown in the serialized output. When in the default state, alpha will only be shown if the alpha channel has a value less than 100%, but if set to True, alpha will always be shown. Setting to False will cause alpha to be ignored in the output.

"},{"location":"strings/#precision","title":"Precision","text":"

precision controls the precision of the output values. The name is a little misleading as it will actually adjust the precision and scale of the values. The default is 5. In some cases, like the sRGB hex output, precision may not really come into play as hex values are rounded to the nearest whole number.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=5, percent=True)\n'rgb(30.346% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=4, percent=True)\n'rgb(30.35% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=3, percent=True)\n'rgb(30.3% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=2, percent=True)\n'rgb(30% 75% 100%)'\n>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=1, percent=True)\n'rgb(30% 80% 100%)'\n

Providing a precision of 0 will simply enable simple rounding to the nearest whole number.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=0, percent=True)\n'rgb(30% 75% 100%)'\n

Providing a precision of -1 is a special input that will give the highest, useful precision that can be given. Precision will be given out to double precision. Higher can be used, but will most likely be unhelpful.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=-1, percent=True)\n'rgb(30.345600000000001% 75% 100%)'\n

One note though, format of the value matters. Here we output in the range of 0 - 255. We can see that a precision of 1, in this case, can throw the color out of gamut. So remember to use a sufficient precision for what you are doing and the values you are working in.

>>> Color(\"rgb(30.3456% 75% 100%)\").to_string(precision=1)\n'rgb(80 200 300)'\n
"},{"location":"strings/#fit","title":"Fit","text":"

fit is set to True by default and controls whether colors are fit to their gamut or not. Some color spaces are technically unbounded, so no fitting may occur in those color spaces. Additionally, some color formats, like sRGB hex, are always fitted (regardless of this setting) as they must fit into the gamut or they cannot be translated.

>>> Color(\"rgb(30% 105% 0%)\").to_string()\n'rgb(109.21 255 60.975)'\n>>> Color(\"rgb(30% 105% 0%)\").to_string(fit=False)\n'rgb(76.5 267.75 0)'\n

Additionally, we can choose a different fitting method by passing fit the name of the method we would like.

>>> Color(\"rgb(30% 105% 0%)\").to_string()\n'rgb(109.21 255 60.975)'\n>>> Color(\"rgb(30% 105% 0%)\").to_string(fit='clip')\n'rgb(76.5 255 0)'\n
"},{"location":"strings/#color","title":"Color","text":"

color, for some color spaces, is the default output, but for others this format can be explicitly requested by setting color to True. If set to True, this will take priority over other format options.

>>> Color(\"rebeccapurple\").to_string(color=True)\n'color(srgb 0.4 0.2 0.6)'\n
"},{"location":"strings/#none","title":"None","text":"

Colors that have undefined channels are internally represented with NaN. On output, these can be displayed as none per the most recent CSS spec. These are very new, so most browsers do not support them. This is disabled by default until a time when this behavior is common enough. NaN values will not survive fitting unless a color channel is naturally undefined. An example would be a hue when the color has saturation or chroma set to zero.

>>> Color('hsl(none 0% 30%)').to_string(none=True)\n'hsl(none 0% 30%)'\n

The one exception is that legacy rgb(), rgba(), hsl(), and hsla() forms (comma separated) do not support none per the CSS spec.

"},{"location":"strings/#format-specific-options","title":"Format Specific Options","text":"

These options may occur in various color spaces depending on the CSS output format.

"},{"location":"strings/#comma","title":"Comma","text":"

In CSS, there are a few color spaces that allow a comma format: srgb and hsl. ColorAide allows these to be read in and to be output in their legacy comma format. These are the only formats that ship with comma support.

If we want commas, we can force the comma syntax by setting comma to True. This can alter some color space output in other subtle ways. As the comma format is the old legacy approach, when sRGB has commas enabled, it will use rgba instead of rgb. If using the non-comma syntax, rgb is always used, even when the color has transparency.

>>> Color(\"rgb(30 75 100 / 20%)\").to_string(comma=True)\n'rgba(30, 75, 100, 0.2)'\n
"},{"location":"strings/#percent","title":"Percent","text":"

RGB, HSL, HWB, CIELab, CIELCh, Oklab, and OkLCh can receive and output colors with optional percents for certain channels. By default, only HSL and HWB output channels with percents by default, and percentages for these color spaces can only be turned off when serializing to the modern syntax (space delimited). When percentage is enabled, ranges will be output in the range of [0%,100%] instead of their usual numeric value.

>>> Color(\"rebeccapurple\").to_string(percent=True)\n'rgb(40% 20% 60%)'\n>>> Color(\"rebeccapurple\").convert('lab').to_string(percent=True)\n'lab(32.393% 30.738% -38.153%)'\n>>> Color(\"rebeccapurple\").convert('hsl').to_string(percent=False)\n'hsl(270 50 40)'\n
"},{"location":"strings/#srgb-specific-options","title":"sRGB Specific Options","text":"

These options are currently specific to the sRGB color space.

"},{"location":"strings/#hex","title":"Hex","text":"

sRGB can output colors to a hex format which is unique compared to HSL and others. Simply enable hex.

>>> Color(\"rebeccapurple\").to_string(hex=True)\n'#663399'\n
"},{"location":"strings/#upper","title":"Upper","text":"

You can force hex to output in uppercase.

>>> Color(\"red\").to_string(hex=True)\n'#ff0000'\n>>> Color(\"red\").to_string(hex=True, upper=True)\n'#FF0000'\n
"},{"location":"strings/#compress","title":"Compress","text":"

When converting to the hex color format, a color can be compressed in certain cases. Enabling compress will compress a hex color if possible.

>>> Color(\"#11223388\").to_string(hex=True)\n'#11223388'\n>>> Color(\"#11223388\").to_string(hex=True, compress=True)\n'#1238'\n
"},{"location":"strings/#names","title":"Names","text":"

sRGB can also output color names. If a color evaluates to a hex code which also evaluates to a color name in the internal CSS color name mapping, then a color name will be returned. If the color does not match a color name, it will fallback to whatever the other options dictate.

>>> Color(\"#663399\").to_string(names=True)\n'rebeccapurple'\n
"},{"location":"temperature/","title":"Correlated Color Temperature","text":"

New 2.4

Correlated color temperature (CCT) is a measurement of the average hue of light as it appears to the eye. It is expressed as the temperature (in Kelvins) something would need to be heated to glow at approximately the same color.

This response can be modeled with a Planckian or black body locus/curve and is often shown in chromaticity diagrams.

1960 Chromaticity Diagram with black body curve in the range of 1,000K - 100,000K

In order to calculate the Planckian locus, color matching functions (CMFs) are needed. The CMFs are obtained from a series of experiments in which subjects set the intensities of three colors required to match a series of monochromatic (single wavelength) lights of equal energy that traverse the visible spectrum. CMFs contain the required data from such experiments and can be used to calculate a number of things, including the Planckian locus.

CMFs

It should be noted that there isn't one set set of CMFs. Over the years there have been multiple attempts to come up with the best CMFs and often done at both 2\u02da and 10\u02da viewing angles. ColorAide only provides CMFS provided by the CIE via coloraide.cmfs, the CIE 1931 2 Degree Standard Observer being the default as it is still the common approach even though better CMFs have been provided.

CMFS coloraide.cmfs.CIE_1931_2DEG coloraide.cmfs.CIE_1964_10DEG coloraide.cmfs.CIE_2015_2DEG coloraide.cmfs.CIE_2015_10DEG

External CMFs could be used as long as they are in the appropriate format and not at increments less than 1nm.

"},{"location":"temperature/#cct","title":"CCT","text":"

When anything gets warm enough it will start to give off light, and the hotter it gets, the more energetic the light is. As an object increases in temperature, it will shift from the red end of the spectrum to the blue end.

ColorAide provides the blackbody() method to generate colors along the black body curve. Simply give it a color space in which the color should be generated and a temperature in Kelvin and ColorAide will return an approximate color along the black body locus.

>>> Steps([Color.blackbody('srgb', t) for t in range(1000, 15000, 50)])\n[color(srgb 1 0.18142 0 / 1), color(srgb 1 0.22157 0 / 1), color(srgb 1 0.25489 0 / 1), color(srgb 1 0.28375 0 / 1), color(srgb 1 0.3093 0 / 1), color(srgb 1 0.33241 0 / 1), color(srgb 1 0.35347 0 / 1), color(srgb 1 0.3729 0 / 1), color(srgb 1 0.39084 0 / 1), color(srgb 1 0.40753 0 / 1), color(srgb 1 0.42314 0 / 1), color(srgb 1 0.43781 0 / 1), color(srgb 1 0.45159 0 / 1), color(srgb 1 0.46445 0 / 1), color(srgb 1 0.47663 0 / 1), color(srgb 1 0.48821 0 / 1), color(srgb 1 0.49913 0 / 1), color(srgb 1 0.50946 0 / 1), color(srgb 1 0.5194 0.00025 / 1), color(srgb 1 0.53171 0.05099 / 1), color(srgb 1 0.54362 0.08665 / 1), color(srgb 1 0.55508 0.11675 / 1), color(srgb 1 0.56621 0.14078 / 1), color(srgb 1 0.57692 0.16381 / 1), color(srgb 1 0.58734 0.18409 / 1), color(srgb 1 0.59742 0.20351 / 1), color(srgb 1 0.60722 0.22203 / 1), color(srgb 1 0.6168 0.23892 / 1), color(srgb 1 0.62597 0.25645 / 1), color(srgb 1 0.63495 0.27277 / 1), color(srgb 1 0.64376 0.28801 / 1), color(srgb 1 0.65219 0.30409 / 1), color(srgb 1 0.66048 0.3192 / 1), color(srgb 1 0.66862 0.33348 / 1), color(srgb 1 0.67644 0.34807 / 1), color(srgb 1 0.68406 0.36243 / 1), color(srgb 1 0.69155 0.37611 / 1), color(srgb 1 0.69892 0.38918 / 1), color(srgb 1 0.70597 0.40288 / 1), color(srgb 1 0.71288 0.41619 / 1), color(srgb 1 0.71969 0.42897 / 1), color(srgb 1 0.7264 0.44127 / 1), color(srgb 1 0.73289 0.45367 / 1), color(srgb 1 0.73912 0.46623 / 1), color(srgb 1 0.74528 0.47835 / 1), color(srgb 1 0.75136 0.49007 / 1), color(srgb 1 0.75737 0.50143 / 1), color(srgb 1 0.7632 0.51278 / 1), color(srgb 1 0.76876 0.52444 / 1), color(srgb 1 0.77426 0.53576 / 1), color(srgb 1 0.7797 0.54675 / 1), color(srgb 1 0.78508 0.55745 / 1), color(srgb 1 0.79039 0.56787 / 1), color(srgb 1 0.79556 0.57824 / 1), color(srgb 1 0.80043 0.58891 / 1), color(srgb 1 0.80525 0.59931 / 1), color(srgb 1 0.81002 0.60946 / 1), color(srgb 1 0.81474 0.61937 / 1), color(srgb 1 0.81941 0.62905 / 1), color(srgb 1 0.82404 0.63852 / 1), color(srgb 1 0.82862 0.64778 / 1), color(srgb 1 0.83282 0.65746 / 1), color(srgb 1 0.83698 0.66693 / 1), color(srgb 1 0.8411 0.6762 / 1), color(srgb 1 0.84518 0.68528 / 1), color(srgb 1 0.84923 0.69418 / 1), color(srgb 1 0.85323 0.7029 / 1), color(srgb 1 0.8572 0.71145 / 1), color(srgb 1 0.86113 0.71985 / 1), color(srgb 1 0.86499 0.72814 / 1), color(srgb 1 0.86851 0.73667 / 1), color(srgb 1 0.87201 0.74505 / 1), color(srgb 1 0.87547 0.75327 / 1), color(srgb 1 0.8789 0.76134 / 1), color(srgb 1 0.88231 0.76927 / 1), color(srgb 1 0.88568 0.77707 / 1), color(srgb 1 0.88902 0.78474 / 1), color(srgb 1 0.89233 0.79228 / 1), color(srgb 1 0.89561 0.7997 / 1), color(srgb 1 0.89887 0.80701 / 1), color(srgb 1 0.9021 0.8142 / 1), color(srgb 1 0.90496 0.82155 / 1), color(srgb 1 0.9078 0.82879 / 1), color(srgb 1 0.91062 0.83591 / 1), color(srgb 1 0.91341 0.84293 / 1), color(srgb 1 0.91618 0.84983 / 1), color(srgb 1 0.91892 0.85664 / 1), color(srgb 1 0.92165 0.86334 / 1), color(srgb 1 0.92435 0.86995 / 1), color(srgb 1 0.92702 0.87646 / 1), color(srgb 1 0.92968 0.88288 / 1), color(srgb 1 0.93231 0.88921 / 1), color(srgb 1 0.93492 0.89545 / 1), color(srgb 1 0.93751 0.90161 / 1), color(srgb 1 0.94008 0.90769 / 1), color(srgb 1 0.94242 0.91377 / 1), color(srgb 1 0.94464 0.9198 / 1), color(srgb 1 0.94685 0.92575 / 1), color(srgb 1 0.94904 0.93162 / 1), color(srgb 1 0.95121 0.9374 / 1), color(srgb 1 0.95337 0.94312 / 1), color(srgb 1 0.9555 0.94876 / 1), color(srgb 1 0.95762 0.95432 / 1), color(srgb 1 0.95973 0.95982 / 1), color(srgb 1 0.96181 0.96525 / 1), color(srgb 1 0.96388 0.97061 / 1), color(srgb 1 0.96594 0.97591 / 1), color(srgb 1 0.96798 0.98114 / 1), color(srgb 1 0.97 0.98631 / 1), color(srgb 1 0.972 0.99142 / 1), color(srgb 1 0.974 0.99646 / 1), color(srgb 0.99855 0.97455 1 / 1), color(srgb 0.99365 0.97172 1 / 1), color(srgb 0.98885 0.96895 1 / 1), color(srgb 0.98416 0.96606 1 / 1), color(srgb 0.97956 0.96315 1 / 1), color(srgb 0.97506 0.96031 1 / 1), color(srgb 0.97065 0.95752 1 / 1), color(srgb 0.96632 0.9548 1 / 1), color(srgb 0.96208 0.95214 1 / 1), color(srgb 0.95793 0.94954 1 / 1), color(srgb 0.95385 0.94699 1 / 1), color(srgb 0.94985 0.94449 1 / 1), color(srgb 0.94593 0.94205 1 / 1), color(srgb 0.94207 0.93966 1 / 1), color(srgb 0.93829 0.93731 1 / 1), color(srgb 0.93458 0.93502 1 / 1), color(srgb 0.93094 0.93277 1 / 1), color(srgb 0.92735 0.93056 1 / 1), color(srgb 0.92384 0.9284 1 / 1), color(srgb 0.92038 0.92627 1 / 1), color(srgb 0.91698 0.92419 1 / 1), color(srgb 0.91364 0.92215 1 / 1), color(srgb 0.91036 0.92015 1 / 1), color(srgb 0.90713 0.91818 1 / 1), color(srgb 0.90395 0.91625 1 / 1), color(srgb 0.90083 0.91435 1 / 1), color(srgb 0.89776 0.91249 1 / 1), color(srgb 0.89474 0.91066 1 / 1), color(srgb 0.89176 0.90887 1 / 1), color(srgb 0.88883 0.9071 1 / 1), color(srgb 0.88604 0.90525 1 / 1), color(srgb 0.88329 0.90342 1 / 1), color(srgb 0.88058 0.90163 1 / 1), color(srgb 0.87791 0.89987 1 / 1), color(srgb 0.87529 0.89814 1 / 1), color(srgb 0.8727 0.89644 1 / 1), color(srgb 0.87016 0.89476 1 / 1), color(srgb 0.86765 0.89312 1 / 1), color(srgb 0.86518 0.8915 1 / 1), color(srgb 0.86274 0.8899 1 / 1), color(srgb 0.86035 0.88833 1 / 1), color(srgb 0.85798 0.88679 1 / 1), color(srgb 0.85565 0.88527 1 / 1), color(srgb 0.85336 0.88377 1 / 1), color(srgb 0.8511 0.8823 1 / 1), color(srgb 0.84886 0.88085 1 / 1), color(srgb 0.84666 0.87942 1 / 1), color(srgb 0.84449 0.87802 1 / 1), color(srgb 0.84235 0.87663 1 / 1), color(srgb 0.84024 0.87527 1 / 1), color(srgb 0.83816 0.87392 1 / 1), color(srgb 0.83611 0.8726 1 / 1), color(srgb 0.83408 0.87129 1 / 1), color(srgb 0.83208 0.87 1 / 1), color(srgb 0.8301 0.86873 1 / 1), color(srgb 0.82816 0.86748 1 / 1), color(srgb 0.82623 0.86625 1 / 1), color(srgb 0.82433 0.86504 1 / 1), color(srgb 0.82246 0.86384 1 / 1), color(srgb 0.82061 0.86265 1 / 1), color(srgb 0.81878 0.86149 1 / 1), color(srgb 0.81698 0.86034 1 / 1), color(srgb 0.8152 0.8592 1 / 1), color(srgb 0.81343 0.85808 1 / 1), color(srgb 0.8117 0.85698 1 / 1), color(srgb 0.80998 0.85589 1 / 1), color(srgb 0.80828 0.85481 1 / 1), color(srgb 0.8066 0.85375 1 / 1), color(srgb 0.80495 0.8527 1 / 1), color(srgb 0.80331 0.85167 1 / 1), color(srgb 0.80176 0.85062 1 / 1), color(srgb 0.80022 0.84959 1 / 1), color(srgb 0.79871 0.84857 1 / 1), color(srgb 0.79721 0.84756 1 / 1), color(srgb 0.79573 0.84656 1 / 1), color(srgb 0.79426 0.84558 1 / 1), color(srgb 0.79282 0.84461 1 / 1), color(srgb 0.79139 0.84365 1 / 1), color(srgb 0.78997 0.8427 1 / 1), color(srgb 0.78857 0.84177 1 / 1), color(srgb 0.78719 0.84084 1 / 1), color(srgb 0.78582 0.83993 1 / 1), color(srgb 0.78447 0.83902 1 / 1), color(srgb 0.78313 0.83813 1 / 1), color(srgb 0.7818 0.83725 1 / 1), color(srgb 0.78049 0.83638 1 / 1), color(srgb 0.7792 0.83552 1 / 1), color(srgb 0.77792 0.83466 1 / 1), color(srgb 0.77665 0.83382 1 / 1), color(srgb 0.77539 0.83299 1 / 1), color(srgb 0.77415 0.83217 1 / 1), color(srgb 0.77292 0.83135 1 / 1), color(srgb 0.77174 0.83054 1 / 1), color(srgb 0.77057 0.82974 1 / 1), color(srgb 0.76941 0.82894 1 / 1), color(srgb 0.76827 0.82816 1 / 1), color(srgb 0.76714 0.82738 1 / 1), color(srgb 0.76602 0.82661 1 / 1), color(srgb 0.76491 0.82585 1 / 1), color(srgb 0.76381 0.8251 1 / 1), color(srgb 0.76272 0.82435 1 / 1), color(srgb 0.76165 0.82362 1 / 1), color(srgb 0.76058 0.82289 1 / 1), color(srgb 0.75953 0.82217 1 / 1), color(srgb 0.75848 0.82145 1 / 1), color(srgb 0.75745 0.82075 1 / 1), color(srgb 0.75642 0.82005 1 / 1), color(srgb 0.75541 0.81935 1 / 1), color(srgb 0.7544 0.81867 1 / 1), color(srgb 0.75341 0.81799 1 / 1), color(srgb 0.75242 0.81732 1 / 1), color(srgb 0.75144 0.81666 1 / 1), color(srgb 0.75048 0.816 1 / 1), color(srgb 0.74952 0.81535 1 / 1), color(srgb 0.74857 0.8147 1 / 1), color(srgb 0.74763 0.81406 1 / 1), color(srgb 0.74669 0.81343 1 / 1), color(srgb 0.74577 0.81281 1 / 1), color(srgb 0.74485 0.81219 1 / 1), color(srgb 0.74395 0.81157 1 / 1), color(srgb 0.74308 0.81096 1 / 1), color(srgb 0.74222 0.81036 1 / 1), color(srgb 0.74137 0.80977 1 / 1), color(srgb 0.74052 0.80918 1 / 1), color(srgb 0.73968 0.80859 1 / 1), color(srgb 0.73886 0.80801 1 / 1), color(srgb 0.73803 0.80744 1 / 1), color(srgb 0.73722 0.80687 1 / 1), color(srgb 0.73641 0.80631 1 / 1), color(srgb 0.73561 0.80575 1 / 1), color(srgb 0.73481 0.80519 1 / 1), color(srgb 0.73403 0.80465 1 / 1), color(srgb 0.73325 0.8041 1 / 1), color(srgb 0.73247 0.80357 1 / 1), color(srgb 0.7317 0.80303 1 / 1), color(srgb 0.73094 0.8025 1 / 1), color(srgb 0.73019 0.80198 1 / 1), color(srgb 0.72944 0.80146 1 / 1), color(srgb 0.7287 0.80094 1 / 1), color(srgb 0.72796 0.80043 1 / 1), color(srgb 0.72723 0.79993 1 / 1), color(srgb 0.72651 0.79943 1 / 1), color(srgb 0.72579 0.79893 1 / 1), color(srgb 0.72508 0.79844 1 / 1), color(srgb 0.72437 0.79795 1 / 1), color(srgb 0.72367 0.79746 1 / 1), color(srgb 0.72297 0.79698 1 / 1), color(srgb 0.72228 0.79651 1 / 1), color(srgb 0.7216 0.79603 1 / 1), color(srgb 0.72092 0.79557 1 / 1), color(srgb 0.72025 0.7951 1 / 1), color(srgb 0.71958 0.79464 1 / 1), color(srgb 0.71892 0.79418 1 / 1), color(srgb 0.71826 0.79373 1 / 1), color(srgb 0.71761 0.79328 1 / 1), color(srgb 0.71697 0.79284 1 / 1), color(srgb 0.71635 0.7924 1 / 1), color(srgb 0.71574 0.79196 1 / 1), color(srgb 0.71514 0.79153 1 / 1), color(srgb 0.71454 0.7911 1 / 1), color(srgb 0.71394 0.79068 1 / 1), color(srgb 0.71335 0.79025 1 / 1), color(srgb 0.71276 0.78984 1 / 1), color(srgb 0.71218 0.78942 1 / 1), color(srgb 0.7116 0.78901 1 / 1), color(srgb 0.71103 0.7886 1 / 1), color(srgb 0.71046 0.78819 1 / 1), color(srgb 0.7099 0.78779 1 / 1), color(srgb 0.70933 0.78739 1 / 1)]\n

ColorAide also provides a method cct() which allows you to get an associated temperature and \u2206uv from a given color. \u2206uv will be discussed later.

>>> color = Color.blackbody('srgb', 2000)\n>>> color\ncolor(srgb 1 0.54362 0.08665 / 1)\n>>> color.cct()\n[1999.9999999999993, -5.5295962522242654e-17]\n
"},{"location":"temperature/#duv","title":"Duv","text":"

In addition to CCT, there is also the concept of Duv or \u2206uv. In the following image, we've now drawn perpendicular lines that intersect the black body curve. These lines are called isotherms. An isotherm is simply a line connecting points having the same temperature at a given time or on average over a given period. In the case of colors, they connect a number of colors that are close to the locus with the same temperature. \u2206uv describes the distance a given uv point is away from the associated uv point on the black body curve, positive being above the curve and negative being below the curve

1960 Chromaticity Diagram with black body curve and isotherms indicating \u00b1 0.03 \u2206uv.

We can calculate a color's associated temperature and its \u2206uv along the associated isotherm.

>>> 'CCT: {}K Duv: {}'.format(*Color('yellow').cct())\n'CCT: 3934.7189746569234K Duv: 0.04024818555895665'\n

CCT and \u2206uv can also be used together to get a specific color that satisfies those requirements.

>>> Color.blackbody('srgb-linear', *Color('yellow').cct())\ncolor(srgb-linear 1 1 0 / 1)\n
"},{"location":"temperature/#limitations","title":"Limitations","text":"

All algorithms to calculate to and from CCT have some limitations and are only approximations, some being more accurate than others. Many algorithms are only accurate up to a certain temperature range. Additionally, it is recommend that colors that exhibit a \u2206uv larger than |5e-2| should be considered inaccurate, and some algorithm's may not do as well out to even this limit.

"},{"location":"temperature/#out-of-gamut-temperatures","title":"Out of Gamut Temperatures","text":"

It should be noted that blackbody() normalizes/scales the returned colors by default as the colors are often much too bright initially, all having a max luminance. This scaling is usually done under a linear RGB color space, Linear Rec2020 being the default as it encompasses the entire curve.

Keep in mind that if the color is not in the display gamut it will need to be gamut mapped, and the gamut mapped value will not exhibit the same temperature. How far off it is will be depends on the disparity of the gamut sizes and how the color was gamut mapped.

CCT of 1200K in relation to the sRGB gamut.

Colors that are outside the traditional RGB gamut (0 - 1) will be scaled to be within that range. If the color is beyond the gamut of the scaling color space, it will not convert back to the same temperature.

>>> c = Color.blackbody('srgb', 1200)\n>>> c\ncolor(srgb 1 0.3093 0 / 1)\n>>> c.cct()\n[1305.4626114185926, -0.0053568532479511925]\n

Using a larger gamut, such as Linear Rec. 2020 that encompasses the entire black body curve, can allow for more accurate results throughout the entire range, but this may be impractical if you need to work in a smaller gamut.

The desired RGB color space to scale within can be specified via scale_space. And if no scaling is desired, it can be turned off by setting scale to False.

>>> c1 = Color.blackbody('display-p3', 1200, scale_space='display-p3-linear')\n>>> c1\ncolor(display-p3 1 0.36021 0.0172 / 1)\n>>> c1.cct()\n[1200.0000066930043, -1.093052476567441e-12]\n>>> c2 = Color.blackbody('display-p3', 1200, scale=False)\n>>> c2\ncolor(display-p3 1.6804 0.62798 0.05495 / 1)\n>>> c2.cct()\n[1200.000006693005, -1.0930524765675185e-12]\n
"},{"location":"temperature/#algorithms","title":"Algorithms","text":"

There are quite a few approaches to calculating to and from CCT, each with their strengths and weaknesses. ColorAide currently only supports a few approaches, specifically those that support the concept of CCT and \u2206uv. Each approach is implemented as a CCT plugin.

Algorithm Key Description Robertson\u00a01968 robertson-1968 Uses the CIE 2\u02da Standard Observer and can handle a range of 1000K - \u221e. Ohno\u00a02013 ohno-2013 Utilizes a combined approach of a triangular and parabolic solver. Current implementation allows for a range of 1000K - 100000K.

Robertson 1968 is the current default CCT approach, but any approach can be selected via the method parameter in blackbody() or cct().

>>> Color.blackbody('srgb-linear', 2500, method='ohno-2013').cct(method='ohno-2013')\n[2500.011703361377, 2.3130833455749082e-07]\n>>> Color.blackbody('srgb-linear', 2500, method='robertson-1968').cct(method='robertson-1968')\n[2499.999999999999, 0.0]\n
"},{"location":"temperature/#robertson-1968","title":"Robertson 1968","text":"

The Robertson 1968 CCT algorithm is registered in Color by default

The \"Robertson 1968\" approach was created by A. R. Robertson and is based on the CIE 2\u02da Standard Observer with a range of 1667K - \u221e. This approach uses a look up table containing 31 precalculated points along the black body curve and is used to approximate temperatures in between.

Robertson's approach is a reasonably fast approximation, but can exhibit moderate errors at times. The margin of error gets increasingly larger at very high temperatures approaching infinity.

ColorAide implements the Robertson 1968 approach by faithfully calculating the original 31 points (with later corrections), but it also uses the same approach to extend the lower range from 1667K to 1000K by calculating 16 additional points. There is no change in behavior from 1667K to \u221e, but it will now properly resolve values as low as 1000K as well.

Practical Range

While Robertson's technically supports a range out to infinity, it becomes increasingly less practical after 100000K due to increasingly less accurate results. Even some results below 100000K may already have fairly sizeable errors.

>>> color = Color.blackbody('srgb-linear', 5000, duv=0.02)\n>>> color\ncolor(srgb-linear 0.94158 1 0.49098 / 1)\n>>> color.cct()\n[5000.000000000003, 0.020000000000000025]\n

Because the calculation logic is built into the plugin, you can actually use the plugin to generate a higher resolution table or even generate one using a different set of CMFs. When registering the plugin, you can configure the CMFs and a few other options to customize the look up table.

Parameters Description cmfs Valid CMFs at a resolution greater than or equal to 1nm. white A white point as xy chromaticity coordinates. mired The mired value points to generate in the table (1e6 / Tkelvin = mired). Values should not be at a resolution lower than 1 mired as it can give the algorithm issues. 0 is acceptable though. sigfig Significant figures to round to. This is required to faithfully generate the values as documented in the papers and is set to 5 by default. If set to 0, no rounding will be done. planck_step This controls the resolution at which the wavelengths in the CMFs are used to calculate the points along the Planckian locus. The original values are calculated with a 1nm resolution, so the default is set to 1. If a given table has a lower resolution, such as 5nm, this value can be adjusted to properly work with that table.

To use a different set of CMFS, such as the CIE 1964 10\u02da Standard Observer, we could override the default plugin.

>>> from coloraide.temperature import robertson_1968\n>>> from coloraide import cmfs\n>>> from coloraide import cat\n>>> mired_points = tuple(range(0, 100, 10)) + tuple(range(100, 601, 25)) + tuple(range(625, 1001, 25))\n>>> class Custom(Color):\n...     ...\n... \n>>> Custom.register(\n...     robertson_1968.Robertson1968(cmfs.CIE_1964_10DEG, cat.WHITES['10deg']['D65'], mired_points, 0),\n...     overwrite=True\n... )\n>>> Steps([Color.blackbody('srgb', t) for t in range(1000, 15000, 50)])\n[color(srgb 1 0.18142 0 / 1), color(srgb 1 0.22157 0 / 1), color(srgb 1 0.25489 0 / 1), color(srgb 1 0.28375 0 / 1), color(srgb 1 0.3093 0 / 1), color(srgb 1 0.33241 0 / 1), color(srgb 1 0.35347 0 / 1), color(srgb 1 0.3729 0 / 1), color(srgb 1 0.39084 0 / 1), color(srgb 1 0.40753 0 / 1), color(srgb 1 0.42314 0 / 1), color(srgb 1 0.43781 0 / 1), color(srgb 1 0.45159 0 / 1), color(srgb 1 0.46445 0 / 1), color(srgb 1 0.47663 0 / 1), color(srgb 1 0.48821 0 / 1), color(srgb 1 0.49913 0 / 1), color(srgb 1 0.50946 0 / 1), color(srgb 1 0.5194 0.00025 / 1), color(srgb 1 0.53171 0.05099 / 1), color(srgb 1 0.54362 0.08665 / 1), color(srgb 1 0.55508 0.11675 / 1), color(srgb 1 0.56621 0.14078 / 1), color(srgb 1 0.57692 0.16381 / 1), color(srgb 1 0.58734 0.18409 / 1), color(srgb 1 0.59742 0.20351 / 1), color(srgb 1 0.60722 0.22203 / 1), color(srgb 1 0.6168 0.23892 / 1), color(srgb 1 0.62597 0.25645 / 1), color(srgb 1 0.63495 0.27277 / 1), color(srgb 1 0.64376 0.28801 / 1), color(srgb 1 0.65219 0.30409 / 1), color(srgb 1 0.66048 0.3192 / 1), color(srgb 1 0.66862 0.33348 / 1), color(srgb 1 0.67644 0.34807 / 1), color(srgb 1 0.68406 0.36243 / 1), color(srgb 1 0.69155 0.37611 / 1), color(srgb 1 0.69892 0.38918 / 1), color(srgb 1 0.70597 0.40288 / 1), color(srgb 1 0.71288 0.41619 / 1), color(srgb 1 0.71969 0.42897 / 1), color(srgb 1 0.7264 0.44127 / 1), color(srgb 1 0.73289 0.45367 / 1), color(srgb 1 0.73912 0.46623 / 1), color(srgb 1 0.74528 0.47835 / 1), color(srgb 1 0.75136 0.49007 / 1), color(srgb 1 0.75737 0.50143 / 1), color(srgb 1 0.7632 0.51278 / 1), color(srgb 1 0.76876 0.52444 / 1), color(srgb 1 0.77426 0.53576 / 1), color(srgb 1 0.7797 0.54675 / 1), color(srgb 1 0.78508 0.55745 / 1), color(srgb 1 0.79039 0.56787 / 1), color(srgb 1 0.79556 0.57824 / 1), color(srgb 1 0.80043 0.58891 / 1), color(srgb 1 0.80525 0.59931 / 1), color(srgb 1 0.81002 0.60946 / 1), color(srgb 1 0.81474 0.61937 / 1), color(srgb 1 0.81941 0.62905 / 1), color(srgb 1 0.82404 0.63852 / 1), color(srgb 1 0.82862 0.64778 / 1), color(srgb 1 0.83282 0.65746 / 1), color(srgb 1 0.83698 0.66693 / 1), color(srgb 1 0.8411 0.6762 / 1), color(srgb 1 0.84518 0.68528 / 1), color(srgb 1 0.84923 0.69418 / 1), color(srgb 1 0.85323 0.7029 / 1), color(srgb 1 0.8572 0.71145 / 1), color(srgb 1 0.86113 0.71985 / 1), color(srgb 1 0.86499 0.72814 / 1), color(srgb 1 0.86851 0.73667 / 1), color(srgb 1 0.87201 0.74505 / 1), color(srgb 1 0.87547 0.75327 / 1), color(srgb 1 0.8789 0.76134 / 1), color(srgb 1 0.88231 0.76927 / 1), color(srgb 1 0.88568 0.77707 / 1), color(srgb 1 0.88902 0.78474 / 1), color(srgb 1 0.89233 0.79228 / 1), color(srgb 1 0.89561 0.7997 / 1), color(srgb 1 0.89887 0.80701 / 1), color(srgb 1 0.9021 0.8142 / 1), color(srgb 1 0.90496 0.82155 / 1), color(srgb 1 0.9078 0.82879 / 1), color(srgb 1 0.91062 0.83591 / 1), color(srgb 1 0.91341 0.84293 / 1), color(srgb 1 0.91618 0.84983 / 1), color(srgb 1 0.91892 0.85664 / 1), color(srgb 1 0.92165 0.86334 / 1), color(srgb 1 0.92435 0.86995 / 1), color(srgb 1 0.92702 0.87646 / 1), color(srgb 1 0.92968 0.88288 / 1), color(srgb 1 0.93231 0.88921 / 1), color(srgb 1 0.93492 0.89545 / 1), color(srgb 1 0.93751 0.90161 / 1), color(srgb 1 0.94008 0.90769 / 1), color(srgb 1 0.94242 0.91377 / 1), color(srgb 1 0.94464 0.9198 / 1), color(srgb 1 0.94685 0.92575 / 1), color(srgb 1 0.94904 0.93162 / 1), color(srgb 1 0.95121 0.9374 / 1), color(srgb 1 0.95337 0.94312 / 1), color(srgb 1 0.9555 0.94876 / 1), color(srgb 1 0.95762 0.95432 / 1), color(srgb 1 0.95973 0.95982 / 1), color(srgb 1 0.96181 0.96525 / 1), color(srgb 1 0.96388 0.97061 / 1), color(srgb 1 0.96594 0.97591 / 1), color(srgb 1 0.96798 0.98114 / 1), color(srgb 1 0.97 0.98631 / 1), color(srgb 1 0.972 0.99142 / 1), color(srgb 1 0.974 0.99646 / 1), color(srgb 0.99855 0.97455 1 / 1), color(srgb 0.99365 0.97172 1 / 1), color(srgb 0.98885 0.96895 1 / 1), color(srgb 0.98416 0.96606 1 / 1), color(srgb 0.97956 0.96315 1 / 1), color(srgb 0.97506 0.96031 1 / 1), color(srgb 0.97065 0.95752 1 / 1), color(srgb 0.96632 0.9548 1 / 1), color(srgb 0.96208 0.95214 1 / 1), color(srgb 0.95793 0.94954 1 / 1), color(srgb 0.95385 0.94699 1 / 1), color(srgb 0.94985 0.94449 1 / 1), color(srgb 0.94593 0.94205 1 / 1), color(srgb 0.94207 0.93966 1 / 1), color(srgb 0.93829 0.93731 1 / 1), color(srgb 0.93458 0.93502 1 / 1), color(srgb 0.93094 0.93277 1 / 1), color(srgb 0.92735 0.93056 1 / 1), color(srgb 0.92384 0.9284 1 / 1), color(srgb 0.92038 0.92627 1 / 1), color(srgb 0.91698 0.92419 1 / 1), color(srgb 0.91364 0.92215 1 / 1), color(srgb 0.91036 0.92015 1 / 1), color(srgb 0.90713 0.91818 1 / 1), color(srgb 0.90395 0.91625 1 / 1), color(srgb 0.90083 0.91435 1 / 1), color(srgb 0.89776 0.91249 1 / 1), color(srgb 0.89474 0.91066 1 / 1), color(srgb 0.89176 0.90887 1 / 1), color(srgb 0.88883 0.9071 1 / 1), color(srgb 0.88604 0.90525 1 / 1), color(srgb 0.88329 0.90342 1 / 1), color(srgb 0.88058 0.90163 1 / 1), color(srgb 0.87791 0.89987 1 / 1), color(srgb 0.87529 0.89814 1 / 1), color(srgb 0.8727 0.89644 1 / 1), color(srgb 0.87016 0.89476 1 / 1), color(srgb 0.86765 0.89312 1 / 1), color(srgb 0.86518 0.8915 1 / 1), color(srgb 0.86274 0.8899 1 / 1), color(srgb 0.86035 0.88833 1 / 1), color(srgb 0.85798 0.88679 1 / 1), color(srgb 0.85565 0.88527 1 / 1), color(srgb 0.85336 0.88377 1 / 1), color(srgb 0.8511 0.8823 1 / 1), color(srgb 0.84886 0.88085 1 / 1), color(srgb 0.84666 0.87942 1 / 1), color(srgb 0.84449 0.87802 1 / 1), color(srgb 0.84235 0.87663 1 / 1), color(srgb 0.84024 0.87527 1 / 1), color(srgb 0.83816 0.87392 1 / 1), color(srgb 0.83611 0.8726 1 / 1), color(srgb 0.83408 0.87129 1 / 1), color(srgb 0.83208 0.87 1 / 1), color(srgb 0.8301 0.86873 1 / 1), color(srgb 0.82816 0.86748 1 / 1), color(srgb 0.82623 0.86625 1 / 1), color(srgb 0.82433 0.86504 1 / 1), color(srgb 0.82246 0.86384 1 / 1), color(srgb 0.82061 0.86265 1 / 1), color(srgb 0.81878 0.86149 1 / 1), color(srgb 0.81698 0.86034 1 / 1), color(srgb 0.8152 0.8592 1 / 1), color(srgb 0.81343 0.85808 1 / 1), color(srgb 0.8117 0.85698 1 / 1), color(srgb 0.80998 0.85589 1 / 1), color(srgb 0.80828 0.85481 1 / 1), color(srgb 0.8066 0.85375 1 / 1), color(srgb 0.80495 0.8527 1 / 1), color(srgb 0.80331 0.85167 1 / 1), color(srgb 0.80176 0.85062 1 / 1), color(srgb 0.80022 0.84959 1 / 1), color(srgb 0.79871 0.84857 1 / 1), color(srgb 0.79721 0.84756 1 / 1), color(srgb 0.79573 0.84656 1 / 1), color(srgb 0.79426 0.84558 1 / 1), color(srgb 0.79282 0.84461 1 / 1), color(srgb 0.79139 0.84365 1 / 1), color(srgb 0.78997 0.8427 1 / 1), color(srgb 0.78857 0.84177 1 / 1), color(srgb 0.78719 0.84084 1 / 1), color(srgb 0.78582 0.83993 1 / 1), color(srgb 0.78447 0.83902 1 / 1), color(srgb 0.78313 0.83813 1 / 1), color(srgb 0.7818 0.83725 1 / 1), color(srgb 0.78049 0.83638 1 / 1), color(srgb 0.7792 0.83552 1 / 1), color(srgb 0.77792 0.83466 1 / 1), color(srgb 0.77665 0.83382 1 / 1), color(srgb 0.77539 0.83299 1 / 1), color(srgb 0.77415 0.83217 1 / 1), color(srgb 0.77292 0.83135 1 / 1), color(srgb 0.77174 0.83054 1 / 1), color(srgb 0.77057 0.82974 1 / 1), color(srgb 0.76941 0.82894 1 / 1), color(srgb 0.76827 0.82816 1 / 1), color(srgb 0.76714 0.82738 1 / 1), color(srgb 0.76602 0.82661 1 / 1), color(srgb 0.76491 0.82585 1 / 1), color(srgb 0.76381 0.8251 1 / 1), color(srgb 0.76272 0.82435 1 / 1), color(srgb 0.76165 0.82362 1 / 1), color(srgb 0.76058 0.82289 1 / 1), color(srgb 0.75953 0.82217 1 / 1), color(srgb 0.75848 0.82145 1 / 1), color(srgb 0.75745 0.82075 1 / 1), color(srgb 0.75642 0.82005 1 / 1), color(srgb 0.75541 0.81935 1 / 1), color(srgb 0.7544 0.81867 1 / 1), color(srgb 0.75341 0.81799 1 / 1), color(srgb 0.75242 0.81732 1 / 1), color(srgb 0.75144 0.81666 1 / 1), color(srgb 0.75048 0.816 1 / 1), color(srgb 0.74952 0.81535 1 / 1), color(srgb 0.74857 0.8147 1 / 1), color(srgb 0.74763 0.81406 1 / 1), color(srgb 0.74669 0.81343 1 / 1), color(srgb 0.74577 0.81281 1 / 1), color(srgb 0.74485 0.81219 1 / 1), color(srgb 0.74395 0.81157 1 / 1), color(srgb 0.74308 0.81096 1 / 1), color(srgb 0.74222 0.81036 1 / 1), color(srgb 0.74137 0.80977 1 / 1), color(srgb 0.74052 0.80918 1 / 1), color(srgb 0.73968 0.80859 1 / 1), color(srgb 0.73886 0.80801 1 / 1), color(srgb 0.73803 0.80744 1 / 1), color(srgb 0.73722 0.80687 1 / 1), color(srgb 0.73641 0.80631 1 / 1), color(srgb 0.73561 0.80575 1 / 1), color(srgb 0.73481 0.80519 1 / 1), color(srgb 0.73403 0.80465 1 / 1), color(srgb 0.73325 0.8041 1 / 1), color(srgb 0.73247 0.80357 1 / 1), color(srgb 0.7317 0.80303 1 / 1), color(srgb 0.73094 0.8025 1 / 1), color(srgb 0.73019 0.80198 1 / 1), color(srgb 0.72944 0.80146 1 / 1), color(srgb 0.7287 0.80094 1 / 1), color(srgb 0.72796 0.80043 1 / 1), color(srgb 0.72723 0.79993 1 / 1), color(srgb 0.72651 0.79943 1 / 1), color(srgb 0.72579 0.79893 1 / 1), color(srgb 0.72508 0.79844 1 / 1), color(srgb 0.72437 0.79795 1 / 1), color(srgb 0.72367 0.79746 1 / 1), color(srgb 0.72297 0.79698 1 / 1), color(srgb 0.72228 0.79651 1 / 1), color(srgb 0.7216 0.79603 1 / 1), color(srgb 0.72092 0.79557 1 / 1), color(srgb 0.72025 0.7951 1 / 1), color(srgb 0.71958 0.79464 1 / 1), color(srgb 0.71892 0.79418 1 / 1), color(srgb 0.71826 0.79373 1 / 1), color(srgb 0.71761 0.79328 1 / 1), color(srgb 0.71697 0.79284 1 / 1), color(srgb 0.71635 0.7924 1 / 1), color(srgb 0.71574 0.79196 1 / 1), color(srgb 0.71514 0.79153 1 / 1), color(srgb 0.71454 0.7911 1 / 1), color(srgb 0.71394 0.79068 1 / 1), color(srgb 0.71335 0.79025 1 / 1), color(srgb 0.71276 0.78984 1 / 1), color(srgb 0.71218 0.78942 1 / 1), color(srgb 0.7116 0.78901 1 / 1), color(srgb 0.71103 0.7886 1 / 1), color(srgb 0.71046 0.78819 1 / 1), color(srgb 0.7099 0.78779 1 / 1), color(srgb 0.70933 0.78739 1 / 1)]\n
"},{"location":"temperature/#ohno-2013","title":"Ohno 2013","text":"

The Ohno 2013 CCT algorithm is registered in Color by default

This is an approach researched by Yoshi Ohno and aims to provide better accuracy. It uses a look up table similar to the Roberson method and employs a combined approach of a triangular solver and a parabolic solver. This can lead to high accuracy if the table is large enough.

Additionally, an \"automatic expansion\" technique can be used that starts with a small table and expands the table on smaller intervals based on the first solution. This can allow for a high accuracy without having to keep a large table in memory.

For good accuracy throughout the range of 1000K - 100000K, as ColorAide supports, a very large table would be needed. If the \"automatic expansion\" technique was used, without caching the data which would cause the table to balloon in memory, the process is much slower.

To mitigate the downside of storing a massive table in memory and to reduce the performance issues when using \"automatic expansion\", ColorAide uses a moderately sized table and creates a spline to interpolate points in between. The expansion technique is then used to get close to the target using the spline as the data table. Once points sufficiently close are found via the automatic expansion, more accurate values are calculated at those locations and are used in the triangular and parabolic solver. This allows us to use a smaller table while mitigating the performance issues associated with the expansion technique, all while maintaining good accuracy. The technique is still slower than the Roberson approach, but it is a more accurate approach.

>>> color = Color.blackbody('srgb-linear', 5000, duv=0.02, method='ohno-2013')\n>>> color\ncolor(srgb-linear 0.94166 1 0.49106 / 1)\n>>> color.cct()\n[5000.015474747112, 0.019993847505661237]\n

ColorAide exposes some of the knobs to control the automatic expansion.

Parameter Description start Used to control the starting range for the search. Default is 1000. For accuracy, the start should not be set lower than 1000. end Used to control the ending range for the search. Default is 100000. For accuracy, the end should not be set higher than 100000. samples Number of sample points to use on each iteration of \"automatic expansion\". The default is 10. iterations Number of iterations to perform when converging close to the temperature. The default is 6 as experimentation seemed indicate it yields the best results with a sample size of 10 and a range of 1000K - 100000K. exact Controls whether the spline approximation is used. When set to True, all values are directly calculated, bypassing the spline. Calculations will be slower when enabled. The default is False and will utilize the spline providing a performance boost.

By default, the entire range of 1000K to 100000K is explored when resolving CCT, but a smaller range can be used. This can be useful if you have an idea of the range, but not the specific value. Using a smaller range may allow for less iterations to achieve the same or better accuracy.

>>> Color('orange').cct(method='ohno-2013')\n[2424.1146637385255, 0.008069417642630583]\n>>> Color('orange').cct(start=2000, end=3000, iterations=3, method='ohno-2013')\n[2424.117026379762, 0.008069404687598869]\n

If a more pure approach is desired, exact can be used to directly use the \"automatic expansion\" without using the spline. The values for the table will be explicitly calculated on the fly. Performance will be affected with minimal to no increase in accuracy.

>>> Color.blackbody('srgb-linear', 5000, duv=0.02).cct(method='ohno-2013')\n[4999.987927661827, 0.020006162744118677]\n>>> Color.blackbody('srgb-linear', 5000, duv=0.02).cct(method='ohno-2013', exact=True)\n[4999.987927661827, 0.020006162744118677]\n

Lastly, the Ohno 2013 method can be used with other CMFs if desired. To do this, the plugin must be instantiated with different CMFs. The plugin supports a few initialization parameters to controls this.

Parameters Description cmfs Valid CMFs at a resolution greater than or equal to 1nm. white A white point as xy chromaticity coordinates. planck_step This controls the resolution at which the wavelengths in the CMFs are used to calculate the points along the Planckian locus. 5 (5nm) is used as the default as it provides decent performance vs accuracy.

To use an different CMFs, such as the CIE 10\u02da Standard Observer, we can add the plugin with our desired configuration, overwriting the defaults.

>>> from coloraide import cmfs\n>>> from coloraide import cat\n>>> from coloraide.temperature.ohno_2013 import Ohno2013\n>>> class Custom(Color):\n...     ...\n... \n>>> Custom.register(\n...     Ohno2013(cmfs.CIE_1964_10DEG, cat.WHITES['10deg']['D65']),\n...     overwrite=True\n... )\n>>> Steps([Custom.blackbody('srgb-linear', t, method='ohno-2013') for t in range(1000, 15000, 50)])\n[color(srgb-linear 1 0.04002 0 / 1), color(srgb-linear 1 0.05154 0 / 1), color(srgb-linear 1 0.06305 0 / 1), color(srgb-linear 1 0.07453 0 / 1), color(srgb-linear 1 0.08593 0 / 1), color(srgb-linear 1 0.09724 0 / 1), color(srgb-linear 1 0.10842 0 / 1), color(srgb-linear 1 0.11944 0 / 1), color(srgb-linear 1 0.1303 0 / 1), color(srgb-linear 1 0.14097 0 / 1), color(srgb-linear 1 0.15144 0 / 1), color(srgb-linear 1 0.16171 0 / 1), color(srgb-linear 1 0.17175 0 / 1), color(srgb-linear 1 0.18157 0 / 1), color(srgb-linear 1 0.19116 0 / 1), color(srgb-linear 1 0.20052 0 / 1), color(srgb-linear 1 0.20965 0 / 1), color(srgb-linear 1 0.21854 0 / 1), color(srgb-linear 1 0.22719 0 / 1), color(srgb-linear 1 0.2371 0.00195 / 1), color(srgb-linear 1 0.2484 0.00608 / 1), color(srgb-linear 1 0.25963 0.01052 / 1), color(srgb-linear 1 0.27081 0.01528 / 1), color(srgb-linear 1 0.28192 0.02035 / 1), color(srgb-linear 1 0.29296 0.02572 / 1), color(srgb-linear 1 0.30393 0.03139 / 1), color(srgb-linear 1 0.31483 0.03735 / 1), color(srgb-linear 1 0.32565 0.04361 / 1), color(srgb-linear 1 0.33639 0.05015 / 1), color(srgb-linear 1 0.34706 0.05696 / 1), color(srgb-linear 1 0.35764 0.06405 / 1), color(srgb-linear 1 0.36814 0.07141 / 1), color(srgb-linear 1 0.37856 0.07902 / 1), color(srgb-linear 1 0.3889 0.08688 / 1), color(srgb-linear 1 0.39915 0.09499 / 1), color(srgb-linear 1 0.40932 0.10333 / 1), color(srgb-linear 1 0.4194 0.1119 / 1), color(srgb-linear 1 0.4294 0.1207 / 1), color(srgb-linear 1 0.43931 0.12971 / 1), color(srgb-linear 1 0.44913 0.13893 / 1), color(srgb-linear 1 0.45886 0.14835 / 1), color(srgb-linear 1 0.46851 0.15796 / 1), color(srgb-linear 1 0.47807 0.16776 / 1), color(srgb-linear 1 0.48754 0.17774 / 1), color(srgb-linear 1 0.49693 0.1879 / 1), color(srgb-linear 1 0.50622 0.19821 / 1), color(srgb-linear 1 0.51543 0.20869 / 1), color(srgb-linear 1 0.52455 0.21932 / 1), color(srgb-linear 1 0.53358 0.23009 / 1), color(srgb-linear 1 0.54253 0.241 / 1), color(srgb-linear 1 0.55139 0.25204 / 1), color(srgb-linear 1 0.56016 0.26321 / 1), color(srgb-linear 1 0.56885 0.2745 / 1), color(srgb-linear 1 0.57745 0.2859 / 1), color(srgb-linear 1 0.58597 0.29741 / 1), color(srgb-linear 1 0.5944 0.30903 / 1), color(srgb-linear 1 0.60274 0.32073 / 1), color(srgb-linear 1 0.611 0.33253 / 1), color(srgb-linear 1 0.61918 0.34442 / 1), color(srgb-linear 1 0.62728 0.35638 / 1), color(srgb-linear 1 0.63529 0.36842 / 1), color(srgb-linear 1 0.64322 0.38053 / 1), color(srgb-linear 1 0.65107 0.3927 / 1), color(srgb-linear 1 0.65884 0.40493 / 1), color(srgb-linear 1 0.66652 0.41722 / 1), color(srgb-linear 1 0.67413 0.42956 / 1), color(srgb-linear 1 0.68166 0.44195 / 1), color(srgb-linear 1 0.68911 0.45438 / 1), color(srgb-linear 1 0.69648 0.46684 / 1), color(srgb-linear 1 0.70377 0.47934 / 1), color(srgb-linear 1 0.71099 0.49187 / 1), color(srgb-linear 1 0.71813 0.50443 / 1), color(srgb-linear 1 0.72519 0.51701 / 1), color(srgb-linear 1 0.73218 0.52961 / 1), color(srgb-linear 1 0.7391 0.54222 / 1), color(srgb-linear 1 0.74594 0.55485 / 1), color(srgb-linear 1 0.75271 0.56748 / 1), color(srgb-linear 1 0.75941 0.58012 / 1), color(srgb-linear 1 0.76604 0.59277 / 1), color(srgb-linear 1 0.77259 0.60541 / 1), color(srgb-linear 1 0.77908 0.61805 / 1), color(srgb-linear 1 0.78549 0.63068 / 1), color(srgb-linear 1 0.79184 0.64331 / 1), color(srgb-linear 1 0.79812 0.65592 / 1), color(srgb-linear 1 0.80433 0.66853 / 1), color(srgb-linear 1 0.81047 0.68111 / 1), color(srgb-linear 1 0.81655 0.69368 / 1), color(srgb-linear 1 0.82256 0.70623 / 1), color(srgb-linear 1 0.82851 0.71875 / 1), color(srgb-linear 1 0.8344 0.73125 / 1), color(srgb-linear 1 0.84022 0.74373 / 1), color(srgb-linear 1 0.84598 0.75617 / 1), color(srgb-linear 1 0.85167 0.76859 / 1), color(srgb-linear 1 0.85731 0.78097 / 1), color(srgb-linear 1 0.86288 0.79332 / 1), color(srgb-linear 1 0.8684 0.80564 / 1), color(srgb-linear 1 0.87385 0.81792 / 1), color(srgb-linear 1 0.87925 0.83016 / 1), color(srgb-linear 1 0.88459 0.84236 / 1), color(srgb-linear 1 0.88987 0.85452 / 1), color(srgb-linear 1 0.89509 0.86664 / 1), color(srgb-linear 1 0.90026 0.87872 / 1), color(srgb-linear 1 0.90538 0.89075 / 1), color(srgb-linear 1 0.91044 0.90273 / 1), color(srgb-linear 1 0.91544 0.91467 / 1), color(srgb-linear 1 0.92039 0.92656 / 1), color(srgb-linear 1 0.92529 0.93841 / 1), color(srgb-linear 1 0.93014 0.9502 / 1), color(srgb-linear 1 0.93493 0.96195 / 1), color(srgb-linear 1 0.93967 0.97364 / 1), color(srgb-linear 1 0.94437 0.98528 / 1), color(srgb-linear 1 0.94901 0.99687 / 1), color(srgb-linear 0.99167 0.94566 1 / 1), color(srgb-linear 0.9805 0.93947 1 / 1), color(srgb-linear 0.96964 0.93342 1 / 1), color(srgb-linear 0.95906 0.92751 1 / 1), color(srgb-linear 0.94877 0.92173 1 / 1), color(srgb-linear 0.93874 0.91607 1 / 1), color(srgb-linear 0.92897 0.91054 1 / 1), color(srgb-linear 0.91944 0.90513 1 / 1), color(srgb-linear 0.91016 0.89984 1 / 1), color(srgb-linear 0.90111 0.89465 1 / 1), color(srgb-linear 0.89229 0.88958 1 / 1), color(srgb-linear 0.88368 0.88461 1 / 1), color(srgb-linear 0.87528 0.87975 1 / 1), color(srgb-linear 0.86709 0.87499 1 / 1), color(srgb-linear 0.85909 0.87032 1 / 1), color(srgb-linear 0.85127 0.86574 1 / 1), color(srgb-linear 0.84364 0.86126 1 / 1), color(srgb-linear 0.83619 0.85687 1 / 1), color(srgb-linear 0.82891 0.85256 1 / 1), color(srgb-linear 0.8218 0.84833 1 / 1), color(srgb-linear 0.81484 0.84419 1 / 1), color(srgb-linear 0.80804 0.84013 1 / 1), color(srgb-linear 0.8014 0.83614 1 / 1), color(srgb-linear 0.7949 0.83223 1 / 1), color(srgb-linear 0.78854 0.82839 1 / 1), color(srgb-linear 0.78231 0.82463 1 / 1), color(srgb-linear 0.77622 0.82093 1 / 1), color(srgb-linear 0.77027 0.8173 1 / 1), color(srgb-linear 0.76443 0.81374 1 / 1), color(srgb-linear 0.75872 0.81024 1 / 1), color(srgb-linear 0.75313 0.8068 1 / 1), color(srgb-linear 0.74765 0.80342 1 / 1), color(srgb-linear 0.74228 0.80011 1 / 1), color(srgb-linear 0.73702 0.79685 1 / 1), color(srgb-linear 0.73187 0.79364 1 / 1), color(srgb-linear 0.72682 0.79049 1 / 1), color(srgb-linear 0.72187 0.7874 1 / 1), color(srgb-linear 0.71702 0.78436 1 / 1), color(srgb-linear 0.71226 0.78137 1 / 1), color(srgb-linear 0.70759 0.77843 1 / 1), color(srgb-linear 0.70301 0.77553 1 / 1), color(srgb-linear 0.69852 0.77269 1 / 1), color(srgb-linear 0.69412 0.76989 1 / 1), color(srgb-linear 0.68979 0.76714 1 / 1), color(srgb-linear 0.68555 0.76443 1 / 1), color(srgb-linear 0.68138 0.76176 1 / 1), color(srgb-linear 0.67729 0.75914 1 / 1), color(srgb-linear 0.67328 0.75656 1 / 1), color(srgb-linear 0.66934 0.75402 1 / 1), color(srgb-linear 0.66547 0.75152 1 / 1), color(srgb-linear 0.66166 0.74906 1 / 1), color(srgb-linear 0.65793 0.74663 1 / 1), color(srgb-linear 0.65426 0.74424 1 / 1), color(srgb-linear 0.65065 0.74189 1 / 1), color(srgb-linear 0.64711 0.73958 1 / 1), color(srgb-linear 0.64362 0.73729 1 / 1), color(srgb-linear 0.6402 0.73505 1 / 1), color(srgb-linear 0.63683 0.73283 1 / 1), color(srgb-linear 0.63352 0.73065 1 / 1), color(srgb-linear 0.63027 0.7285 1 / 1), color(srgb-linear 0.62707 0.72638 1 / 1), color(srgb-linear 0.62392 0.7243 1 / 1), color(srgb-linear 0.62082 0.72224 1 / 1), color(srgb-linear 0.61778 0.72021 1 / 1), color(srgb-linear 0.61478 0.71821 1 / 1), color(srgb-linear 0.61183 0.71624 1 / 1), color(srgb-linear 0.60893 0.71429 1 / 1), color(srgb-linear 0.60607 0.71238 1 / 1), color(srgb-linear 0.60326 0.71048 1 / 1), color(srgb-linear 0.60049 0.70862 1 / 1), color(srgb-linear 0.59777 0.70678 1 / 1), color(srgb-linear 0.59509 0.70497 1 / 1), color(srgb-linear 0.59245 0.70318 1 / 1), color(srgb-linear 0.58984 0.70141 1 / 1), color(srgb-linear 0.58728 0.69967 1 / 1), color(srgb-linear 0.58476 0.69795 1 / 1), color(srgb-linear 0.58227 0.69625 1 / 1), color(srgb-linear 0.57982 0.69458 1 / 1), color(srgb-linear 0.57741 0.69293 1 / 1), color(srgb-linear 0.57503 0.6913 1 / 1), color(srgb-linear 0.57269 0.68969 1 / 1), color(srgb-linear 0.57038 0.6881 1 / 1), color(srgb-linear 0.56811 0.68653 1 / 1), color(srgb-linear 0.56586 0.68498 1 / 1), color(srgb-linear 0.56365 0.68345 1 / 1), color(srgb-linear 0.56147 0.68194 1 / 1), color(srgb-linear 0.55933 0.68045 1 / 1), color(srgb-linear 0.55721 0.67898 1 / 1), color(srgb-linear 0.55512 0.67752 1 / 1), color(srgb-linear 0.55306 0.67609 1 / 1), color(srgb-linear 0.55102 0.67467 1 / 1), color(srgb-linear 0.54902 0.67327 1 / 1), color(srgb-linear 0.54704 0.67188 1 / 1), color(srgb-linear 0.54509 0.67051 1 / 1), color(srgb-linear 0.54317 0.66916 1 / 1), color(srgb-linear 0.54127 0.66782 1 / 1), color(srgb-linear 0.53939 0.6665 1 / 1), color(srgb-linear 0.53754 0.6652 1 / 1), color(srgb-linear 0.53572 0.66391 1 / 1), color(srgb-linear 0.53392 0.66264 1 / 1), color(srgb-linear 0.53214 0.66138 1 / 1), color(srgb-linear 0.53039 0.66013 1 / 1), color(srgb-linear 0.52866 0.6589 1 / 1), color(srgb-linear 0.52695 0.65769 1 / 1), color(srgb-linear 0.52526 0.65648 1 / 1), color(srgb-linear 0.52359 0.6553 1 / 1), color(srgb-linear 0.52195 0.65412 1 / 1), color(srgb-linear 0.52032 0.65296 1 / 1), color(srgb-linear 0.51871 0.65181 1 / 1), color(srgb-linear 0.51713 0.65067 1 / 1), color(srgb-linear 0.51556 0.64955 1 / 1), color(srgb-linear 0.51402 0.64844 1 / 1), color(srgb-linear 0.51249 0.64734 1 / 1), color(srgb-linear 0.51098 0.64625 1 / 1), color(srgb-linear 0.50949 0.64517 1 / 1), color(srgb-linear 0.50802 0.64411 1 / 1), color(srgb-linear 0.50656 0.64306 1 / 1), color(srgb-linear 0.50512 0.64202 1 / 1), color(srgb-linear 0.5037 0.64099 1 / 1), color(srgb-linear 0.5023 0.63997 1 / 1), color(srgb-linear 0.50091 0.63896 1 / 1), color(srgb-linear 0.49954 0.63796 1 / 1), color(srgb-linear 0.49818 0.63697 1 / 1), color(srgb-linear 0.49684 0.63599 1 / 1), color(srgb-linear 0.49552 0.63503 1 / 1), color(srgb-linear 0.4942 0.63407 1 / 1), color(srgb-linear 0.49291 0.63312 1 / 1), color(srgb-linear 0.49163 0.63219 1 / 1), color(srgb-linear 0.49036 0.63126 1 / 1), color(srgb-linear 0.48911 0.63034 1 / 1), color(srgb-linear 0.48787 0.62943 1 / 1), color(srgb-linear 0.48665 0.62853 1 / 1), color(srgb-linear 0.48544 0.62764 1 / 1), color(srgb-linear 0.48424 0.62675 1 / 1), color(srgb-linear 0.48306 0.62588 1 / 1), color(srgb-linear 0.48188 0.62502 1 / 1), color(srgb-linear 0.48072 0.62416 1 / 1), color(srgb-linear 0.47958 0.62331 1 / 1), color(srgb-linear 0.47844 0.62247 1 / 1), color(srgb-linear 0.47732 0.62164 1 / 1), color(srgb-linear 0.47621 0.62081 1 / 1), color(srgb-linear 0.47511 0.62 1 / 1), color(srgb-linear 0.47403 0.61919 1 / 1), color(srgb-linear 0.47295 0.61839 1 / 1), color(srgb-linear 0.47189 0.6176 1 / 1), color(srgb-linear 0.47083 0.61681 1 / 1), color(srgb-linear 0.46979 0.61603 1 / 1), color(srgb-linear 0.46876 0.61526 1 / 1), color(srgb-linear 0.46774 0.6145 1 / 1), color(srgb-linear 0.46673 0.61374 1 / 1), color(srgb-linear 0.46573 0.61299 1 / 1), color(srgb-linear 0.46474 0.61225 1 / 1), color(srgb-linear 0.46376 0.61151 1 / 1), color(srgb-linear 0.46279 0.61079 1 / 1), color(srgb-linear 0.46182 0.61006 1 / 1), color(srgb-linear 0.46087 0.60935 1 / 1), color(srgb-linear 0.45993 0.60864 1 / 1), color(srgb-linear 0.459 0.60793 1 / 1), color(srgb-linear 0.45808 0.60724 1 / 1), color(srgb-linear 0.45716 0.60655 1 / 1), color(srgb-linear 0.45626 0.60586 1 / 1), color(srgb-linear 0.45536 0.60519 1 / 1), color(srgb-linear 0.45447 0.60451 1 / 1), color(srgb-linear 0.45359 0.60385 1 / 1), color(srgb-linear 0.45272 0.60319 1 / 1), color(srgb-linear 0.45186 0.60253 1 / 1), color(srgb-linear 0.451 0.60188 1 / 1), color(srgb-linear 0.45015 0.60124 1 / 1)]\n
"},{"location":"about/acknowledgments/","title":"Acknowledgments","text":"

All projects gain help and inspiration from somewhere, and we wanted to document the places in which we we gathered knowledge, ideas, and help.

"},{"location":"about/acknowledgments/#projects","title":"Projects","text":""},{"location":"about/acknowledgments/#colorjs","title":"Color.js","text":"

When we began writing ColorAide, we wanted a simple interface to deal with different colors. We also wanted to support CSS colors which a lot of people are familiar with. We had a number of questions about the CSS spec and stumbled on Color.js, a JavaScript library developed and maintained by the co-authors of the CSS spec. We found Color.js and its authors helped clarify a number of confusing points. Additionally, their library did end up heavily inspiring many aspects of our own API as its approach very much aligned with the direction we had already started down.

"},{"location":"about/acknowledgments/#culori","title":"Culori","text":"

The Culori library helped inspire the use of cubic splines as interpolation methods.

"},{"location":"about/acknowledgments/#references","title":"References","text":"

When researching color vision deficiencies, aside from the usual scientific papers, a couple of sites were found to be quite helpful.

  • daltonlens.org was particularly helpful. As it comes from the perspective of an actual Protan, it provided reviews on various algorithm's and explained in great depth some approaches and why they were preferred over others.
  • ixora.io was another useful site that went into great details specifically about the Vi\u00e9not approach.
"},{"location":"about/changelog/","title":"Changelog","text":""},{"location":"about/changelog/#210","title":"2.10","text":"
  • NEW: Declare official support for Python 3.12.
  • NEW: Color.steps and Color.discrete now accept delta_e_args to allow configuring the underlying distance algorithm when using the delta_e option.
  • NEW: CIE Lab, both D50 and D65, are now derived from a CIELab class. CIE LCh, both D50 and D65, are also now derived from a CIELCh class. This makes it easy to determine a CIE Lab or CIE LCh space from other Lab-like spaces.
  • NEW: \u2206E*76, \u2206E*94, \u2206E*00, and \u2206E*cmc all accept a new parameter called space which allows the user to specify a registered Lab color space name (one that is derived from the CIELab class) to use as the distancing color space. This allows a user to use D50 Lab (or any other variant) for distancing if required.
  • FIX: For consistency, \u2206E*94 and \u2206E*cmc now use Lab D65 by default just like \u2206E*76 and \u2206E*00. This fixes an issue where the docs indicated that they use D65, but in actuality they were using D50.
"},{"location":"about/changelog/#291post1","title":"2.9.1.post1","text":"
  • FIX: Fix incorrect changelog mention of recent fix being for HSL instead of HWB.
"},{"location":"about/changelog/#291","title":"2.9.1","text":"
  • FIX: Average should allow controlling powerless be disabled by default for backwards compatibility.
  • FIX: HWB should use the algorithm defined in CSS that allows for round tripping even in the negative lightness direction. Previously we were converting directly from HSV.
"},{"location":"about/changelog/#29","title":"2.9","text":"
  • NEW: Add HWBish mixin class.
  • NEW: Deprecate algebra.no_nan(), algebra.no_nans(), and algebra.is_nan().
  • NEW: When averaging in a cylindrical space, always treat achromatic hues as powerless for better results.
  • NEW: Add experimental support for CSS \"powerless\" hue handling and carrying-forward in interpolation, both disabled by default.
  • FIX: Fix RLAB conversion.
  • FIX: Fix clipping of hues.
  • ENHANCE: Tweaks to some matrix calculations.
  • ENHANCE: Various performance related tweaks.
"},{"location":"about/changelog/#28","title":"2.8","text":"
  • NEW: Add Cubehelix color space.
  • NEW: When precision is set to -1 for string output, double precision (17) will be assumed.
  • ENHANCE: More robust and generally better matrix inverse. Related inverse matrices have been regenerated for consistency.
"},{"location":"about/changelog/#272","title":"2.7.2","text":"
  • FIX: More accurate easing logic.
"},{"location":"about/changelog/#271","title":"2.7.1","text":"
  • FIX: Fix issue where harmony would convert some colors to cylindrical spaces and not properly consider order of channels.
  • FIX: XYB, while Lab like in its default configuration, has such a large disparity in the non-lightness components that the ranges for them should not be the same when using percentages.
  • FIX: Lab like space mixins should not try and order a and b like coordinates when calling indexes(), but should return them in there current order with lightness first. The meaning of these components can be different enough for a given color space to make normalizing their ordered configuration meaningless and alter inherit hue direction when processing for harmony.
"},{"location":"about/changelog/#27","title":"2.7","text":"
  • NEW: Add new RYB color space.
  • NEW: Add Regular mixin class for normal, 3 channel color spaces (sRGB, CMY, RYB, etc.).
  • NEW: harmony() can now accept and transform Labish and Regular color spaces to cylindrical spaces.
"},{"location":"about/changelog/#26","title":"2.6","text":"
  • NEW: Add padding parameter to limit color scales when interpolating.
"},{"location":"about/changelog/#25","title":"2.5","text":"
  • NEW: Add new discrete() function that creates a discrete interpolation object.
  • NEW: Deprecate coloraide.algebra.apply function in favor of new vectorize functions.
  • FIX: Fix small typing issue.
  • FIX: Tweaks to Oklab 64 bit matrix precision.
  • FIX: Fix prismatic and cmyk achromatic check logic.
  • FIX: Ensure IPT uses the exact white point as documented in the paper.
  • FIX: Fix various corner cases of algebraic functions and implement some performance improvements.
"},{"location":"about/changelog/#24","title":"2.4","text":"
  • NEW: Add Rec. 709 RGB color space.
  • NEW: Add the 1960 UCS color space.
  • NEW: Add correlated color temperature support with new cct() and blackbody() API.
  • NEW: Add support for Robertson 1968 and Ohno 2013 CCT plugins.
  • NEW: Add support for determining if a color is in the Pointer Gamut and provide a way to clamp a color to the gamut.
  • NEW: Include CMFS: CIE 1931 2 Degree Standard Observer, CIE 1964 10 Degree Standard Observer, CIE 2015 2 Degree Standard Observer, and CIE 2015 10 Degree Standard Observer.
  • NEW: Add split_chromaticity() method which will split a color into its chromaticity and luminance parts.
  • NEW: Add chromaticity() which will create a new color from a given set of chromaticity coordinates.
  • NEW: Relax chromatic_adaptation() type requirement of white point chromaticity inputs.
  • NEW: luminance(), xy(), and uv() all now accept an optional white point via the white parameter to control the white point in which the returned values are relative to. luminance() still defaults to D65 but will use the current color's white point, like xy() and uv() if white is set to None.
  • NEW: white() now accepts a positional parameter allowing it to output the white point of the current color as various chromaticity coordinates in addition to the default XYZ coordinates.
  • FIX: Fix case where deregistering all plugins with * was not deregistering Filter plugins.
"},{"location":"about/changelog/#23","title":"2.3","text":"
  • NEW ACEScc will now resolve undefined color channels (non-alpha) with a non-zero default that represents black for consistency with other ACES color spaces.
  • ENHANCE: Streamline averaging algorithm to increase performance.
  • FIX: Ensure that HCT consistently clamps negative lightness and chroma to zero.
"},{"location":"about/changelog/#222","title":"2.2.2","text":"
  • FIX: Improve HCT round trip conversion speed and improve conversion in some weak areas.
"},{"location":"about/changelog/#221","title":"2.2.1","text":"
  • FIX: Averaging of a channel set with only undefined values should return an undefined value.
  • FIX: Averaging should be done in linear light by default for a sane default. Default is now srgb-linear.
"},{"location":"about/changelog/#22","title":"2.2","text":"
  • NEW: Add XYB color space.
  • ENHANCE: More efficient averaging.
  • FIX: Fix issue where if all colors have the same channel undefined that a divide by zero can occur.
"},{"location":"about/changelog/#21","title":"2.1","text":"
  • NEW: Add new color averaging method.
  • FIX: Interpolation should not modify any input colors.
"},{"location":"about/changelog/#202","title":"2.0.2","text":"
  • FIX: Consistent normalization of HWB hue.
  • FIX: Consistent normalization of color in harmony monochromatic.
"},{"location":"about/changelog/#201","title":"2.0.1","text":"
  • FIX: Incorrect result when interpolating from a cylindrical space to a rectangular space and using out_space.
"},{"location":"about/changelog/#20","title":"2.0","text":"
  • BREAK: interpolate, steps, mix, filter, compose, and harmony will no longer base the output color on the first input color. Colors will be evaluated in the specified color space and be output in that space unless out_space is used to specify a specific output color space. For migration, specify the desired out_space if the working space does not match the desired output.

  • BREAK: Achromatic and undefined color channel handling has been rewritten. Color space objects no longer utilize the normalize() or achromatic_hue() method and instead now use a new is_achromatic() and resolve_channel() methods.

  • NEW: Expose the new Color.is_achromatic() method to tell if colors, even non-cylindrical colors, are achromatic or reasonably close to achromatic.

  • NEW: Color channel definitions can specify a non-zero default for an undefined channel. Use resolve_channel() for more advanced handling.

  • NEW: CAM16, CAM16 UCS, CAM16 SCD, CAM15 LCD, CAM16 JMh, HCT, Jzazbz, JzCzhz, and IPT all currently require a dynamic approach to detect achromatic colors. Undefined LCh chroma and hue channels and Lab a and b channels can now resolve to non-zero values when undefined for better achromatic interpolation.

  • NEW: ACEScct will now resolve undefined color channels (non-alpha) with a non-zero default that represents black as zero is actually out of gamut for that space.

  • NEW: filter, compose, and harmony all now support the out_space parameter.

  • NEW: All <space>ish mixin classes now give access to normalized names and indexes as names() and indexes() opposed to <space>ish_names() etc. Old methods are still available but are deprecated.

  • NEW: All RGB, HSL, and HSV color spaces are now created with a respective RGBish, HSLish, and HSVish mixin class.

  • NEW: Separable blend modes will now be evaluated in whatever RGB-ish color space is provided.

  • NEW: compose will throw an error if a non-RGB-ish color space is provided.

  • NEW: Color.normalize() added a new nans parameter that when set to False will prevent achromatic hue normalization and will just force all channels to be defined.

  • NEW: Color.coords() and Color.alpha(), which used to be available during the alpha/beta period have been re-added. coords() accesses just the color channels (no alpha channel) while alpha() gets the alpha channel.

  • NEW: Coordinate access functions: get(), set(), coords(), and alpha() functions now have a nans parameter that when set to False will ensure the component(s) is returned as a real number instead of NaN. Set operations only apply this when passing the current value to a callback for relative modification.

  • NEW: A norm parameter is now added to convert and update. When set to False, it will prevent achromatic normalization of hues during conversion. If no conversion is needed, the color is returned as is.

  • NEW: ColorAide used to gamut map colors such as HSL, HSV, and HWB when interpolating into those spaces. This is no longer done. It is possible to gamut map wider gamuts with these color spaces, so it will be up to the user to apply gamut mapping when it is determined they need it.

  • NEW: EXTENDED_RANGE is no longer needed and is removed from current color space classes.

  • NEW: Improved accuracy for Oklab, OkLCh, Okhsl, and Okhsv.

  • NEW: New \"continuous\" interpolation method.

  • FIX: Fix aliases in IPT and IgPgTg.

  • FIX: Fix some conversion issues with CAM16 based color spaces that was caused due to bad achromatic handling.

"},{"location":"about/changelog/#182","title":"1.8.2","text":"
  • FIX: Fix some exception messages.
"},{"location":"about/changelog/#181","title":"1.8.1","text":"
  • FIX: Ensure Judd-Vos correction is applied to linear RGB to LMS conversion for CVD.
  • FIX: Fix outdated API information in docs.
"},{"location":"about/changelog/#180","title":"1.8.0","text":"
  • NEW: Modern sRGB, HSL, and HWB should allow mixed percentage and numbers. HSL and HWB percentages in the hsl() and hwb() formats respectively will resolve to numbers in the range [0, 100]. These changes reflect the latest changes in the CSS Level 4 Color spec.
  • NEW: HSL and HWB can serialize to a modern syntax that does not use percentages, but the default still uses percentages.
  • NEW: Rework CSS parsing for better performance.
  • FIX: Handle some parsing corner cases that are handled by browsers, but not by ColorAide. For example, color(srgb 1-0.5.4) should parse as color(srgb 1 -0.5 0.4).
  • FIX: Ensure that COLOR_FORMAT is respected.
"},{"location":"about/changelog/#171","title":"1.7.1","text":"
  • FIX: Ensure CAM16 spaces mirrors positive and negative percentages for a and b components.
  • FIX: Since the CAM16 JMh model can not predict achromatic colors with negative lightness and, more importantly, negative lightness is not useful, limit the lower end of lightness in CAM16 spaces to zero.
  • FIX: When a CAM16 JMh (or HCT) color's chroma, when not discounting illuminance, has chroma drop below the actual ideal achromatic chroma threshold, just use the ideal chroma to ensure better conversion back to XYZ.
  • FIX: Jzazbz and JzCzhz model can never translate a color with a negative lightness, so just clamp negative lightness while in Jzazbz and JzCzhz.
  • FIX: Fix a math error in CAM16.
  • FIX: Fix CAM16 JMh M limit which was too low.
  • FIX: IPT was set to \"bound\" when it should have an unbounded gamut.
  • FIX: When both comma and none are enabled it could make undefined alpha values show up as none in legacy CSS format.
  • FIX: Sane handling of inverse lightness in DIN99o.
"},{"location":"about/changelog/#17","title":"1.7","text":"
  • NEW: Add support for CAM16 Jab and JMh: cam16 and cam16-jmh respectively.
  • NEW: Add support for CAM16 UCS (Jab forms): cam16-ucs, cam16-scd, and cam16-lcd.
  • NEW: Add support for the HCT color space (hct) which combines the colorfulness and hue from CAM16 JMh and the lightness from CIELab.
  • NEW: Gamut mapping classes derived from fit_lch_chroma can set DE_OPTIONS to pass \u2206E parameters.
  • NEW: While rare, some cylindrical color spaces have an algorithm such that achromatic colors convert best with a very specific hue. Internally, this is now handled during conversions, but there can be reasons where knowing the hue can be useful such as plotting. Cylindrical spaces now expose a method called achromatic_hue() which will return this specific hue if needed.
  • FIX: Fix rec2100-hlg transform.
  • FIX: Some color transformation improvements.
  • FIX: Relax some achromatic detection logic for sRGB cylindrical models. Improves achromatic hue detection results when converting to and from various non-sRGB color spaces.
"},{"location":"about/changelog/#16","title":"1.6","text":"
  • NEW: Add rec2100-hlg color space.
  • BREAKING: rec2100pq should have been named rec2100-pq for consistency. It has been renamed to rec2100-pq and serializes with the CSS ID of --rec2100-pq. This is likely to have little impact on most users.
"},{"location":"about/changelog/#15","title":"1.5","text":"
  • NEW: Formally add support for Python 3.11.
  • NEW: Add support for custom domains when interpolating.
  • NEW: set() can now take a dictionary of channels and values and set multiple channels at once.
  • NEW: get() can now take a list of channels and will return a list of those channel values.
  • ENHANCE: Simplify some type annotation syntax.
  • ENHANCE: Some minor performance enhancements.
  • FIX: Fix OkLCh CSS parsing.
"},{"location":"about/changelog/#14","title":"1.4","text":"
  • NEW: A color space can now declare its dynamic range. By default, spaces are assumed to be SDR, but can declare themselves as HDR, or something else. This allows ColorAide to make decisions based on a color's dynamic range.
  • NEW: Add channel aliases for IPT and IPT-like color spaces (IgPgTg and ICtCp): intensity, protan, and tritan.
  • FIX: The ICtCp and oRGB space would return the Lab-ish equivalents for a and b in reverse order if calling Labish.labish_names. This was not actually called anywhere in the code, but is now fixed for any future cases that may require calling it.
  • FIX: Undefined channels should be ignored when clipping a color.
  • FIX: Do not apply SDR shortcuts in gamut mapping when fitting in a non-SDR color gamut, such as HDR.
"},{"location":"about/changelog/#13","title":"1.3","text":"
  • ENHANCE: Color vision deficiency filters can now be instantiated with different default methods for severe and anomalous cases.
  • FIX: Fix premultiplication handling when using compose.
"},{"location":"about/changelog/#12","title":"1.2","text":"
  • NEW: Add new monotone interpolation method.
  • ENHANCE: Better extrapolation past end of spline.
  • FIX: Small speed up in natural spline calculation.
  • FIX: Fix import that should have been relative, not absolute.
"},{"location":"about/changelog/#11","title":"1.1","text":"
  • NEW: Slight refactor of interpolation plugin so that common code does not need to be duplicated, and the interpolate method no longer needs to accept an easing parameter as the plugin class exposes a new ease method to automatically acquire the proper, specified easing function and apply it.
  • NEW: Functions built upon interpolation can now use a new extrapolate parameter to enable extrapolation if interpolation inputs exceed 0 - 1. point will be passed to Interpolator.interpolate un-clamped if extrapolate is enabled. If a particular interpolation plugin needs to do additional work to handle extrapolation, they can check self.extrapolate to know whether extrapolation is enabled.
  • NEW: Implement and provide the following easing functions as described in the CSS Easing Level 1 spec: cubic_bezier, ease, ease_in, ease_out, and ease_in_out. Also provide a simple linear easing function.
  • New: Add natural and catrom cubic spline options for interpolation. The catrom (Catmull-Rom) spline requires the plugin to be registered in order to use it.
  • FIX: Due to floating point math, B-spline could sometimes return an interpolation of fully opaque colors with an imperceptible amount of transparency. If alpha is very close (1e-6) to being opaque, just round it to opaque.
  • FIX: An easing function's output should not be clamped, only the input, and that only needs to occur on the the outer range of an entire interpolation.
"},{"location":"about/changelog/#10","title":"1.0","text":"

Stable Release!

Checkout migration guide if you were an early adopter.

  • NEW: Bezier interpolation dropped for B-spline which provides much better interpolation.
  • NEW: All new interpolation methods now supports hue fix-ups: shorter, longer, increasing, decreasing, and specified.
  • NEW: Interpolation is now exposed as a plugin to allow for expansion.
  • FIX: Fixed an issue related to premultiplication and undefined alpha channels.
"},{"location":"about/changelog/#10rc1","title":"1.0rc1","text":"

Plugin Refactor

For more flexibility there was one final rework of plugins. Registering requires all plugins to be instantiated before being passed into Color.register, but this allows a user redefine some defaults of certain plugins.

coloraide.ColorAll was moved to coloraide.everythng.ColorAll to avoid allocating plugins when they are not desired.

In the process, we also renamed a number of plugin classes for consistency and predictability, details found below.

  • NEW: Updated some class names for consistency and predictability. XyY \u2192 xyY, Din99o \u2192 DIN99o, SRGB \u2192 sRGB, and ORGB \u2192 oRGB.

    Lastly, LCh should be the default casing convention. This convention will be followed unless a spec mentions otherwise. Changes: Lch \u2192 LCh, LchD65 \u2192 LChD65, Oklch \u2192 OkLCh, Lchuv \u2192 LChuv, Lch99o \u2192 LCh99o, LchChroma \u2192 LChChroma, OklchChroma \u2192 OkLChChroma, and Lchish \u2192 LChish.

  • NEW: Updated migration guide with recent plugin changes.

  • NEW: coloraide.ColorAll renamed and moved to coloraide.everything.ColorAll. This prevents unnecessary inclusion and allocation of objects that are not desired.
  • NEW: Default Color object now only registers bradford CAT by default, all others must be registered separately, or coloraide.everything.Color could be used.
  • NEW: All plugin classes must be instantiated when being registered. This allows some plugins to be instantiated with different defaults. This allows some plugins to be configured with different defaults.

    # Before change:\nColor.register([Plugin1, Plugin2])\n\n# After change:\nColor.register([Plugin1(), Plugin2(optional_parm=True)])\n
  • FIX: Negative luminance is now clamped during contrast calculations.

"},{"location":"about/changelog/#10b3","title":"1.0b3","text":"
  • FIX: Fixed the bad CAT16 matrix for chromatic adaptation.
  • FIX: Small fix related to how CAT plugin classes are defined for better abstraction.
  • FIX: Restrict optional keywords in Color.register() and Color.deregister() to keyword only parameters.
"},{"location":"about/changelog/#10b2","title":"1.0b2","text":"

Breaking Changes

1.0b2 only introduces one more last breaking change that was forgotten in 1.0b1.

  • BREAK: Remove filters parameter on new class instantiation.
  • NEW: Added new migration guide to the documentation to help early adopters move to the 1.0 release.
  • NEW: Added HPLuv space described in the HSLuv spec.
  • NEW: Added new color spaces: ACES 2065-1, ACEScg, ACEScc, and ACEScct.
  • NEW: Contrast is now exposed as a plugin to allow for future expansion of approaches. While there is currently only one approach, methods can be selected via the method attribute.
  • NEW: Add new random method for generating a random color for a given color space.
"},{"location":"about/changelog/#10b1","title":"1.0b1","text":"

Breaking Changes

1.0b1 introduces a number of breaking changes. As we are very close to releasing the first stable release, we've taken opportunity to address any issues related to speed and usability. While this is unfortunate for early adopters, we feel that in the long run that these changes will make ColorAide a better library. We've also added new a new Bezier interpolation method and added many more color spaces!

  • BREAK: The coloraide.Color object now only registers a subset of the available color spaces and \u2206E algorithms in order to create a lighter default color object. coloraide.ColorAll has been provided for a quick way to get access to all available color spaces and plugins. Generally, it is recommend to subclass Color and register just what is desired.

  • BREAK: Reworked interpolation:

    • interpolate and steps functions are now @classmethods. 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):

    {\"space\": \"name\", \"coords\": [0, 0, 0], \"alpha\": 1}\n

    This allows for quicker processing and less complexity dealing with channel names and aliases.

  • BREAK: The CSS Level 4 Color spec has accepted our proposed changes to the gamut mapping algorithm. With this change, the oklch-chroma gamut mapping algorithm is now compliant with the CSS spec, and css-color-4 is no longer needed. If you were experimenting with css-color-4, please use oklch-chroma instead. The algorithm is faster and does not have the color banding issue that css-color-4 had, and it is now exactly the same as the CSS spec.

  • BREAK: New breaking change. Refactor of Space plugins. Space plugins are no longer instantiated which cuts down on overhead lending to better performance. BOUNDS and CHANNEL_NAMES attributes were combined into one attribute called CHANNELS which serves the same purpose as the former attributes. Space plugins also no longer need to define channel property accessors as those are handled through CHANNELS in a more generic way. This is a breaking change for any custom plugins.

    Additionally, the Space plugin's null_adjust method has been renamed as normalize matching its functionality and usage in regards to the Color object. It no longer accepts color coordinates and alpha channel coordinates separately, but will receive them as a single list and return them as such.

  • BREAK: Color's fit and clip methods now perform the operation in place, modifying the current color directly. The in_place parameter has been removed. To create a new color when performing these actions, simply clone the color first: color.clone().clip().

  • BREAK: Remove deprecated dynamic properties which helps to increase speed by removing overhead on class property access.

  • BREAK: Remove deprecated dynamic properties which helps to increase speed by removing overhead on class property access. Use indexing instead: color['red'] or color[0].

  • BREAK: Remove deprecated coords() method. Use indexing and slices instead: color[:-1].

  • 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 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.

    Non-CSS color spaces will also respect their defined ranges when using percentages in the color() form.

  • NEW: Add silent option to deregister so that if a proper category is specified, and the plugin does not exit, the operation will not throw an error.

  • NEW: Add new color spaces: display-p3-linear, a98-rgb-linear, rec2020-linear, prophoto-rgb-linear, and rec2100pq, hsi, rlab, hunter-lab, xyy, prismatic, orgb, cmy, cmyk, ipt, and igpgtg.

  • NEW: Monochromatic color harmony must also be performed in a cylindrical color space to make achromatic detection easier. This means all color harmonies now must be performed under a cylindrical color space.

  • NEW: Use Lab D65 for \u2206E 2000, \u2206E 76, \u2206E HyAB, Euclidean distance, and LCh D65 for LCh Chroma gamut mapping. Lab D65 is far more commonly used for the aforementioned \u2206E methods. LCh Chroma gamut mapping, which uses \u2206E 2000 needs to use the same D65 white point to avoid wasting conversion time.

  • FIX: Better handling of monochromatic harmonies that are near white or black.

  • FIX: Small fix to steps \u2206E logic.

"},{"location":"about/changelog/#0181","title":"0.18.1","text":"
  • FIX: Fix issue where when generating steps with a max_delta_e, the \u2206E was reduced too much causing additional, unnecessary steps along with longer processing time.
"},{"location":"about/changelog/#0180","title":"0.18.0","text":"
  • NEW: Allow dictionary input to use aliases in the dictionary.
  • FIX: If too many channels are given to a color space via raw data, ensure the operation fails.
  • FIX: Sync up achromatic logic of the Okhsl and Okhsv normalize function with the actual conversion algorithm.
  • FIX: Regression that caused cat16 not to work due to a misnamed variable.
"},{"location":"about/changelog/#0170","title":"0.17.0","text":"

Interpolations Are Now Premultiplied

ColorAide has moved to make premultiplication the default for interpolation methods such as mix, steps, and interpolate. The aim is to provide more accurate interpolation when using transparent colors. In cases where premultiplication is not desired, it can be disabled by setting it to False. There are real reasons to do so as it may be desirous to mimic an old implementation that has always used naive interpolation of transparent colors.

Additionally, in the past, premultiplication was not really documented as it had not been fully tested. Premultiplication is now covered in the documentation.

  • NEW: All mixing/interpolation methods will use premultiply=True by default.
  • NEW: Allow aliases in interpolation's progress mappings.
  • FIX: Fix premultiplication when alpha is undefined.
  • FIX: Fix some potential issues in some matrix math logic.
  • FIX: Piecewise() object didn't default all the non-required parameters to None as documented.
"},{"location":"about/changelog/#0160","title":"0.16.0","text":"

Deprecations

In interest of speed, and due to the overhead inflicted on every class attribute access, we've decided to deprecate dynamic properties. This includes dynamic color properties (e.g. Color.red) and dynamic \u2206E methods (e.g. Color.delta_e_2000()). As far as color channel coordinate access is concerned, we've reworked a faster more useful approach. \u2206E already has a suitable replacement and will be the only approach moving forward.

  1. Use of delta_e_<method> is deprecated. Users should use the already available delta_e(color, method=name) approach when using non-default \u2206E methods.

  2. Color channel access has changed. Dynamic channel properties have been deprecated. Usage of Color.coords() has also been deprecated. All channels can now easily be accessed with indexing. Color.get() and Color.set() have not changed.

    • You can index with numbers: Color[0].
    • You can index with channel names: Color['red'].
    • You can slice to get specific color coordinates: Color[:-1].
    • You can get all coordinates: Color[:] or list(Color).
    • You can even iterate coordinates: [c for c in Color].
    • Indexing also supports assignment: Color[0] = 1 or Color[:3] = [1, 1, 1].

Please consider updating usage to utilize the suggested approaches. The aforementioned methods will be removed sometime before the 1.0 release.

  • NEW: Color objects are now indexable and channels can be retrieved using either numbers or strings, e.g., Color[0] or Color['red']. Slicing and assignments via slicing are also supported: Color1[:] = Color2[:].
  • NEW: Color.coords(), dynamic color properties, and dynamic \u2206E methods are all deprecated.
  • NEW: Input method names for distancing, gamut mapping, compositing, and space methods are now case sensitive. There were inconsistencies in some places, so it was opted to make all case sensitive.
  • NEW: The ability to create color harmonies has been added via the new harmony() method. Also, the default color space used to calculate color harmonies can be overridden by the class property HARMONY.
  • NEW: Add new support for filters added via the filter() method. Filters include the W3C Filter Effects Level 1 and color vision deficiency simulation.
  • NEW: Some performance enhancements in conversions.
  • NEW: Chromatic adaptation is now exposed as a plugin. New CAT plugins can be created externally and registered.
  • FIX: Okhsl and Okhsv handling of achromatic values during conversion.
"},{"location":"about/changelog/#0151","title":"0.15.1","text":"
  • FIX: Fix an issue related to matching colors in a buffer at a given offset.
"},{"location":"about/changelog/#0150","title":"0.15.0","text":"

Warning

No changes in the public API have changed, but type annotations have. If you were importing type annotations, you will have to update them.

Also, if any undocumented math related methods were accessed (for plugins or otherwise) they've been moved to coloraide.algebra

  • NEW: A number of performance improvements.
  • NEW: Regenerate all matrices with our own matrix tools so that there is consistency between precision of pre-generated matrices and on-the-fly matrix generation. Reduces some noise in a few color space transforms.
  • NEW: Changes to type annotations. Mutable<type>, where type is either Matrix, Vector, or Array, are simply known as <type>. Types previously specified as <type>, where type is either Matrix, Vector, or Array, are now known as <type>Like. The types are expected to be mutable lists, anything else is noted as \"like\".
  • NEW: All matrix and math utilities have been moved to coloraide.algebra.
  • FIX: Fix rare issue where precision adjustment could fail.
  • FIX: Fix matrix divide logic when dividing a number or vector by a matrix. There are no actual usage of these cases in the code but they were fixed in case they are used in the future.
"},{"location":"about/changelog/#0141","title":"0.14.1","text":"
  • FIX: Fix bug related to parsing strings without full matching.
"},{"location":"about/changelog/#0140","title":"0.14.0","text":"

Note

No changes should break existing color space plugins. Moved objects and references are still also available in old locations, and new functionality is implemented in such a way as to not break existing plugins, but plugins should be updated as sometime before the 1.0 release, such legacy access will be removed.

  • NEW: Faster parsing. Instead of parsing color(space ...) each time it is evaluated for a different color space, parse it generically and then associate it with a given registered color space. If a color spaces wishes to opt out of the color(space ...) input format, the space should set COLOR_FORMAT to False. This means there is no need to call super.match() when overriding Color.match() to ensure support for the color(space ...) format as it will be handled unless COLOR_FORMAT is turned off. DEFAULT_MATCH usage should also be discontinued as it now does nothing.
  • NEW: Other speed optimizations.
  • NEW: All CSS parsing and serialization is now contained in a single module at coloraide.css. This simplifies the current color space classes greatly when it comes to supporting CSS specific formats.
  • NEW: Move our white space mapping to the cat module as it makes more sense there.
  • NEW: GamutBound, GamutUnbound, and associated flags are now contained under coloraide.gamut.bounds.
  • NEW: normalize will also remove masked values to properly adjust the color.
  • FIX: Compositing and blending should not \"fit\" colors before applying, it is only specified that the range should be clamped at the end of blending.
  • FIX: Fix issue where a subclassed Color() object could not recognize the base class or other subclasses.
"},{"location":"about/changelog/#0130","title":"0.13.0","text":"
  • NEW: Add new closest method that takes a list of colors and returns the one that is closet to the calling color object.
  • NEW: CSS color syntax no longer allows for forgiving channels in color(). This means that when a channel other than alpha is omitted, we will no longer treat them as undefined. Instead, the color will simply fail to parse. Raw data channels also must specify all channels.
  • NEW: Clamp lower bounds of chroma at the channel level.
  • NEW: coloraide.spaces.WHITES is now a 2 deep dictionary containing both 2\u02da and 10\u02da observer variants of white points.
  • NEW: Color space plugins now specify WHITE as a tuple with the x and y chromaticity coordinates. This allows a space to specify unknown white points if desired.
  • FIX: Fix longer hue interpolation when \u03b81 - \u03b82 = 0. The spec is wrong in this case, and interpolation should still occur the long way around instead of keeping hue constant.
  • FIX: Reduce redundancy in some CSS parsing patterns.
  • FIX: Minor performance improvements.
  • FIX: Legacy rgb(), rgba(), hsl(), and hsla() comma separated forms in CSS do not support none, only the new space separated forms do.
  • FIX: Ensure py.typed is installed with package so that type annotations work properly.
"},{"location":"about/changelog/#0120","title":"0.12.0","text":"
  • NEW: Add a gamut mapping variant that matches the CSS Color Level 4 spec.
  • FIX: Fix precision rounding issue.
"},{"location":"about/changelog/#0110","title":"0.11.0","text":"

Breaking Changes

  1. Prior to 0.11.0, if you specified a cylindrical space directly, ColorAide would normalize undefined hues the same way that the conversion algorithm did. In the below case, saturation is zero, so the hue was declared undefined.

    >>> Color('hsl(270 0% 50%)')\ncolor(--hsl none 0 0.5 / 1)\n

    We should not have been doing this, and it made some cases of interpolation a bit confusing. It is no longer done as the hues are in fact specified by the user, even if they are powerless in relation to contributing to the rendered color. When a cylindrical color is converted or if a user declares the channel as undefined with none or some other way, then the channel will be declared undefined, because in these cases, they truly are.

    >>> Color('white').convert('hsl')\ncolor(--hsl none 0 1 / 1)\n>>> Color('color(--hsl none 0 0.5)')\ncolor(--hsl none 0 0.5)\n

    If you are working directly in a cylindrical color space and ever wish to force the normalization of color hues as undefined when the color meets the usual requirements as specified by the color space's current rules, just call normalize on the color and it will apply the same logic that occurs during the conversion process.

    >>> Color('hsl(270 0% 50%)').normalize()\ncolor(--hsl none 0 0.5 / 1)\n
    2. If you relied on commas in CSS forms that did not support them, this behavior is no longer allowed. It was thought that CSS may consider allowing comma formats in formats like hwb(), etc., and it was considered, but ultimately the decision was to avoid adding such support. We've updated our input and output support to reflect this. Color spaces can always be subclassed and have this support added back, if desired, but will not be shipped as the default anymore. 3. The D65 form of Luv and LChuv is now the only supported Luv based color spaces by default now. D50 Luv and LChuv have been dropped and luv and lchuv now refers to the D65 version. In most places, the D65 is the most common used white space as most monitors are calibrated for this white point. The only reason CIELab and CIELCh are D50 by default is that CSS requires it. Anyone interested in using Luv with a different white point can easily subclass the current Luv and create a new plugin color space that uses the new white point. 4. Renamed DIN99o LCh identifier to the short name of lch99o.

  • NEW: ColorAide now only ships with the D65 version Luv and LChuv as D65, in most places is the expected white space. Now, the identifier luv and lchuv will refer to the D65 version of the respective color spaces. D50 variants are no longer available by default.
  • NEW: Add the HSLuv color space.
  • NEW: DIN99o LCh identifier was renamed from din99o-lch to lch99o. To use in CSS color() form, use --lch99o.
  • NEW: Refactor chroma reduction/MINDE logic to cut processing time in half. Gamut mapping results remain very similar.
  • NEW: Be more strict with CSS inputs and outputs. hwb(), lab(), lch(), oklab(), and oklch() no longer support comma string formats.
  • NEW: Officially drop Python 3.6 support.
  • FIX: Do not assume user defined, powerless hues as undefined. If they are defined by the user, they should be respected, even if they have no effect on the current color. This helps to ensure interpolations acts in an unsurprising way. If a user manually specifies the channel with none, then it will be considered undefined, or if the color goes through a conversion to a space that cannot pick an appropriate hue, they will also be undefined.
"},{"location":"about/changelog/#0100","title":"0.10.0","text":"
  • NEW: Switch back to using CIELCh for gamut mapping (lch-chroma). There are still some edge cases that make oklch-chroma less desirable.
  • FIX: Fix an issue where when attempting to generate steps some \u2206E distance apart, the maximum step range was not respected and could result in large hangs.
"},{"location":"about/changelog/#090","title":"0.9.0","text":"

Breaking Changes

Custom gamut mapping plugins no longer return coordinates and require the method to update the passed in color.

  • NEW: Improved, faster gamut mapping algorithm.
  • NEW: FIT plugins (gamut mapping) no longer return coordinates but should modify the color passed in.
  • NEW: Expose default interpolation space as a class variable that can be controlled when creating a custom class via class inheritance.
  • NEW: Colors can now directly specify the \u2206E method that is used when interpolating color steps and using max_delta_e via the new delta_e argument. If the delta_e parameter is omitted, the color object's default \u2206E method will be used.
  • NEW: Oklab is now the default interpolation color space.
  • NEW: Interpolation will now avoid fitting colors that are out of gamut unless the color space cannot represent out of gamut colors. Currently, all of the RGB colors (srgb, display-p3, etc.) all support extended ranges, but the HSL, HWB, and HSV color models for srgb (including spaces such as okhsl and okhsv) do not support extended ranges and will still be gamut mapped.
  • FIX: Remove some incorrect code from the gamut mapping algorithm that would shortcut the mapping to reduce chroma to zero.
"},{"location":"about/changelog/#080","title":"0.8.0","text":"

Breaking Changes

The use of xyz as the color space name has been changed in favor of xyz-d65. This better matches the CSS specification. As we are still in a prerelease state, we have not provided any backwards compatibility.

CSS color input strings in the form color(xyz x y z) will continue to be accepted as CSS will allow both the xyz and the xyz-d65 identifier, but output serialization will prefer the color(xyz-d65 x y z) form as using xyz is an alias for xyz-d65.

Again, this breaking change only affects operations where the color space \"name\" is used in the API to specify usage of a specific color space in order to create a color, convert, mutate, interpolate, etc.

Color('red').convert('xyz')      # Bad\nColor('red').convert('xyz-d65')  # Okay\n\nColor('xyz' [0, 0, 0])      # Bad\nColor('xyz-d65' [0, 0, 0])  # Okay\n\nColor('red').interpolate('green', space='xyz')      # Bad\nColor('red').interpolate('green', space='xyz-d65')  # Okay\n\n# No changes to CSS inputs\nColor('color(xyz 0 0 0)')      # Okay\nColor('color(xyz-d65 0 0 0)')  # Okay\n
  • NEW: Add the official CSS syntax oklab() and oklch() for the Oklab and OkLCh color spaces respectively.
  • NEW: Custom fit plugin's fit method now allows additional kwargs in its signature. The API will accept kwargs allowing a custom fit plugin to have configurable parameters. None of the current built-in plugins provide additional parameters, but this is provided in case it is found useful in the future.
  • NEW: XYZ D65 space will now be known as xyz-d65, not xyz. Per the CSS specification, we also ensure XYZ D65 color space serializes as xyz-d65 instead of the alias xyz. CSS input string format will still accept the xyz identifier as this is defined in the CSS specification as an alias for xyz-d65, but when serializing a color to a string, the xyz-d65 will be used as the preferred form.
  • NEW: By default, gamut mapping is done with oklch-chroma which matches the current CSS specification. If desired, the old way (lch-chroma) can manually be specified or set as the default by subclassing Color and setting FIT to lch-chroma.
  • FIX: Ensure the convert method's fit parameter is typed appropriately and is documented correctly.
"},{"location":"about/changelog/#070","title":"0.7.0","text":"
  • NEW: Formally expose srgb-linear as a valid color space.
  • NEW: Distance plugins and gamut mapping plugins now use classmethod instead of staticmethod. This allows for inheritance from other classes and the overriding of plugin options included as class members.
  • NEW: Tweak LCh chroma gamut mapping threshold.
  • FIX: Issue where it is possible, when generating steps, to cause a shift in midpoint of colors if exceeding the maximum steps. Ensure that no stops are injected if injecting a stop between every color would exceed the max steps.
"},{"location":"about/changelog/#060","title":"0.6.0","text":"
  • NEW: Update spaces such that they provide a single conversion point which simplifies color space API and centralizes all conversion logic allowing us to pull chromatic adaptation out of spaces.
  • NEW: color() output format never uses percent when serializing, but will optionally accept percent as input.
  • NEW: Slight refactor of color space, delta E, and gamut mapping plugins. All now specify there name via the property NAME instead of methods space() for color spaces and name() for other plugins.
  • NEW: Restructure source structure by flattening out some directories and better organizing source files. This changes some import paths.
  • NEW: Color spaces do not specify alpha in CHANNEL_NAMES as the alpha name cannot be changed.
  • NEW: Color space objects do not need a constant to track number of color channels.
"},{"location":"about/changelog/#050","title":"0.5.0","text":"
  • NEW: Add type annotations and refactor code to better accommodate the type annotations. Public API not really affected, but a bit of the internals have changed.
  • FIX: Fix issue where compose, if backdrop list is empty, would not respect in_place option.
"},{"location":"about/changelog/#040","title":"0.4.0","text":"
  • NEW: Officially support Python 3.10.
  • NEW: Slightly more accurate Oklab matrix calculation.
  • NEW: Exported dictionary form can now be used as a normal color input in functions like contrast, interpolate, etc.
  • NEW: Color objects will accept a dictionary mapping when alpha is not specified. When this occurs, alpha is assumed to be 1.
  • FIX: Fix an object compare issue.
"},{"location":"about/changelog/#030","title":"0.3.0","text":"

Breaking Changes

XYZ changes below will cause breakage as xyz now refers to XYZ with D65 instead of D50. Also, CSS identifiers changed per the recent specification change.

  • NEW: When calling dir() on Color(), ensure dynamic methods are in the list.
  • NEW: xyz now refers to XYZ D65. CSS color() function now specifies D65 color as either color(xyz x y z) or color(xyz-d65 x y z). XYZ D50 is now specified as color(xyz-D50 x y z).
  • NEW: Add CIELuv and CIELChuv D65 variants.
"},{"location":"about/changelog/#020","title":"0.2.0","text":"
  • NEW: Provide dedicated clip method. clip is still a specifiable method under the fit function. It is also a reserved name under fit and cannot be overridden via plugins or be removed.
  • NEW: Add more conversion shortcuts to OK family of color spaces.
  • FIX: Fix an issue where the shorter conversion path wasn't always taken as convert couldn't find to/from methods if the color space name had - in it.
"},{"location":"about/changelog/#010","title":"0.1.0","text":"

First non-alpha prerelease. Notable changes from the last alpha listed below.

Breaking Changes

There are some breaking changes if coming from the previous alpha releases. All sRGB cylindrical spaces' non-hue data ranges are no longer scaled to 0 - 100, but use 0 - 1. Hue ranges have not changed.

  • NEW: By accepting HSL, HSV, and HWB as non-hue channels as 0-100, we do lose a little precision, so for 1.0, we are switching to accepting and returning raw data values between 0 - 1. We've kept hue between 0 - 360 as it is easier for users to deal with hues between 0 - 360. Doing this will also match the new color spaces Okhsl and Okhsv that need to be kept at 0 - 1 to get better rounding.
  • NEW: We do not currently restrict percentages anymore in color() functions. There is no hard rules that we need to at this time and no currently specified spaces that do this in the CSS specification. This is relaxed for now until some future time when it becomes clear we must.
  • NEW: New okhsl and okhsv color space.
  • NEW: All color channels now accept the none keyword to specify an undefined channel. They can also optionally output CSS strings with the keyword.
  • NEW: Interpolation will return an undefined channel if both colors have that channel set to undefined.
  • NEW: Provide a way to dump a color object to a simple dictionary and have the Color() object accept that dictionary to recreate the color object.
  • NEW: Provide cat16 chromatic adaptation.
  • NEW: Add normalize method to force channel normalization (evaluation of channels and setting undefined as appropriate).
  • NEW: Interpolated and composited colors will normalize undefined channels when returning a color.
  • NEW: Jzazbz now also has an alias for az and bz channels as a and b respectively.
  • FIX: Fix an attribute \"get\" issue where attributes that were not present on the Color() object appeared to be present when using hasattr().
  • FIX: More accurate Oklab matrix.
"},{"location":"about/contributing/","title":"Contributing & Support","text":"

There are many ways to help support this project, regardless of skills and abilities. If you enjoy this project and want to get involved, consider checking out one of the various ways below. Feel free to get creative, there may be other ways to contribute in which we have not thought of!

"},{"location":"about/contributing/#become-a-sponsor","title":"Become a Sponsor","text":"

Open source projects take time and money. Help support the project by becoming a sponsor. You can add your support at any tier you feel comfortable with. No amount is too little. We also accept one time contributions via PayPal.

GitHub Sponsors PayPal

"},{"location":"about/contributing/#bug-reports","title":"Bug Reports","text":"
  1. Please read the documentation and search the issue tracker to try and find the answer to your question before posting an issue.

  2. When creating an issue on the repository, please provide as much info as possible:

    • Version being used.
    • Operating system.
    • Version of Python.
    • Errors in console.
    • Detailed description of the problem.
    • Examples for reproducing the error. You can post pictures, but if specific text or code is required to reproduce the issue, please provide the text in a plain text format for easy copy/paste.

    The more info provided, the greater the chance someone will take the time to answer, implement, or fix the issue.

  3. Be prepared to answer questions and provide additional information if required. Issues in which the creator refuses to respond to follow up questions will be marked as stale and closed.

"},{"location":"about/contributing/#reviewing-code","title":"Reviewing Code","text":"

Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss solutions to overcome weakness in the algorithm.

"},{"location":"about/contributing/#answer-questions-in-issues","title":"Answer Questions in Issues","text":"

Take time and answer questions and offer suggestions to people who've created issues in the issue tracker. Often people will have questions that you might have an answer for. Or maybe you know how to help them accomplish a specific task they are asking about. Feel free to share your experience to help others out.

"},{"location":"about/contributing/#pull-requests","title":"Pull Requests","text":"

Pull requests are welcome, and a great way to help fix bugs and add new features.

"},{"location":"about/contributing/#documentation-improvements","title":"Documentation Improvements","text":"

A ton of time has been spent not only creating and supporting this tool and related extensions, but also spent making this documentation. If you feel it is still lacking, show your appreciation for the tool and/or extensions by helping to improve the documentation.

"},{"location":"about/license/","title":"License","text":"

MIT License

Copyright \u00a9 2020 - 2023 Isaac Muse

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"},{"location":"about/releases/1.0/","title":"1.0 Migration Notes","text":"

ColorAide has been a constantly evolving project. As we've pushed for a stable release, the 1.0 milestone was no exception. For any of the early adopters, there are a number of things to be aware of when migrating to 1.0. In this guide, we'll cover the changes most likely to impact users.

"},{"location":"about/releases/1.0/#plugins","title":"Plugins","text":"

Plugins have gone through a number of reworks. In 1.0, we now require all plugins to be registered as instances. Prior to 1.0 plugins were just passed in un-instantiated. Some plugins were used as static classes almost, and some (color spaces) were instantiated on the creation of every color.

In 1.0, all plugins are required to be instantiated prior to registration. This gives the user the opportunity to specify any alternative defaults if desired.

>>> Color('red').delta_e('blue', method='cmc')\n108.56925233888809\n>>> from coloraide.distance.delta_e_cmc import DECMC\n>>> class Custom(Color):\n...     DELTA_E = \"cmc\"\n... \n>>> Custom.register(DECMC(l=1, c=1), overwrite=True)\n>>> Custom('red').delta_e('blue')\n109.7597278634398\n
"},{"location":"about/releases/1.0/#plugin-renames","title":"Plugin Renames","text":"

ColorAide had some inconsistencies when it came to some plugin names. For instance, we would use variations of Lch, LCH, etc. This made it more difficult to predict how a plugin was named, and remember it. In this case appropriate casing is usually LCh. All plugins referring to LCh were renamed to be more consistent.

Outside of LCh related classes, there were a few additional renames to better match the color space of interest's real name.

Old\u00a0Name New\u00a0Name Lch LCh LchD65 LChD65 Oklch OkLCh Lchuv LChuv Lch99o LCh99o LchChroma LChChroma OklchChroma OkLChchroma Lchish LChish XyY xyY Din99o DIN99o SRGB sRGB SRGBLinear sRGBLinear ORGB oRGB"},{"location":"about/releases/1.0/#default-plugins","title":"Default Plugins","text":"

Over the course of development, we've added a good number of color spaces. With the 1.0 release, it was decided to have the default Color object not register all available color spaces out of the box as the amount of color spaces has grown quite substantially.

As not all color spaces are registered by default, \u2206E plugins tied to color spaces no longer registered by default will also not be registered by default.

Lastly, since the bradford CAT is the default, it was deemed unnecessary to register all the other CAT plugins by default.

With all of that said, all of the exiting plugins are still available and can be included if/when needed. If any of the plugins that are no longer registered by default are needed, there are a couple of options:

  1. The recommended way is to just subclass the Color object and cherry pick the plugins that are needed. When classing, all the plugins registered in the base will be copied over to the derived class. Then we can just pass in the instantiated plugins.

    >>> from coloraide import Color as Base\n>>> from coloraide.spaces.jzazbz import Jzazbz\n>>> from coloraide.distance.delta_e_z import DEZ\n>>> class Color(Base): ...\n... \n>>> Color.register([Jzazbz(), DEZ()])\n>>> Color('red').convert('jzazbz')\ncolor(--jzazbz 0.13438 0.11789 0.11188 / 1)\n>>> Color('red').delta_e('blue', method='jz')\n0.33960388420164006\n
  2. We also provide a new color object derived from Color that includes all color spaces called ColorAll. This object won't be as light, but will provide quick and easy access to everything ColorAide offers. By default, it registers every plugin. It can be found under coloraide.everything.

    >>> from coloraide.everything import ColorAll as Color\n>>> Color('purple').convert('hunter-lab')\ncolor(--hunter-lab 24.796 50.842 -35.444 / 1)\n
"},{"location":"about/releases/1.0/#dynamic-propertiesfunctions-and-coordinate-access","title":"Dynamic Properties/Functions and Coordinate Access","text":"

Prior to 1.0, ColorAide's Color object had color channel properties that would magically mutate based on what the current color space was that the object currently held. While cool, this added overhead to every class attribute access. In an effort to dramatically reduce unnecessary overhead, this feature had to be rethought.

Additionally, \u2206E methods were also added dynamically. For instance, if we had the \u2206E 2000 distancing plugin registered, we'd have access to \u2206E via Color.delta_e_2000 or Color.delta_e(color, method='2000'). Again, the overhead to magically provide these properties the way we were posed the same problem as what was seen with dynamic color channel properties

Additionally, the Color object used to have a coords() function to get all the non-alpha color channels. This function was not really problematic, but as we decided a solution for the dynamic properties, it became apparent that we would no longer need such a function.

1.0 removed the overhead of magic dynamic properties and functions. In particular, we decided to approach color channel access in a new and different way.

Moving forward, the Color object is now iterable and indexable. Channels can be directly indexed via channel names or numerical indexes. You can even use slices:

>>> color = Color('orange')\n>>> color\ncolor(srgb 1 0.64706 0 / 1)\n>>> color['blue']\n0.0\n>>> color[0]\n1.0\n>>> color[:-1]\n[1.0, 0.6470588235294118, 0.0]\n

Color objects are also iterable, so you can just loop them as well, or cast the object as a list.

>>> for channel in Color('orange'):\n...     print(channel)\n... \n1.0\n0.6470588235294118\n0.0\n1.0\n>>> list(Color('green'))\n[0.0, 0.5019607843137255, 0.0, 1.0]\n

Setting channels is just as easy and can be done by indexing channels with names, numerical indexes, or even slices.

>>> color = Color('transparent')\n>>> color[:] = [1, 0, 0, 0.5]\n>>> color\ncolor(srgb 1 0 0 / 0.5)\n

As far as \u2206E methods are concerned, we already had two different ways to approach this, so we simply removed the dynamic functions. To access any of the different \u2206E methods, simply call the generic delta_e function and provide the method.

>>> Color('red').delta_e('green', method='2000')\n72.18053591241998\n
"},{"location":"about/releases/1.0/#gamut-mapping-and-clipping","title":"Gamut Mapping and Clipping","text":"

During our path to 1.0, we noticed that when performing gamut mapping and clipping, in most cases, we were performing them \"in place\" instead of the default which generated new Color instances. There are times when we occasionally wanted a new instance of the color when fitting a color to its gamut, but that turned out to not be the norm.

Generating new instances obviously will create more overhead, and in some cases, such as color mixing, returning a new color opposed to mutating the existing one makes a lot more sense, but with gamut mapping and clipping, for efficiency, we were often forcing \"in place\" operations.

1.0 now does gamut mapping and clipping in place by default. With this change, the in_place parameter is not longer available for fit() and clip().

So, if migrating to 1.0, if you were calling fit() and clip() directly, a few changes will need to be made. If you'd like to do an in place gamut correction, simply call the function. If you'd like to generate a new instance, clone the color first.

>>> color1 = Color('display-p3', [1, 1, 0])\n>>> color1.fit('srgb')\ncolor(display-p3 0.99859 0.9923 0.32854 / 1)\n>>> color1\ncolor(display-p3 0.99859 0.9923 0.32854 / 1)\n>>> color2 = Color('display-p3', [0, 1, 0])\n>>> color3 = color2.clone().fit('srgb')\n>>> color2, color3\n(color(display-p3 0 1 0 / 1), color(display-p3 0.45742 0.98328 0.29762 / 1))\n
"},{"location":"about/releases/1.0/#dictionary-output","title":"Dictionary Output","text":"

The dictionary format for input and output as been simplified for the 1.0 release. Prior to 1.0, the Color object used to export color dictionaries with the space name and each channel under an individually named key:

{'space': 'srgb', 'r': 1, 'g': 0, 'b': 0, 'alpha': 1}\n

This required more overhead, particularly when parsing to handle channel alias and the like. For 1.0, we've streamlined the format to export the data with all color coordinates under coords and the alpha channel still under alpha. This makes streamlines the process of handling dictionaries as inputs and outputting them when requested, in turn, improving performance.

>>> d = Color('rebeccapurple').to_dict()\n>>> d\n{'space': 'srgb', 'coords': [0.4, 0.2, 0.6], 'alpha': 1.0}\n>>> Color(d)\ncolor(srgb 0.4 0.2 0.6 / 1)\n
"},{"location":"about/releases/1.0/#interpolation","title":"Interpolation","text":"

Interpolation was an area we were generally unhappy with, so it was majorly overhauled.

Prior to 1.0, interpolation could be a bit awkward. Interpolation used to require the first color in the interpolation to be the calling object, and all the rest had to be fed in.

Color('red').interpolate(['blue', 'green', 'orange'])\n

When performing a simple mix, this felt natural and made sense:

Color('red').mix('blue', 0.25)\n

But with long chains of colors, this just felt cumbersome. To remedy this, we changed the interpolate and steps methods to @classmethods. We left mix as is since with two colors it feels natural.

So moving forward, interpolate and steps will execute interpolations from class methods.

>>> Color.interpolate(['red', 'blue', 'green', 'orange'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d301d0>\n>>> Color.steps(['red', 'blue', 'green', 'orange'], steps=10)\n[color(--oklab 0.62796 0.22486 0.12585 / 1), color(--oklab 0.56931 0.13909 -0.01995 / 1), color(--oklab 0.51066 0.05332 -0.16574 / 1), color(--oklab 0.45201 -0.03246 -0.31153 / 1), color(--oklab 0.47459 -0.06841 -0.17179 / 1), color(--oklab 0.49717 -0.10435 -0.03206 / 1), color(--oklab 0.51975 -0.1403 0.10768 / 1), color(--oklab 0.61073 -0.07466 0.12558 / 1), color(--oklab 0.70171 -0.00903 0.14348 / 1), color(--oklab 0.79269 0.05661 0.16138 / 1)]\n

This means that you do not have to call the function from an instantiated object, and if you do, the instantiated color that is making the call will not be included in the interpolation. Only the colors in the list are considered during the interpolation.

>>> Color('white').interpolate(['red', 'blue', 'green', 'orange'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c4a3d0>\n

This will make even more sense as we highlight the other changes.

Another problem we faced was the awkwardness of color stops and easing functions. Before we used to have a Piecewise object that you'd wrap a channel in to create color stops or inject easing functions and other various behaviors between colors, but it had to be applied on the second color in the chain, and this didn't quite work for the first color. If you wanted to add a stop to the first color, you then had to use a special stop parameter\u2026it was unintuitive.

from coloraide import Piecewise\nColor('red').interpolate(['blue', Piecewise('orange', 0.75, progress=lambda t: t * 3), 'purple'], stop=0.25)\n

In 1.0, we simplified things greatly. Since interpolate and steps now require that all colors must be in the input list if they are to be considered for interpolation, we can process them all in a consistent and more intuitive manner.

As before, steps and interpolate allow you to set function parameters to generally control the behavior for the entire interpolation across all colors. You can also still add easing functions via progress which will also affect the entire interpolation by default, but now you can inject easing functions directly between colors which will only be applied between those two colors.

>>> Color.interpolate(['red', lambda t: t * 3, 'orange', 'purple'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d89550>\n

You can also directly wrap any color in the list with stop to change the color stop position. Since the first color is now treated like all the other colors, there is no need for the stop function parameter either.

>>> from coloraide import stop\n>>> Color.interpolate([stop('red', 0.25), stop('orange', 0.75), 'purple'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112db3bd0>\n

And if you are familiar with CSS color hinting, which essentially alters the midpoint between two color stops, we've added a hint function which takes a new relative midpoint and returns a midpoint easing function which essentially acts the same as CSS interpolation hints.

>>> from coloraide import hint\n>>> Color.interpolate(['yellow', 'pink'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d89bd0>\n>>> Color.interpolate(['yellow', hint(0.25), 'pink'])\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d65e10>\n

All of this makes for a less confusing experience when using interpolation. Additionally, all of the changes simplified the logic allowing us to even add a new interpolation method!

>>> Color.interpolate(['red', 'blue', 'green', 'orange'], method='bspline')\n<coloraide.interpolate.bspline.InterpolatorBSpline object at 0x112d82ed0>\n
"},{"location":"about/releases/1.0/#color-space-filters","title":"Color Space Filters","text":"

In the beginning, the Color space object was created with a naive filtering system. It added a little overhead, but the real issue was the fact that it only filtered inputs through new, match, and through normal instantiation. It did not filter through almost any other method that accepted inputs. It was decided to leave color filtering up to the user.

>>> c = Color('display-p3', [1, 1, 0])\n>>> try:\n...     if c.space() not in ['srgb', 'hsl', 'hwb']:\n...         raise ValueError('Invalid Color Space')\n... except ValueError as e:\n...     print(e)\n... \nInvalid Color Space\n
"},{"location":"api/","title":"Color API","text":""},{"location":"api/#nan","title":"coloraide.NaN","text":"Description NaN is a convenience constant for float('nan'). Import path

NaN is imported from the coloraide library:

from coloraide import NaN\n
"},{"location":"api/#stop","title":"coloraide.stop","text":"
class stop:\n    def __init__(\n        self,\n        color: ColorInput,\n        value: float\n    ) -> None:\n        ...\n
Description stop objects are used in interpolate methods. They allow a user to specify a color stop for a given color during the interpolation process. Import Path

stop is imported from coloraide library:

from coloraide import stop\n
Parameters 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."},{"location":"api/#hint","title":"coloraide.hint","text":"
def hint(\n    mid: float,\n) -> Callable[..., float]:\n
Description hint returns an easing function that adjust the midpoint between two color stops. Import Path

hint is imported from coloraide library:

from coloraide import hint\n
Parameters Parameters Defaults Description mid A numerical value, relative to the two color stops it occurs between. The value will be used as the new midpoint."},{"location":"api/#color","title":"coloraide.Color","text":"
class Color:\n    def __init__(\n        self,\n        color: ColorInput,\n        data: VectorLike | None = None,\n        alpha: float = util.DEF_ALPHA,\n        **kwargs: Any\n    ) -> None:\n        ...\n
Description The Color class object is a wrapper around the internal color space objects. Color is the base Color object and only registers a select number of color spaces by default. It provides an API interface to allow users to specify and manipulate colors. Import path

Color is imported from the coloraide library:

from coloraide import Color\n
Parameters Parameters Defaults Description color A color string, a dictionary describing the color, or another Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored."},{"location":"api/#colorall","title":"coloraide.everything.ColorAll","text":"
class ColorAll(Color):\n    def __init__(\n        self,\n        color: ColorInput,\n        data: VectorLike | None = None,\n        alpha: float = util.DEF_ALPHA,\n        **kwargs: Any\n    ) -> None:\n        ...\n
Description The ColorAll class object is derived from Color and extends the registered color spaces to include all offered by ColorAide. Import path

ColorAll is imported from the coloraide library:

from coloraide.everything import ColorAll\n
Parameters Parameters Defaults Description color A color string, a dictionary describing the color, or another ColorAll class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored."},{"location":"api/#register","title":"Color.register","text":"
def register(\n    cls,\n    plugin: Plugin | Sequence[Plugin],\n    *,\n    overwrite: bool = False,\n    silent: bool = False\n) -> None:\n    ...\n
Description Register a plugin(s). Parameters Parameters Defaults Description plugin A plugin instance or list of plugin instances to register. overwrite False overwrite will allow an already registered plugin to be overwritten if the plugin to register specifies a name that is already used for registration. silent False silent will avoid throwing an error if the name is already found and overwrite is set to False in the specified category."},{"location":"api/#deregister","title":"Color.deregister","text":"
@classmethod\ndef deregister(\n    cls,\n    plugin: str | Sequence[str], *,\n    silent: bool = False\n) -> None:\n    ...\n
Description Remove an already registered plugin(s). Parameters Parameters Defaults Description plugin A string or list of strings that describe the plugin(s) to be removed. Strings should be in the format category:name where category is either space, delta-e, cat, filter, contrast, interpolate, or fit and name is the name the plugin was registered under. * will remove all plugins and category:* will remove all within a specific category. silent False silent will avoid throwing an error if the name can not be found in the specified category."},{"location":"api/#match","title":"Color.match","text":"
@classmethod\ndef match(\n    cls,\n    string: str,\n    start: int = 0,\n    fullmatch: bool = False\n) -> ColorMatch | None:\n    ...\n
Description

The match class method provides access to the color matching interface and allows a user to provide a color string and get back a ColorMatch object. ColorMatch objects contain three properties:

class ColorMatch:\n    def __init__(\n        self,\n        color: Color,\n        start: int,\n        end: int\n    ) -> None:\n        ...\n
Parameter Description color The Color object. start The starting point within the string buffer where the color was found. end The ending point within the string buffer where the color was found.

Match does not search the entire buffer, but simply matches at the location specified by start.

Parameters Parameters Defaults Description string A string representing the color. start 0 Accepts an integer offset into the provided string buffer to start the match. fullmatch False A boolean which defines whether match must match to the end of the string buffer. Return Returns a ColorMatch object."},{"location":"api/#new","title":"Color.new","text":"
def new(\n    self,\n    color: ColorInput,\n    data: VectorLike | None = None,\n    alpha: float = util.DEF_ALPHA,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description The new class method exposes the interface of creating new color objects. Using new is the same as using Color(). Parameters Parameters Defaults Description color A color string, or other Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored. Return Returns a Color object."},{"location":"api/#random","title":"Color.random","text":"
@classmethod\ndef random(\n    cls,\n    space: str,\n    *,\n    limits: Sequence[Sequence[float] | None] | None = None\n) -> Color:\n    ...\n
Description Generate a random color in the provided space. The color space's channel range will be used as a limit for the channel. For color spaces with no clearly defined gamut, these values can be arbitrary. In such cases, it may be advisable to fit the returned color space to a displayable gamut. Parameters Parameters Defaults Description space The color space name in which to generate a random color in. limits None An optional list of constraints for various color channels. Each entry should either be a sequence contain a minimum and maximum value, or should be None. None values will be ignored and the color space's specified channel range will be used instead. Any missing entries will be treated as None. Return Returns a Color object."},{"location":"api/#clone","title":"Color.clone","text":"
def clone(\n    self\n):\n
Description The clone method provides a way to create a duplicate of the current Color instance. Return Returns a Color object."},{"location":"api/#update","title":"Color.update","text":"
def update(\n    self,\n    color: ColorInput,\n    data: VectorLike | None = None,\n    alpha: float = util.DEF_ALPHA,\n    *,\n    norm: bool = True,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description The update method provides a way to update the underlying color space with coordinates from any color space. The method's signature is the same as new except that it adds an additional norm parameter used to skip achromatic hue normalization when converting to the current color space. The object itself will assume the equivalent color in the current color space that matches the input color's value (assuming no algorithmic limitations preventing an equivalent color). Parameters Parameters Defaults Description color A color string, or other Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored. norm True When set to False, this prevents achromatic normalization when updating from a different color space. If no update occurs, nothing is done. Return Returns a reference to the current Color object."},{"location":"api/#mutate","title":"Color.mutate","text":"
def mutate(\n    self,\n    color: ColorInput,\n    data: VectorLike | None = None,\n    alpha: float = util.DEF_ALPHA,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description The mutate method is similar to update except that it does not convert the input color to the current color space, but instead replaces the current color space and values with the input color's color space and values. Parameters Parameters Defaults Description color A color string, or other Color class object. If given data, a string must be used and should represent the color space to use. data None data accepts a list of numbers representing the coordinates of the color. If provided, color must be a string specifying the color space. alpha 1 alpha accepts a number specifying the alpha channel. Must be used in conjunction with data or it will be ignored. Return Returns a reference to the current Color object."},{"location":"api/#convert","title":"Color.convert","text":"
def convert(\n    self,\n    space: str,\n    *,\n    fit: bool | str = False,\n    in_place: bool = False,\n    norm: bool = True\n) -> Color:\n    ...\n
Description Converts a Color object from one color space to another. If the current color space matches the specified color space, a clone of the current color will be returned with no changes to the channel values. If in_place is True, the current object will be modified in place. Parameters Parameters Defaults Description space A string representing the desired final color space. fit False Parameter specifying whether the current color should be gamut mapped into the final, desired color space. If set to True, the color will be gamut mapped using the default gamut mapping method. If set to a string, the string will be interpreted as the name of the gamut mapping method to be used. in_place False Boolean specifying whether the convert should alter the current Color object or return a new one. norm True When set to False, this prevents achromatic normalization when converting from a different color space. If no update occurs, nothing is done. Return Returns a reference to the converted Color object. If in_place is True, the return will be a reference to the current Color object."},{"location":"api/#space","title":"Color.space","text":"
def space(\n    self\n) -> str:\n    ...\n
Description Retrieves the current color space name as specified by the underlying color space object. Return Returns a string with the name of the current color space."},{"location":"api/#normalize","title":"Color.normalize","text":"
def normalize(\n    self,\n    *,\n    nans: bool = True\n) -> Color:\n    ...\n
Description Force normalization of a color's channels by cleaning up channels that setting hue to undefined if the color is achromatic. if nans is set to False, the hue normalization step (setting hue to undefined) will be skipped. Normalize modifies the current color in place. Parameters Parameters Defaults Description nans True Perform hue normalization (setting hue to undefined if the color is achromatic). Return Returns a reference to the current Color object after normalizing the channels for undefined hues."},{"location":"api/#to_dict","title":"Color.to_dict","text":"
def to_dict(\n    self,\n    *,\n    nans: bool = True\n) -> Mapping[str, Any]:\n    ...\n
Description

Dump the color object to a simple dictionary.

{\n    'space': 'srgb',            # Color space name\n    'coords': [1.0, 0.0, 0.0],  # Color channel values\n    'alpha': 1.0                # Alpha channel value\n}\n
Parameters Parameters Defaults Description nans True Return channel values having undefined values resolved as defined values. Return A dictionary containing the color space name and channel values."},{"location":"api/#to_string","title":"Color.to_string","text":"
def to_string(\n    self,\n    **kwargs: Any\n) -> str:\n    ...\n
Description Method that converts the current color to an output format supported by the color space. While a number of the parameters are common, some may be specific to the color space. The usage guide covers color space specific options in more details. Parameters

Common parameters:

Parameters Defaults Description alpha None Boolean or None value which determines whether the output includes alpha. If None, the default alpha will only be shown if less than 1. If True, alpha will always be shown. If False, alpha will be omitted. precision 5 Integer value that sets precision and scale. Precision and scale will match the value if greater than zero. If 0, values will be rounded to the nearest integer. If -1, number will be output at the highest precision. fit True A boolean that controls whether gamut mapping is performed on string creation. By default, colors will be fit to their own color space. This can be disabled by setting to False. color False A boolean that will determine if the color(space coord+ / alpha) format is used for string output. Has highest precedence. percent Varies A boolean that will output color channels as percents. Not all color spaces support percents, or may support percents only in certain scenarios. Default value may be determined by the color space.

sRGB specific parameters:

Parameters Defaults Description hex. False String output will be in #RRGGBBAA format. names False Boolean indicating a preference for CSS color names. When translating a color to it's closest hex form, if that hex value matches a CSS color name, that color name will be returned as the output. hex does not have to be True for this to apply. compress False If hex is True and compress is True, hex values will be compressed if possible: #RRGGBBAA \u2192 #RGBA.

Space dependent parameters:

Parameters Defaults Description comma False If supported by the color space and the current output format, commas will be used instead of space format: rgba(0, 0, 0, 1) \u2192 rgb(0 0 0 /1). Return Returns a string representation of the current color."},{"location":"api/#luminance","title":"Color.luminance","text":"
def luminance(\n    self,\n    *,\n    white: VectorLike | None = cat.WHITES['2deg']['D65']\n\n) -> float:\n    ...\n
Description Get the relative luminance. Relative luminance is obtained from the Y coordinate in the XYZ color space. XYZ, in this case, has a D65 white point. Parameters Parameters Defaults Description white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns an float indicating the relative luminance."},{"location":"api/#colorcontrast","title":"Color.contrast","text":"
def contrast(\n    self,\n    color: ColorInput,\n    method: str | None = None\n) -> float:\n    ...\n
Description Get the contrast ratio based on the relative luminance between two colors. Parameters Parameters Defaults Description color A color string or Color object representing a color. method None Specify the method used to obtain the contrast value. If None, the default specified by the class will be used. Return Returns a float indicating the contrast ratio between two colors."},{"location":"api/#distance","title":"Color.distance","text":"
def distance(\n    self,\n    color: ColorInput,\n    *,\n    space: str = \"lab\"\n) -> float:\n    ...\n
Description Performs a euclidean distance algorithm on two colors. Parameters Parameters Defaults Description color A color string or Color object representing a color. space \"lab\" Color space to perform distancing algorithm in. Return Returns a float indicating euclidean distance between the two colors."},{"location":"api/#delta_e","title":"Color.delta_e","text":"
def delta_e(\n    self,\n    color: ColorInput,\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> float:\n    ...\n
Description

Performs a delta E distance algorithm on two colors. Default algorithm that is used is Delta E 1976 (76). Some methods have additional weighting that can be configured through method specific options which are represented by **kwargs.

Available methods:

Name Input Parameters \u2206E*ab\u00a0(CIE76) 76 \u2206E*cmc\u00a0(CMC\u00a0l:c\u00a0(1984)) cmc l=2, c=1 \u2206E*94\u00a0(CIE94) 94 kl=1, k1=0.045, k2=0.015 \u2206E*00 \u00a0(CIEDE2000) 2000 kl=1, kc=1, kh=1 \u2206EHyAB\u00a0(HyAB) hyab space=\"lab\" \u2206Eok ok scalar=1 \u2206Eitp\u00a0(ICtCp) itp scalar=720 \u2206Ez\u00a0(Jzazbz) jz \u2206E99o\u00a0(DIN99o) 99o \u2206Ecam16 cam16 model=ucs \u2206EHCT hct Parameters Parameters Defaults Description color A color string or Color object representing a color. method None String that specifies the method to use. If None, the default will be used. **kwargs Any distancing specific parameters to pass to \u2206E method. Return Returns a float indicating the delta E distance between the two colors."},{"location":"api/#closest","title":"Color.closest","text":"
def closest(\n    self,\n    colors: Sequence[ColorInput],\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description Given a list of colors, calculates the closest color to the calling color object. Parameters Parameters Defaults Description colors A list of color strings, Color object, or dictionary representing a color. method None String that specifies the method of color distancing to use. **kwargs Any distancing specific parameters to pass to \u2206E method. Return The Color that is closest to the calling color object. In the off chance that an empty list is passed in None will be returned."},{"location":"api/#mask","title":"Color.mask","text":"
def mask(\n    self,\n    channel: str | Sequence[str],\n    *,\n    invert: bool = False,\n    in_place: bool = False\n) -> Color:\n    ...\n
Description The mask method will set any and all specified channels to NaN. If invert is set to True, mask will set any and all channels not specified to NaN. Parameters Parameters Defaults Description channel A string specifying a channel, or a list of strings specifying multiple channels. Specified channels will be masked (or the only channels not masked if invert is True). invert False Use inverse masking logic and mask all channels that are not specified. in_place False Boolean used to determine if the current color should be modified \"in place\" or a new Color object should be returned. Return Returns a reference to the masked Color object. If in_place is True, the return will be a reference to the current Color object."},{"location":"api/#interpolate","title":"Color.interpolate","text":"
@classmethod\ndef interpolate(\n    cls,\n    colors: Sequence[ColorInput | interpolate.stop | Callable[..., float]],\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    progress: Mapping[str, Callable[..., float]] | Callable[..., float] | None = None,\n    hue: str = util.DEF_HUE_ADJ,\n    premultiplied: bool = True,\n    extrapolate: bool = False,\n    domain: list[float] | None = None,\n    method: str = \"linear\",\n    padding: float | tuple[float, float] | None = None,\n    carryforward: bool | None = False,\n    powerless: bool | None = False,\n    **kwargs: Any\n) -> Interpolator:\n    ...\n
Description

The interpolate method creates a function that takes a value between 0 - 1 and interpolates a new color based on the input value.

If more than one color is provided, the returned function will span the interpolations between all the provided colors with the same range of 0 - 1.

Interpolation can be customized by limiting the interpolation to specific color channels, providing custom interpolation functions, and even adjusting the hue logic used.

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\u00a0Evaluation Description shorter Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 [-180, 180]. longer Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 {[-360, -180], [180, 360]}. increasing Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 [0, 360]. decreasing Angles are adjusted so that \u03b8\u2082 - \u03b8\u2081 \u2208 [-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. bspline An interpolation method that employs cubic B-spline curves to calculate an interpolation path through multiple colors. natural A natural interpolation spline based on the cubic B-spline curve. monotone An interpolation method that utilizes a monotonic cubic spline based on the Hermite spline. catrom Interpolation based on the Catmull-Rom cubic spline. Parameters Parameters Defaults Description colors A list of color strings, Color objects, dictionaries representing a color, stop objects, or easing functions. space \"lab\" Color space to interpolate in. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space. progress None An optional function that that allows for custom logic to perform non-linear interpolation. hue \"shorter\" Define how color spaces which have hue angles are interpolated. Default evaluates between the shortest angle. premultiplied True Use premultiplied alpha when interpolating. extrapolate False Interpolations should extrapolate when values exceed the domain range ([0, 1] by default). domain None A list of numbers defining the domain range of the interpolation. method \"linear\" The interpolation method to use. padding None Adjust the padding of the interpolation range. carryforward False Carry forward undefined channels when converting to the interpolation space. If None, will use the class default which is False by default. powerless None Treat explicitly defined hues as powerless when the color is considered achromatic. If None, will use the class default which is False by default. Return Returns a function that takes a range within the specified domain, the default being [0..1]. The function returns a new, interpolated Color object."},{"location":"api/#steps","title":"Color.steps","text":"
@classmethod\ndef steps(\n    cls,\n    colors: Sequence[ColorInput | interpolate.stop | Callable[..., float]],\n    *,\n    steps: int = 2,\n    max_steps: int = 1000,\n    max_delta_e: float = 0,\n    delta_e: str | None = None,\n    delta_e_args: dict[str, Any] | None = None,\n    **interpolate_args: Any\n) -> list[Color]:\n    ...\n
Description

Creates an interpolate function and iterates through it with user defined step parameters to produce discrete color steps. Will attempt to provide the minimum number of steps without exceeding max_steps. If max_delta_e is provided, the distance between each stop will be cut in half until there are no colors with a distance greater than the specified max_delta_e. The default \u2206E method is used by default, but it can be changed with the delta_e parameter.

If more than one color is provided, the steps will be returned from the interpolations between all the provided colors.

Like interpolate, the default interpolation space is lab.

Parameters Parameters Defaults Description color A list of color strings, Color objects, dictionaries representing a color, stop objects, or easing functions. steps 2 Minimum number of steps. max_steps 1000 Maximum number of steps. max_delta_e 0 Maximum delta E distance between the color stops. A value of 0 or less will be ignored. delta_e None A string indicating which \u2206E method to use. If nothing is supplied, the class object's current default \u2206E method will be used. delta_e_args None A dictionary containing keyword arguments to be passed to the delta_e method. **interpolate_args See\u00a0interpolate Keyword arguments defined in interpolate. Return List of Color objects."},{"location":"api/#discrete","title":"Color.discrete","text":"
@classmethod\ndef discrete(\n    cls,\n    colors: Sequence[ColorInput | interpolate.stop | Callable[..., float]],\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    steps: int | None = None,\n    max_steps: int = 1000,\n    max_delta_e: float = 0,\n    delta_e: str | None = None,\n    delta_e_args: dict[str, Any] | None = None,\n    domain: list[float] | None = None,\n    **interpolate_args: Any\n) -> Interpolator:\n    ...\n
Description

Generates an interpolate function with discrete color scale. By default it assumes as many discrete colors as the user inputs, but steps can be used to generate more or less using the input colors. As discrete is built on steps, it takes all the same arguments.

Like interpolate, the default interpolation space is lab.

Parameters Parameters Defaults Description color A list of color strings, Color objects, dictionaries representing a color, stop objects, or easing functions. space \"lab\" Color space to interpolate in. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space. steps None Minimum number of steps. If None, steps will be set to the number of input colors. max_steps 1000 Maximum number of steps. max_delta_e 0 Maximum delta E distance between the color stops. A value of 0 or less will be ignored. delta_e None A string indicating which \u2206E method to use. If nothing is supplied, the class object's current default \u2206E method will be used. delta_e_args None A dictionary containing keyword arguments to be passed to the delta_e method. domain None A list of numbers defining the domain range of the interpolation. **interpolate_args See\u00a0interpolate Keyword arguments defined in interpolate. Return Returns a function that takes a range within the specified domain, the default being [0..1]. The function returns a new, interpolated Color object."},{"location":"api/#mix","title":"Color.mix","text":"
def mix(\n    self,\n    color: ColorInput,\n    percent: float = util.DEF_MIX,\n    *,\n    in_place: bool = False,\n    **interpolate_args: Any\n) -> Color:\n    ...\n
Description Interpolates between two colors returning a color that represents the mixing of the base color and the provided color mixed at the provided percent, where percent applies to how much the provided color contributes to the the final result. Parameters Parameters Defaults Description color A color string, Color object, and/or dictionary representing a color. percent 0.5 A numerical value between 0 - 1 representing the percentage at which the parameter color will be mixed. in_place False Boolean used to determine if the the current color should be modified \"in place\" or a new Color object should be returned. **interpolate_args See\u00a0interpolate Keyword arguments defined in interpolate. Return Returns a reference to the new Color object or a reference to the current Color if in_place is True."},{"location":"api/#average","title":"Color.average","text":"
@classmethod\ndef average(\n    cls,\n    colors: Iterable[ColorInput],\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    premultiplied: bool = True,\n    powerelss: bool | None = None\n    **kwargs: Any\n) -> Color:\n
Description Get the average mean of all channels given a particular set of input colors. Parameters Parameters Defaults Description colors An iterable of color strings, Color objects, and/or dictionaries representing a color. space None An optional string to specify what color space the colors should be averaged in. If none is provided, Oklab is assumed. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified via space. premultiplied True Specify whether colors should be premultiplied during the averaging process. powerless None Treat explicitly defined hues as powerless when the color is considered achromatic. If None, will use the class default which is False by default. Return Returns a reference to the new Color object representing the average of the input colors."},{"location":"api/#cvd","title":"Color.filter","text":"
def filter(\n    self,\n    name: str,\n    amount: float | None = None,\n    *,\n    space: str | None = None,\n    in_place: bool = False,\n    out_space: str | None = None,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description

Apply a color filter to alter a given color. The non-CVD filters are based on the W3C Filter Effects and behave in the same manner. Colors are evaluated in the sRGB Linear color space unless otherwise specified via the space parameter. No other color space will be accepted except sRGB and sRGB Linear.

An amount can be provided to adjust how much the color is filtered. Any clamping that occurs with the amount parameter, and related ways in which amount are applied, follow the W3C Filter Effects spec.

Some filters, such as CVDs, may take additional arguments via kwargs.

Filters Name Default Brightness brightness 1 Saturation saturate 1 Contrast contrast 1 Opacity opacity 1 Invert invert 1 Hue\u00a0rotation hue-rotate 0 Sepia sepia 1 Grayscale grayscale 1 CVD\u00a0Filters Name Default Protanopia\u00a0CVD protan 1 Deuteranopia\u00a0CVD deutan 1 Tritanopia\u00a0CVD tritan 1 Parameters Parameters Defaults Description name The name of the filter that should be applied. amount See\u00a0above A numerical value adjusting to what degree the filter is applied. Input range can vary depending on the filter being used. Default can also dependent on the filter being used. space None Controls the algorithm used for simulating the given CVD. in_place False Boolean used to determine if the the current color should be modified \"in place\" or a new Color object should be returned. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified via space. **kwargs Additional filter specific parameters.

CVDs also take an optional method parameter that allows for specifying the CVD algorithm to use.

Simulation\u00a0Approach Name Brettel 1997 brettel Vi\u00e9not, Brettel, and Mollon 1999 vienot Machado 2009 machado Return Returns a reference to the new Color object or a reference to the current Color if in_place is True."},{"location":"api/#harmony","title":"Color.harmony","text":"
def harmony(\n    self,\n    name: str,\n    *,\n    space: str | None = None,\n    out_space: str | None = None,\n    **kwargs: Any\n) -> list[Color]:\n    ...\n
Description

The harmony method uses the current color and returns a set of harmonious colors (including the current color). The color harmonies are based on the classical color harmonies of color theory. By default, harmonious colors are selected under the perceptually uniform OkLCh color space, but other cylindrical color spaces can be used.

Harmony Name Monochromatic mono Complementary complement Split\u00a0Complement split Analogous analogous Triadic triad Tetradic\u00a0Square square Tetradic\u00a0Rectangle rectangle Wheel wheel Parameters Parameters Defaults Description name Name of the color harmony to use. space 'oklch' Color space under which the harmonies will be calculated. Must be a cylindrical space. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified via space. **kwargs Any harmony specific parameters to pass to the called harmony. Return Returns a list of Color objects."},{"location":"api/#compose","title":"Color.compose","text":"
def compose(\n    self,\n    backdrop: ColorInput | Sequence[ColorInput],\n    *,\n    blend: str | bool = True,\n    operator: str | bool = True,\n    space: str | None = None,\n    out_space: str | None = None,\n    in_place: bool = False\n) -> Color:\n    ...\n
Description

Apply compositing which consists of a blend mode and a Porter Duff operator for alpha compositing. The current color is treated as the source (top layer) and the provided color as the backdrop (bottom layer). Colors will be composited in the srgb color space unless otherwise specified.

Colors should generally be RGB-ish colors (sRGB, Display P3, A98 RGB, etc.). The algorithm is designed only for RGB-ish colors. Non-RGB-ish colors are likely to provide nonsense results.

Supported blend modes are:

Blend Modes normal multiply darken lighten burn dodge screen overlay hard-light exclusion difference soft-light hue saturation luminosity color color hue saturation luminosity

Supported Port Duff operators are:

Operators clear copy destination source-over destination-over source-in destination-in source-out destination-out source-atop destination-atop xor lighter Parameters Parameters Defaults Description backdrop A background color represented with either a string or Color object. blend None A blend mode to use to use when compositing. Values should be a string specifying the name of the blend mode to use. If None, normal will be used. If False, blending will be skipped. operator None A Porter Duff operator to use for alpha compositing. Values should be a string specifying the name of the operator to use. If None, source-over will be used. If False, alpha compositing will be skipped. space None A color space to perform the overlay in. If None, the base color's space will be used. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space. in_place False Boolean used to determine if the the current color should be modified \"in place\" or a new Color object should be returned. Return Returns a reference to the new Color object or a reference to the current Color if in_place is True."},{"location":"api/#clip","title":"Color.clip","text":"
def clip(\n    self,\n    space: str | None = None\n) -> Color:\n
Description Performs simple clipping on color channels that are out of gamut. Parameters Parameters Defaults Description space None The color space that the color must be mapped to. If space is None, then the current color space will be used. Return Returns a reference to the current Color after fitting its coordinates to the specified gamut."},{"location":"api/#fit","title":"Color.fit","text":"
def fit(\n    self,\n    space: str | None = None,\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> Color:\n    ...\n
Description

Fits color to the current or specified color gamut.

By default, lch-chroma gamut mapping is used. This is essentially an approach that holds lightness and hue constant in the CIELCh color space while reducing chroma until the color is in gamut. Clipping is done at each step of the way and the color distance measured to see how close our color is to the intended color.

The supported gamut mapping methods are:

Name Input Clipping clip OkLCh Chroma oklch-chroma LCh Chroma lch-chroma Parameters

Some methods could have additional parameters to configure the behavior, these would be done through **kwargs. None of built-in gamut mapping methods currently have additional parameters.

Parameters Defaults Description space None The color space that the color must be mapped to. If space is None, then the current color space will be used. method None String that specifies which gamut mapping method to use. If None, lch-chroma will be used. Return Returns a reference to the current Color after fitting its coordinates to the specified gamut."},{"location":"api/#in_gamut","title":"Color.in_gamut","text":"
def in_gamut(\n    self, space: str | None = None,\n    *,\n    tolerance: float = util.DEF_FIT_TOLERANCE\n) -> bool:\n
Description Checks if the current color is in the current or specified gamut. Parameters Parameters Defaults Description space None The color space that the color must be fit within. If space is None, then the current color space will be used. tolerance 0.000075 Tolerance allowed when checking bounds of color. Return Returns a boolean indicating whether the color is in the specified gamut."},{"location":"api/#get","title":"Color.get","text":"
def get(\n    self,\n    name: str | list[str] | tuple[str, ...],\n    *,\n    nans: bool = True\n) -> float | list[float]:\n
Description Retrieves the coordinate value from the specified channel or values from a sequence of specified channels. Channels must be a channel name in the current color space or a channel name in the specified color space using the syntax: space.channel. Parameters Parameters Defaults Description name Channel name or sequence of channel names. Channel names can define the color space and channel name to retrieve value from a different color space. nans True Determines whether an undefined value is allowed to be returned. If disabled, undefined values will be resolved before returning. Return

Returns a numerical value that is stored internally for the specified channel, or a calculated value in the case that a channel in a different color space is requested. If more than one value is requested, the a list of numerical values will be returned.

"},{"location":"api/#set","title":"Color.set","text":"
def set(\n    self,\n    name: str | dict[str, float | Callable[..., float]],\n    value: float | Callable[..., float] | None = None,\n    *,\n    nans: bool = True\n) -> Color:\n
Description

Sets the given value to the specified channel. If the name is provided in the form space.channel, the value will be applied to the channel of the specified color space while keeping current color space the same.

The value can be a numerical value or a function that accepts a numerical channel value and returns a numerical channel value.

name can also be a dictionary of channels, each with a value. In this case, the value parameter of the function can be ignored.

This function returns the current colors reference so that multiple sets can be chained together.

Parameters Parameters Defaults Description name A string containing a channel name or color space and channel separated by a . specifying the what channel to set. If value is omitted, name can also be a dictionary containing multiple channels, each specifying their own value to set. value A numerical value, a string value accepted by the specified color space, or a function. nans True When doing relative sets via a callback input, ensure the channel value passed to the callback is a real number, not an undefined value. Return Returns a reference to the current Color object."},{"location":"api/#coords","title":"Color.coords","text":"
def coords(\n    self,\n    *,\n    nans: bool = True\n) -> Vector:\n    ...\n
Description Get the color channels (no alpha). If nans is set to False, all undefined values will be returned as defined. Parameters Parameters Defaults Description nans True If nans is set to False, all undefined values will be returned as defined. Return Returns a list of float values, one for each color channel."},{"location":"api/#coords","title":"Color.alpha","text":"
def alpha(\n    self,\n    *,\n    nans: bool = True\n) -> float:\n    ...\n
Description Get the alpha channel's value. If nans is set to False, an undefined value will be returned as defined. Parameters Parameters Defaults Description nans True If nans is set to False, an undefined value will be returned as defined. Return Returns a float."},{"location":"api/#is_achromatic","title":"Color.is_achromatic","text":"
def is_achromatic(\n    self\n) -> bool:\n    ...\n
Description Can be called on any color to determine if the color is achromatic. If a color is achromatic, or very close to achromatic, it will return True. Return Returns a boolean indicating whether the color is achromatic."},{"location":"api/#is_nan","title":"Color.is_nan","text":"
def is_nan(\n    self,\n    name: str\n) -> bool:\n    ...\n
Description Retrieves the coordinate value from the specified channel and checks whether the value is undefined (set to NaN). Channel must be a channel name in the current color space or a channel name in the specified color space using the syntax: space.channel. Parameters Parameters Defaults Description name A string indicating what channel property to check. Return Returns a boolean indicating whether the specified color space's channel is NaN."},{"location":"api/#white","title":"Color.white","text":"
def white(\n    self\n) -> Vector:\n    ...\n
Description Retrieves the white point for the current color's color space. Return Returns a set of XYZ coordinates that align with the white point for the given color space."},{"location":"api/#blackbocy","title":"Color.blackbody","text":"
@classmethod\ndef blackbody(\n    cls,\n    temp: float,\n    duv: float = 0.0,\n    *,\n    scale: bool = True,\n    scale_space: str | None = None,\n    out_space: str | None = None,\n    method: str | None = None,\n    **kwargs: Any\n) -> Color:\n
Description Creates a color from a temperature in Kelvin and an optional \u2206uv. The color will be scaled within the linear RGB space specified by scale_space and can be disabled by setting scale to False. By default, the Ohno 2013 algorithm is used and can be configured via method. Parameters Parameters Defaults Description temp A positive temperature in Kelvin. Accepted range of temperature is based on the algorithm. duv 0.0 An optional \u2206uv specifying the distance from the black body curve. scale True Scale the color with a linear RGB color space as defined by scale_space. scale_space 'srgb-linear' If scale is enabled, scale_space defines the RGB color space in which the returned color should be scaled within. The color space should be a linear space for best results. If undefined, srgb-linear will be used. out_space None Color space that the new color should be in. If None, the return color will be in the same color space as specified by space or xyz-d65 if space is None. method None A string specifying the algorithm to use. By default robertson-1968 is used. **kwargs Any plugin specific parameters to pass to the blackbody method. Return Returns a reference to the current Color."},{"location":"api/#cct","title":"Color.cct","text":"
def cct(\n    self,\n    *,\n    method: str | None = None,\n    **kwargs: Any\n) -> Vector:\n
Description Returns the associated CCT and \u2206uv for a given color. If the color is beyond an acceptable range for the algorithm or the color is very far from the locus, the result may be surprising. Parameters Parameters Defaults Description method None A string specifying the algorithm to use. By default robertson-1968 is used. **kwargs Any plugin specific parameters to pass to the blackbody method. Return Returns a list containing the correlated color temperature in Kelvin and the \u2206uv."},{"location":"api/#colorchromatic_adaptation","title":"Color.chromatic_adaptation","text":"
@classmethod\ndef chromatic_adaptation(\n    cls,\n    w1: tuple[float, float],\n    w2: tuple[float, float],\n    xyz: VectorLike,\n    *,\n    method: str | None = None\n) -> Vector:\n    ...\n
Description A class method that converts an XYZ set of coordinates between two given white points. The first white point must match the white point of the current coordinates and the second white point must be the desired white point to use. method dictates the method of chromatic adaptation to use. Parameters Parameters Defaults Description w1 Current white point of the XYZ coordinates. w2 Desired white point of the XYZ coordinates. xyz The XYZ coordinates to adapt. method None The method of chromatic adaptation to use. If not specified, the current class's default method will be used. Return Returns a set of XYZ coordinates that have been chromatically adapted to the desired white point."},{"location":"api/#xy","title":"Color.xy","text":"
def xy(\n    self,\n    *,\n    white: VectorLike | None = None\n) -> Vector:\n
Description Retrieves the CIE 1931 (x, y) chromaticity coordinates for a given color. Parameters Parameters Defaults Description white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns a tuple of CIE 1931 (x, y) chromaticity points for the given color. The XYZ translation to xy will use the current color's white point to ensure the values are relative to the proper white point."},{"location":"api/#xy","title":"Color.uv","text":"
def uv(\n    self,\n    mode: str = '1976',\n    *,\n    white: VectorLike | None = None\n) -> Vector:\n    ...\n
Description Retrieves the UCS 1960 (u, v) chromaticity coordinates for a given color or the CIE 1976 UCS (u', v') chromaticity coordinates, the latter being the default. Parameters Parameters Defaults Description mode '1976' A string indicating what mode to use. 1976 refers to the (u', v') points as described by CIE 1976 UCS and 1960 describes the (u, v) points as documented by CIE 1960 UCS. white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns a tuple of (u, v) \u2013 either 1976 (u', v') or 1960 (u, v) \u2013 chromaticity points for the given color. The XYZ translation to uv will use the current color's white point to ensure the values are relative to the proper white point."},{"location":"api/#get_chromaticity","title":"Color.get_chromaticity","text":"
def get_chromaticity(\n    self,\n    cspace: str = 'uv-1976',\n    *,\n    white: VectorLike | None = None\n) -> Vector:\n    ...\n
Description Retrieves the 1931 xy, 1960 uv, or 1976 u'v' chromaticity coordinates with the luminance (Y). Coordinates are returned in the format specified by cspace and will use the white point of the current color. Parameters Parameters Defaults Description cspace 'uv-1976' A string indicating what chromaticity space to use. uv-1976 being the default. white None Specify the white in which to chromatically adapt the points from, if none is specified, the current color's white point is assumed. Return Returns a list of chromaticity coordinates. Results will either be in [x, y, Y] for 1931 xy, [u, v, Y] for 1960 uv, or [u', v', Y] for 1976 u'v'."},{"location":"api/#get_chromaticity","title":"Color.chromaticity","text":"
@classmethod\ndef chromaticity(\n    cls,\n    space: str,\n    coords: VectorLike,\n    cspace: str = 'uv-1976',\n    *,\n    scale: bool = False,\n    scale_space: str | None = None,\n    white: VectorLike | None = None\n) -> Color:\n    ...\n
Description Returns a color that satisfies the provided chromaticity coordinates. Coordinates can be in the form 1931 xyY, 1960 uvY, or 1976 u'v'Y and can be configured via cspace. The target space to convert to should be specified via space. Chromaticity coordinates should math the white space of the targeted space, but if they are not the white point of the chromaticity coordinates can be specified with white. Parameters Parameters Defaults Description space Color space to chromaticities to. coords The chromaticity coordinates. Values can be in either 3D form (with luminance Y). cspace 'uv-1976' A string indicating what chromaticity space to use. uv-1976 being the default. white None Specify the white in which to chromatically adapt the points from, if none is specified, the targeted color's white point is assumed. scale True Scale the color with a linear RGB color space as defined by scale_space. scale_space None If scale is enabled, scale_space defines the RGB color space in which the returned color should be scaled within. The color space should be a linear space for best results. If undefined, srgb-linear will be used. Return Returns a reference to a new Color object that satisfies the chromaticity coordinates."},{"location":"api/#convert_chromaticity","title":"Color.convert_chromaticity","text":"
@classmethod\ndef convert_chromaticity(\n    cls,\n    cspace1: str,\n    cspace2: str,\n    coords: VectorLike\n) -> Vector:\n    ...\n

Description

  • Converts a 2D chromaticity pair from one chromaticity space to another. Supported spaces are xy-1931, uv-1960, and uv-1976.

Parameters

  • Parameters Defaults Description cspace1 Initial chromaticity space for the given coordinates. cspace2 Target chromaticity space for the given coordinates. coords The 2D chromaticity coordinates to convert.

Return

  • Returns the converted 2D chromaticity coordinates.
"},{"location":"colors/","title":"Color Spaces","text":"

ColorAide aims to support all the color spaces and models currently offered in modern CSS, such as sRGB, Display P3, CIELab, Oklab, etc. We also include a number of color spaces that are not available in CSS.

ColorAide registers a subset of the offered color spaces by default. But additional color spaces can be registered by subclassing the Color object and then registering any additional required plugins, such as color spaces.

Everything but the Kitchen Sink

It is not generally recommended to register all possible color spaces (and plugins in general). The suggested approach is to cherry pick additional color spaces as needed by simply subclassing Color and then registering the desired plugins, but if desired coloraide.everything.ColorAll already includes all plugins and can be imported to get access to every supported plugin.

"},{"location":"colors/#default-color-spaces","title":"Default Color Spaces","text":"

While ColorAide supports a lot of color spaces, it is rare that a user would ever need every color space implemented by ColorAide available at all times, so to keep the Color object lighter, and color matching logic quicker, the coloraide.Color object does not register all color spaces by default.

Default Color\u00a0Spaces XYZ\u00a0D65 XYZ\u00a0D50 Linear sRGB Linear Display\u00a0P3 Linear A98\u00a0RGB Linear Rec.\u00a02020 Linear ProPhoto\u00a0RGB sRGB Display\u00a0P3 A98\u00a0RGB Rec.\u00a02020 ProPhoto\u00a0RGB HSL HSV HWB Lab LCh Lab\u00a0D65 LCh\u00a0D65 Oklab OkLCh"},{"location":"colors/#color-space-map","title":"Color Space Map","text":"

When registering a plugin, it is important that all required plugins in the conversion path are registered as well. Below we've provided a diagram of all available color spaces and how they translate to one another.

Click any of the color spaces to jump to the related documentation.

flowchart TB\n\n    acescc --- acescg ---- xyz-d65\n        acescct --- acescg\n\n    aces2065-1 --- xyz-d65\n\n    oklch --- oklab ----- xyz-d65\n        okhsl --- oklab\n        okhsv --- oklab\n\n    display-p3 --- display-p3-linear --- xyz-d65\n\n    a98-rgb --- a98-rgb-linear --- xyz-d65\n\n    srgb-linear --- xyz-d65\n        rec709 --- srgb-linear\n        srgb --- srgb-linear\n            cubehelix --- srgb\n            ryb --- srgb\n            orgb --- srgb\n            prismatic --- srgb\n            hsi --- srgb\n            cmy --- srgb\n            cmyk --- srgb\n            xyb --- srgb\n            hsl --- srgb\n                hsv --- hsl\n            hwb --- srgb\n\n    rec2020-linear --- xyz-d65\n        rec2020 --- rec2020-linear\n        rec2100-pq --- rec2020-linear\n        rec2100-hlg --- rec2020-linear\n\n    prophoto-rgb --- prophoto-rgb-linear --- xyz-d50 ----- xyz-d65\n\n    lch --- lab --- xyz-d50\n\n    xyz-d65 --- lab-d65 --- lch-d65\n\n    xyz-d65 --- cam16-jmh --- cam16\n        cam16 --- cam16-ucs\n        cam16 --- cam16-scd\n        cam16 --- cam16-lcd\n\n    xyz-d65 --- hct\n\n    xyz-d65 --- jzazbz --- jzczhz\n\n    xyz-d65 --- ipt\n\n    xyz-d65 --- ictcp\n\n    xyz-d65 --- igpgtg\n\n    xyz-d65 --- din99o --- lch99o\n\n    xyz-d65 --- hunter-lab\n\n    xyz-d65 --- rlab\n\n    xyz-d65 --- luv --- lchuv\n        luv --- hsluv\n        luv --- hpluv\n\n    xyz-d65 --- xyy\n\n    xyz-d65 --- ucs\n\n    xyz-d65(XYZ D65)\n    xyz-d50(XYZ D50)\n    rec2020(Rec. 2020)\n    rec2020-linear(Linear Rec. 2020)\n    rec2100-pq(Rec. 2100 PQ)\n    rec2100-hlg(Rec. 2100 HLG)\n    srgb-linear(Linear sRGB)\n    srgb(sRGB)\n    rec709(Rec. 709)\n    hsl(HSL)\n    hsv(HSV)\n    hwb(HWB)\n    display-p3-linear(Linear Display P3)\n    display-p3(Display P3)\n    a98-rgb-linear(Linear A98 RGB)\n    a98-rgb(A98 RGB)\n    prophoto-rgb-linear(Linear ProPhoto RGB)\n    prophoto-rgb(ProPhoto RGB)\n    lab(Lab)\n    lch(LCh)\n    lab-d65(Lab D65)\n    lch-d65(LCh D65)\n    oklab(Oklab)\n    oklch(OkLCh)\n    okhsl(Okhsl)\n    okhsv(Okhsv)\n    luv(Luv)\n    lchuv(LChuv)\n    hsluv(HSLuv)\n    hpluv(HPLuv)\n    din99o(DIN99o)\n    lch99o(DIN99o LCh)\n    jzazbz(Jzazbz)\n    jzczhz(JzCzhz)\n    ictcp(ICtCp)\n    orgb(oRGB)\n    ipt(IPT)\n    igpgtg(IgPgTg)\n    hunter-lab(Hunter Lab)\n    rlab(RLAB)\n    hsi(HSI)\n    cmy(CMY)\n    cmyk(CMYK)\n    xyy(xyY)\n    ucs(CIE 1960 UCS)\n    prismatic(Prismatic)\n    aces2065-1(ACES2065-1)\n    acescg(ACEScg)\n    acescc(ACEScc)\n    acescct(ACEScct)\n    cam16(CAM16)\n    cam16-jmh(CAM16 JMh)\n    cam16-ucs(CAM16 UCS)\n    cam16-scd(CAM16 SCD)\n    cam16-lcd(CAM16 LCD)\n    hct(HCT)\n    xyb(XYB)\n    ryb(RYB)\n    cubehelix(Cubehelix)\n\n    click xyz-d65 \"./xyz_d65/\" _self\n    click xyz-d50 \"./xyz_d50/\" _self\n    click rec2020 \"./rec2020/\" _self\n    click rec2020-linear \"./rec2020_linear/\" _self\n    click rec2100-pq \"./rec2100_pq/\" _self\n    click rec2100-hlg \"./rec2100_hlg/\" _self\n    click srgb-linear \"./srgb_linear/\" _self\n    click srgb \"./srgb/\" _self\n    click rec709 \"./rec709/\" _self\n    click hsl \"./hsl/\" _self\n    click hsv \"./hsv/\" _self\n    click hwb \"./hwb/\" _self\n    click display-p3-linear \"./display_p3_linear/\" _self\n    click display-p3 \"./display_p3/\" _self\n    click a98-rgb-linear \"./a98_rgb_linear/\" _self\n    click a98-rgb \"./a98_rgb/\" _self\n    click prophoto-rgb-linear \"./prophoto_rgb_linear/\" _self\n    click prophoto-rgb \"./prophoto_rgb/\" _self\n    click lab \"./lab/\" _self\n    click lch \"./lch/\" _self\n    click lab-d65 \"./lab_d65/\" _self\n    click lch-d65 \"./lch_d65/\" _self\n    click oklab \"./oklab/\" _self\n    click oklch \"./oklch/\" _self\n    click okhsl \"./okhsl/\" _self\n    click okhsv \"./okhsv/\" _self\n    click luv \"./luv/\" _self\n    click lchuv \"./lchuv/\" _self\n    click hsluv \"./hsluv/\" _self\n    click hpluv \"./hpluv/\" _self\n    click din99o \"./din99o/\" _self\n    click lch99o \"./lch99o/\" _self\n    click jzazbz \"./jzazbz/\" _self\n    click jzczhz \"./jzczhz/\" _self\n    click ictcp \"./ictcp/\" _self\n    click orgb \"./orgb/\" _self\n    click ipt \"./ipt/\" _self\n    click igpgtg \"./igpgtg/\" _self\n    click hunter-lab \"./hunter_lab/\" _self\n    click rlab \"./rlab/\" _self\n    click hsi \"./hsi/\" _self\n    click cmy \"./cmy/\" _self\n    click cmyk \"./cmyk/\" _self\n    click xyy \"./xyy/\" _self\n    click ucs \"./ucs/\" _self\n    click prismatic \"./prismatic/\" _self\n    click aces2065-1 \"./aces2065_1/\" _self\n    click acescg \"./acescg/\" _self\n    click acescc \"./acescc/\" _self\n    click acescct \"./acescct/\" _self\n    click cam16 \"./cam16/\" _self\n    click cam16-jmh \"./cam16_jmh/\" _self\n    click cam16-ucs \"./cam16_ucs/\" _self\n    click cam16-scd \"./cam16_scd/\" _self\n    click cam16-lcd \"./cam16_lcd/\" _self\n    click hct \"./hct/\" _self\n    click xyb \"./xyb/\" _self\n    click ryb \"./ryb/\" _self\n    click cubehelix \"./cubehelix/\" _self
"},{"location":"colors/a98_rgb/","title":"A98 RGB","text":"

The A98 RGB color space is registered in Color by default

Properties

Name: a98-rgb

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Adobe\u00ae RGB 1998 Chromaticities

The Adobe\u00ae RGB (1998) color space or opRGB is a color space developed by Adobe Systems\u00ae, Inc. in 1998. It was designed to encompass most of the colors achievable on CMYK color printers, but by using RGB primary colors on a device such as a computer display. The Adobe\u00ae RGB (1998) color space encompasses roughly 50% of the visible colors specified by the CIELab color space - improving upon the gamut of the sRGB color space, primarily in cyan-green hues.

A98 RGB is an Adobe\u00ae 98 Compatible color space.

Learn about A98 RGB

"},{"location":"colors/a98_rgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/a98_rgb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(a98-rgb r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"a98-rgb\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(a98-rgb r g b / a) form.

>>> Color('a98-rgb', [0.85859, 0, 0])\ncolor(a98-rgb 0.85859 0 0 / 1)\n>>> Color('a98-rgb', [0.91489, 0.64117, 0.15031]).to_string()\n'color(a98-rgb 0.91489 0.64117 0.15031)'\n
"},{"location":"colors/a98_rgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.a98_rgb import A98RGB\n\nclass Color(Base): ...\n\nColor.register(A98RGB())\n
"},{"location":"colors/a98_rgb_linear/","title":"Linear A98 RGB","text":"

The Linear A98 RGB color space is registered in Color by default

Properties

Name: a98-rgb-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Adobe\u00ae RGB 1998 Chromaticities

The Linear A98 RGB space is the same as A98 RGB except that the transfer function is linear-light (there is no gamma-encoding).

Learn about A98 RGB

"},{"location":"colors/a98_rgb_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/a98_rgb_linear/#inputoutput","title":"Input/Output","text":"

Linear A98 RGB is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --a98-rgb-linear:

color(--a98-rgb-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"a98-rgb-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--a98-rgb-linear r g b / a) form.

>>> Color(\"a98-rgb\", [0.71513, 0, 0])\ncolor(a98-rgb 0.71513 0 0 / 1)\n>>> Color(\"a98-rgb-linear\", [0.82231, 0.37626, 0.01549]).to_string()\n'color(--a98-rgb-linear 0.82231 0.37626 0.01549)'\n
"},{"location":"colors/a98_rgb_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.a98_rgb_linear import A98RGBLinear\n\nclass Color(Base): ...\n\nColor.register(A98RGBLinear())\n
"},{"location":"colors/aces2065_1/","title":"ACES 2065-1","text":"

The ACES 2065-1 color space is not registered in Color by default

Properties

Name: aces2065-1

White Point: D60 / 2\u02da

Coordinates:

Name Range r [0, 65504] g [0, 65504] b [0, 65504]

CIE 1931 xy Chromaticity \u2013 ACES AP0 Chromaticities

ACES 2065-1 is a linear color space that uses a set of primaries known as AP0 and has the widest gamut of all the ACES color spaces and fully encompasses the entire visible spectrum. It is meant primarily as an archival format due to its ability to encapsulate all visible colors. Typically, this is the color space you would use to transfer images/animations between production studios.

While it is considered an RGB color space, it also has enormous dynamic range with channels being able to well exceed the traditional range of 1.

Learn about ACES 2065-1

"},{"location":"colors/aces2065_1/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/aces2065_1/#inputoutput","title":"Input/Output","text":"

ACES 2065-1 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --aces2065-1:

color(--aces2065-1 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"aces2065-1\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--aces2065-1 r g b / a) form.

>>> Color(\"aces2065-1\", [0.43963, 0.08978, 0.01754])\ncolor(--aces2065-1 0.43963 0.08978 0.01754 / 1)\n>>> Color(\"aces2065-1\", [0.58374, 0.39584, 0.05951]).to_string()\n'color(--aces2065-1 0.58374 0.39584 0.05951)'\n
"},{"location":"colors/aces2065_1/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.aces2065_1 import ACES20651\n\nclass Color(Base): ...\n\nColor.register(ACES20651())\n
"},{"location":"colors/acescc/","title":"ACEScc","text":"

The ACEScc color space is not registered in Color by default

Properties

Name: acescc

White Point: D60 / 2\u02da

Coordinates:

Name Range* r [-0.0729,\u00a01.468] g [-0.0729,\u00a01.468] b [-0.0729,\u00a01.468]

* Ranges are approximate and have been rounded.

ACEScc is a color space based on the API primaries and is primarily used for color grading. It is a logarithmic color space, unlike ACEScg, and maps black at 0 and white at 1.

Learn about ACEScc

"},{"location":"colors/acescc/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/acescc/#inputoutput","title":"Input/Output","text":"

ACEScc is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --acescc:

color(--acescc r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"acescc\", [1, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--acescc r g b / a) form.

>>> Color('acescc', [0.51451, 0.33604, 0.23515])\ncolor(--acescc 0.51451 0.33604 0.23515 / 1)\n>>> Color('acescc', [0.53009, 0.48237, 0.32561]).to_string()\n'color(--acescc 0.53009 0.48237 0.32561)'\n
"},{"location":"colors/acescc/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.acescc import ACEScc\n\nclass Color(Base): ...\n\nColor.register(ACEScc())\n
"},{"location":"colors/acescct/","title":"ACEScct","text":"

The ACEScc color space is not registered in Color by default

Properties

Name: acescct

White Point: D60 / 2\u02da

Coordinates:

Name Range* r [-0.3584,\u00a01.468] g [-0.3584,\u00a01.468] b [-0.3584,\u00a01.468]

* Ranges are approximate and rounded to 3 decimal places.

ACEScct is very similar to ACEScc except that it adds a \"toe\" or a gamma curve in the dark region of the color space. This encoding is more appropriate for legacy color correction operators.

Learn about ACEScct

"},{"location":"colors/acescct/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/acescct/#inputsoutput","title":"Inputs/Output","text":"

ACEScct is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --acescct:

color(--acescct r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"acescct\", [1, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--acescct r g b / a) form.

>>> Color(\"acescct\", [0.51451, 0.33604, 0.23515])\ncolor(--acescct 0.51451 0.33604 0.23515 / 1)\n>>> Color(\"acescct\", [0.53009, 0.48237, 0.32561]).to_string()\n'color(--acescct 0.53009 0.48237 0.32561)'\n
"},{"location":"colors/acescct/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.acescct import ACEScct\n\nclass Color(Base): ...\n\nColor.register(ACEScct())\n
"},{"location":"colors/acescg/","title":"ACEScg","text":"

The ACEScg color space is not registered in Color by default

Properties

Name: acescg

White Point: D60 / 2\u02da

Coordinates:

Name Range r [0, 65504] g [0, 65504] b [0, 65504]

CIE 1931 xy Chromaticity \u2013 ACES AP1 Chromaticities

ACEScg is a color space often used by CG artists. It is \"scene-referred\" or linear. It doesn't have as wide a color gamut as ACES 2065-1 as it uses a different set of primaries called AP1, but it is far larger than most other color spaces one might use and has an enormous dynamic range.

Learn about ACEScg

"},{"location":"colors/acescg/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/acescg/#inputoutput","title":"Input/Output","text":"

ACEScg is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --acescg:

color(--acescg r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"acescg\", [1, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--acescg r g b / a) form.

>>> Color(\"acescg\", [0.6131, 0.07019, 0.02062])\ncolor(--acescg 0.6131 0.07019 0.02062 / 1)\n>>> Color(\"acescg\", [0.74085, 0.41498, 0.06184]).to_string()\n'color(--acescg 0.74085 0.41498 0.06184)'\n
"},{"location":"colors/acescg/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.acescg import ACEScg\n\nclass Color(Base): ...\n\nColor.register(ACEScg())\n
"},{"location":"colors/cam16/","title":"CAM16","text":"

The CAM16 color space is not registered in Color by default

Properties

Name: cam16

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-90, 90] b [-90, 90]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 color space.

A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source.

CAM16 is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous different attributes:

Name Description J Lightness C Chroma h hue s saturation Q Brightness M Colorfulness H Hue Quadrature

A color space can be constructed of using a subset of these attributes: JCh, JMh, Jsh, QCh, QMh, Qsh, etc. You can also construct Lab like spaces taking using the hue and either C, M, or s. The cam16 color space in ColorAide represents a Jab configuration based off M (Colorfulness).

Learn more.

"},{"location":"colors/cam16/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16/#inputoutput","title":"Input/Output","text":"

The CAM16 UCS space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16:

color(--cam16 j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16 j a b / a) form.

>>> Color(\"cam16\", [46.026, 72.143, 37.385], 1)\ncolor(--cam16 46.026 72.143 37.385 / 1)\n>>> Color(\"cam16\", [68.056, 13.955, 41.212], 1).to_string()\n'color(--cam16 68.056 13.955 41.212)'\n
"},{"location":"colors/cam16/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16 import CAM16\n\nclass Color(Base): ...\n\nColor.register(CAM16())\n
"},{"location":"colors/cam16_jmh/","title":"CAM16 JMh","text":"

The CAM16 JMh color space is not registered in Color by default

Properties

Name: cam16-jmh

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] m [0, 105] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 JMh color space.

A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source.

CAM16 is a successor of CIECAM02 with various fixes and improvements. The model actually defines numerous different attributes:

Name Description J Lightness C Chroma h hue s saturation Q Brightness M Colorfulness H Hue Quadrature

A color space can be constructed of using a subset of these attributes: JCh, JMh, Jsh, QCh, QMh, Qsh, etc. You can also construct Lab like spaces taking using the hue and either C, M, or s. The cam16-jmh color space in ColorAide represents the JMh configuration.

Learn more.

"},{"location":"colors/cam16_jmh/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness m colorfulness h hue"},{"location":"colors/cam16_jmh/#inputoutput","title":"Input/Output","text":"

The CAM16 JMh space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-jmh:

color(--cam16-jmh j m h / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-jmh j m h / a) form.

>>> Color(\"cam16-jmh\", [59.178, 40.82, 21.153], 1)\ncolor(--cam16-jmh 59.178 40.82 21.153 / 1)\n>>> Color(\"cam16-jmh\", [78.364, 9.6945, 28.629], 1).to_string()\n'color(--cam16-jmh 78.364 9.6945 28.629)'\n
"},{"location":"colors/cam16_jmh/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_jmh import CAM16JMh\n\nclass Color(Base): ...\n\nColor.register(CAM16JMh())\n
"},{"location":"colors/cam16_lcd/","title":"CAM16 LCD","text":"

The CAM16 LCD color space is not registered in Color by default

Properties

Name: cam16-lcd

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-75, 75] b [-75, 75]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 LCD color space.

This is the LCD variant of the CAM16 UCS color space and is optimized for \"large\" color distancing. See CAM16 UCS for more info.

Learn more.

"},{"location":"colors/cam16_lcd/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16_lcd/#inputoutput","title":"Input/Output","text":"

The CAM16 LCD space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-lcd:

color(--cam16-lcd j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-lcd j a b / a) form.

>>> Color(\"cam16-lcd\", [46.026, 81.254, 27.393], 1)\ncolor(--cam16-lcd 46.026 81.254 27.393 / 1)\n>>> Color(\"cam16-lcd\", [68.056, 43.51, 71.293], 1).to_string()\n'color(--cam16-lcd 68.056 43.51 71.293)'\n
"},{"location":"colors/cam16_lcd/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_ucs import CAM16LCD\n\nclass Color(Base): ...\n\nColor.register(CAM16LCD())\n
"},{"location":"colors/cam16_scd/","title":"CAM16 SCD","text":"

The CAM16 SCD color space is not registered in Color by default

Properties

Name: cam16-scd

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-40, 40] b [-40, 40]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 SCD color space.

This is the SCD variant of the CAM16 UCS color space and is optimized for \"small\" color distancing. See CAM16 UCS for more info.

Learn more.

"},{"location":"colors/cam16_scd/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16_scd/#inputoutput","title":"Input/Output","text":"

The CAM16 SCD space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-scd:

color(--cam16-scd j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-scd j a b / a) form.

>>> Color(\"cam16-scd\", [59.178, 33.597, 17.41], 1)\ncolor(--cam16-scd 59.178 33.597 17.41 / 1)\n>>> Color(\"cam16-scd\", [78.364, 8.3723, 24.725], 1).to_string()\n'color(--cam16-scd 78.364 8.3723 24.725)'\n
"},{"location":"colors/cam16_scd/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_ucs import CAM16SCD\n\nclass Color(Base): ...\n\nColor.register(CAM16SCD())\n
"},{"location":"colors/cam16_ucs/","title":"CAM16 UCS","text":"

The CAM16 UCS color space is not registered in Color by default

Properties

Name: cam16-ucs

White Point: D65 / 2\u02da

Coordinates:

Name Range* j [0, 100] a [-50, 50] b [-50, 50]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CAM16 UCS color space.

A color appearance model (CAM) is a mathematical model that seeks to describe the perceptual aspects of human color vision, i.e. viewing conditions under which the appearance of a color does not tally with the corresponding physical measurement of the stimulus source.

The CAM16 model is a successor of CIECAM02 with various fixes and improvements. The CAM16 UCS space takes the CAM16 model and applies an additional nonlinear transformation to lightness and colorfulness so that a color difference metric \u0394E can be based more closely on Euclidean distance. The cam16-ucd color space in ColorAide is based off CAM16 (Jab) which uses M (colorfulness) to derive the a and b values. There are also SCD and LCD variants which optimize the spaces for \"small\" and \"large\" color distancing respectively.

Learn more.

"},{"location":"colors/cam16_ucs/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases j lightness a b"},{"location":"colors/cam16_ucs/#inputoutput","title":"Input/Output","text":"

The CAM16 UCS space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cam16-ucs:

color(--cam16-ucs j a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cam16-ucs j a b / a) form.

>>> Color(\"cam16-ucs\", [59.178, 40.82, 21.153], 1)\ncolor(--cam16-ucs 59.178 40.82 21.153 / 1)\n>>> Color(\"cam16-ucs\", [78.364, 9.6945, 28.629], 1).to_string()\n'color(--cam16-ucs 78.364 9.6945 28.629)'\n
"},{"location":"colors/cam16_ucs/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide_extras.spaces.cam16_ucs import CAM16UCS\n\nclass Color(Base): ...\n\nColor.register(CAM16UCS())\n
"},{"location":"colors/cmy/","title":"CMY","text":"

The CMY color space is not registered in Color by default

Properties

Name: cmy

White Point: D65 / 2\u02da

Coordinates:

Name Range* c [0, 1] m [0, 1] y [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

The sRGB gamut represented within the CMY color space.

The CMY color model is a subtractive color model in which cyan, magenta and yellow pigments or dyes are added together in various ways to reproduce a broad array of colors. The name of the model comes from the initials of the three subtractive primary colors: cyan, magenta, and yellow.

The CMY color space, as ColorAide Extras has chosen to implement it, is directly calculated from the sRGB color space, and as such, is based off the sRGB primaries.

Learn more.

"},{"location":"colors/cmy/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases c cyan m magenta y yellow"},{"location":"colors/cmy/#inputoutput","title":"Input/Output","text":"

CMY is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cmy:

color(--cmy c m y / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cmy c m y / a) form.

>>> Color(\"cmy\", [0, 1, 1])\ncolor(--cmy 0 1 1 / 1)\n>>> Color(\"cmy\", [0, 0.35294, 1]).to_string()\n'color(--cmy 0 0.35294 1)'\n
"},{"location":"colors/cmy/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.cmy import CMY\n\nclass Color(Base): ...\n\nColor.register(CMY())\n
"},{"location":"colors/cmyk/","title":"CMYK","text":"

The CMYK color space is not registered in Color by default

Properties

Name: cmyk

White Point: D65 / 2\u02da

Coordinates:

Name Range* c [0, 1] m [0, 1] y [0, 1] k [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

The CMYK color model is a just like CMY except that it adds an additional channel k to control blackness.

The CMYK color space, as ColorAide Extras has chosen to implement it, is directly calculated from the sRGB color space, and as such, is based off the sRGB primaries.

Learn more.

"},{"location":"colors/cmyk/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases c cyan m magenta y yellow k black"},{"location":"colors/cmyk/#inputoutput","title":"Input/Output","text":"

CMY is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cmyk:

color(--cmyk c m y k / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cmyk c m y k / a) form.

>>> Color(\"cmyk\", [0, 1, 1, 0])\ncolor(--cmyk 0 1 1 0 / 1)\n>>> Color(\"cmyk\", [0, 0.35294, 1, 0]).to_string()\n'color(--cmyk 0 0.35294 1 0)'\n
"},{"location":"colors/cmyk/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.cmyk import CMYK\n\nclass Color(Base): ...\n\nColor.register(CMYK())\n
"},{"location":"colors/cubehelix/","title":"Cubehelix","text":"

The Cubehelix color space is not registered in Color by default

Properties

Name: cubehelix

White Point: D65 / 2\u02da

Coordinates:

Name Range* h [0, 360) s [0, 4.614] l [0, 1]

* The maximum saturation represents how high saturation can go, not that all colors with that saturation will be valid. As seen in the 3D rendering, while the coordinates are cylindrical, the shape of the space is not a cylinder.

The sRGB gamut represented within the Cubehelix color space.

Cubehelix is a color scheme created by Dave Green. It was originally created for the display of astronomical intensity images. It is not really one color scheme, but a method to generate various \"cubehelix\" color schemes. The name comes from the way the colors spiral through the sRGB color space.

Mike Bostock of Observable and D3 fame along with Jason Davies took the color scheme and created a cylindrical color space with it. This is the color space that is implemented in ColorAide.

Cubehelix color schemes can be easily generated by interpolating in the color space.

>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [360, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112e4e290>\n>>> Color.interpolate([c1, c2], space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112dadc50>\n

You can change the scheme by changing the start and end angle.

>>> c1 = Color('cubehelix', [0 + 180, 1, 0])\n>>> c2 = Color('cubehelix', [360 + 180, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1124d1410>\n>>> Color.interpolate([c1, c2], space='cubehelix', hue='longer')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112ed8f50>\n

You can increase the rotations by setting hue interpolation to specified and extending the angle difference to a distance greater than 360.

>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [360 * 3, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1117e74d0>\n>>> Color.interpolate([c1, c2], space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112be8b50>\n

You can even reverse the rotation by utilizing a negative difference in hue.

>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [-360, 1, 1])\n>>> Color.discrete([c1, c2], steps=16, space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112b6f890>\n>>> Color.interpolate([c1, c2], steps=16, space='cubehelix', hue='specified')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112de27d0>\n

To adjust gamma, simply apply a gamma easing to lightness.

>>> def ease_gamma(y=1.0):\n...     \"\"\"Ease gamma.\"\"\"\n... \n...     return lambda t: t ** y\n... \n>>> gamma = ease_gamma(0.2)\n>>> c1 = Color('cubehelix', [0, 1, 0])\n>>> c2 = Color('cubehelix', [-360, 1, 1])\n>>> Color.discrete(\n...     [c1, c2],\n...     steps=16,\n...     space='cubehelix',\n...     hue='specified',\n...     progress={'l': gamma}\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112df2d50>\n>>> Color.interpolate(\n...     [c1, c2],\n...     space='cubehelix',\n...     hue='specified',\n...     progress={'l': gamma}\n... )\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d80b90>\n

Viewing the interpolation in 3D, we can see the spiraling of colors that gave the color scheme the name Cubehelix.

Learn more.

"},{"location":"colors/cubehelix/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/cubehelix/#inputoutput","title":"Input/Output","text":"

The Cubehelix space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --cubehelix:

color(--cubehelix h s l / a)  // Color function\n

The string representation of the color object and the default string output use the color(--cubehelix h s l / a) form.

>>> Color(\"cubehelix\", [351.81, 1.9489, 0.3])\ncolor(--cubehelix 351.81 1.9489 0.3 / 1)\n>>> Color(\"cubehelix\", [36.577, 1.7357, 0.68176]).to_string()\n'color(--cubehelix 36.577 1.7357 0.68176)'\n
"},{"location":"colors/cubehelix/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.cubehelix import Cubehelix\n\nclass Color(Base): ...\n\nColor.register(Cubehelix())\n
"},{"location":"colors/din99o/","title":"DIN99o","text":"

The DIN99o color space is not registered in Color by default

Properties

Name: din99o

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] a [-55, 55] b [-55, 55]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the DIN99o color space.

The DIN99 color space system is a further development of the CIELab color space system developed by the FNF / FNL 2 Colorimetry Working Committee. It takes the CIELab space (with a D65 illuminant) and compresses it such that the space yields better equidistant using Euclidean distance. The whole color space is essentially modified to better fit the color distancing algorithm opposed to CIELab which has adapted the color distancing algorithm to better fit the color space, the latest iteration being \u2206E*00.

Learn about DIN99o

"},{"location":"colors/din99o/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/din99o/#inputoutput","title":"Input/Output","text":"

As DIN99o is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --din99o:

color(--din99o l u v / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"din99o\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--din99o l u v / a) form.

>>> Color(\"din99o\", [57.289, 39.498, 30.518])\ncolor(--din99o 57.289 39.498 30.518 / 1)\n>>> Color(\"din99o\", [77.855, 16.444, 40.318]).to_string()\n'color(--din99o 77.855 16.444 40.318)'\n
"},{"location":"colors/din99o/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.din99o import DIN99o\n\nclass Color(Base): ...\n\nColor.register(DIN99o())\n
"},{"location":"colors/display_p3/","title":"Display P3","text":"

The Display P3 color space is registered in Color by default

Properties

Name: display-p3

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Display P3 Chromaticities

Display P3 is a combination of the DCI-P3 color gamut with the D65 white point together with the sRGB gamma curve. It originated from the DCI-P3 color gamut's implementation in digital cinema projectors, as this standard offers more vibrant greens and reds than the traditional sRGB color gamut.

Learn about Display P3

"},{"location":"colors/display_p3/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/display_p3/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(display-p3 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"display-p3\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(display-p3 r g b / a) form.

>>> Color('display-p3', [0.91749, 0.20029, 0.13856])\ncolor(display-p3 0.91749 0.20029 0.13856 / 1)\n>>> Color('display-p3', [0.94965, 0.6629, 0.23297]).to_string()\n'color(display-p3 0.94965 0.6629 0.23297)'\n
"},{"location":"colors/display_p3/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.display_p3 import DisplayP3\n\nclass Color(Base): ...\n\nColor.register(DisplayP3())\n
"},{"location":"colors/display_p3_linear/","title":"Linear Display P3","text":"

The Linear Display P3 color space is registered in Color by default

Properties

Name: display-p3-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Display P3 Chromaticities

The Linear Display P3 space is the same as Display P3 except that the transfer function is linear-light (there is no gamma-encoding).

Learn about Display P3

"},{"location":"colors/display_p3_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/display_p3_linear/#inputoutput","title":"Input/Output","text":"

Linear Display P3 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --display-p3-linear:

color(--display-p3-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"display-p3-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--display-p3-linear r g b / a) form.

>>> Color(\"display-p3-linear\", [0.82246, 0.03319, 0.01708])\ncolor(--display-p3-linear 0.82246 0.03319 0.01708 / 1)\n>>> Color(\"display-p3-linear\", [0.88926, 0.39697, 0.04432]).to_string()\n'color(--display-p3-linear 0.88926 0.39697 0.04432)'\n
"},{"location":"colors/display_p3_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.display_p3_linear import DisplayP3\n\nclass Color(Base): ...\n\nColor.register(DisplayP3Linear())\n
"},{"location":"colors/hct/","title":"HCT","text":"

The HCT color space is not registered in Color by default

Properties

Name: hct

White Point: D65 / 2\u02da

Coordinates:

Name Range* h [0, 360) c [0, 145] t [0, 100]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the HCT color space.

The HCT color space is Google's attempt at a perceptually accurate color system. Essentially, it is two color spaces glued together: 'H' (hue) and 'C' (chroma) come from the CAM16 color appearance model and 'T' (tone) is the lightness from the CIELAB (D65) color space. The space was created to take the more consistent perceptual hues from CAM16 and use the better lightness prediction found in CIELAB. The color space has the advantage of being well suited for creating color schemes with decent contrast and makes it easy to create nice tonal palettes, but the downside is that it is expensive to translate to and from compared to other color spaces.

Since HCT is partly based on CAM16, it inherits the expensive operations used to translate color to and from the CAM16 color model. In the forward direction (to HCT) color conversions are only marginally more expensive than CAM16, but in the reverse direction (from HCT) the conversions are much more expensive. This is because the CAM16 color model needs the context of chroma, hue, and lightness in order to translate any of its components, but HCT throws away CAM16 lightness and uses CIELAB lightness which has no direct relation to the other components. In order to translate color from HCT, more complex methods are needed to approximate the missing CAM16 lightness in order for a good round trip conversion.

Google implements the HCT color space in their \"Material Color Utilities\" library, but in that library it is restricted to sRGB and only to 8 bit precision. Wide gamut colors such as Display P3 cannot be used.

ColorAide's goal was not to port Material's Color Utilities, but to implement HCT as a proper color space that can be used in sRGB and other wide gamut color spaces. In ColorAide we implement the HCT color space exactly as described and create the space from both CIELAB and CAM16. We then provide a generic approximation back out of HCT at a higher precision to better support not only sRGB, but other wide gamut color spaces such as: Display P3, Rec. 2020, A98 RGB, etc.

Conversion Limitations

Extreme colors, like those in ProPhoto RGB that fall outside the visible spectrum, may be difficult to round trip with the same high accuracy as other colors well inside the visible spectrum. These colors naturally stress the CAM16 color model and make approximation from HCT even more difficult. With that said, most color spaces within the visible spectrum should convert reasonably well.

Learn more.

"},{"location":"colors/hct/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue c chroma t tone, lightness"},{"location":"colors/hct/#inputoutput","title":"Input/Output","text":"

The HCT space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hct:

color(--hct h c t / a)  // Color function\n

The string representation of the color object and the default string output use the color(--hct h c t / a) form.

>>> Color(\"hct\", [27.41, 113.36, 53.237], 1)\ncolor(--hct 27.41 113.36 53.237 / 1)\n>>> Color(\"hct\", [71.257, 60.528, 74.934], 1).to_string()\n'color(--hct 71.257 60.528 74.934)'\n
"},{"location":"colors/hct/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hct import HCT\n\nclass Color(Base): ...\n\nColor.register(HCT())\n
"},{"location":"colors/hct/#tonal-palettes","title":"Tonal Palettes","text":"

One of the applications of HCT is generating tonal palettes. When coupled with ColorAide's \u2206Ehct distancing algorithm and the hct-chroma gamut mapping algorithm, we can produce tonal palettes just like in Material Color Utilities.

>>> c = Color('hct', [325, 24, 50])\n>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n>>> Steps([c.clone().set('tone', tone).convert('srgb').to_string(hex=True, fit='hct-chroma') for tone in tones])\n['#000000', '#29132e', '#3f2844', '#573e5b', '#705574', '#8a6d8d', '#a587a8', '#c1a1c3', '#debcdf', '#fbd7fc', '#ffebfd', '#ffffff']\n

Material Color Utilities, as they currently implement it, only works within the sRGB color space, but ColorAide implements HCT such that it can be used in various wide gamuts as well.

>>> tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n>>> c1 = Color('display-p3', [1, 0, 1]).convert('hct')\n>>> Steps([c1.clone().set('tone', tone).convert('display-p3').to_string(fit='hct-chroma') for tone in tones])\n['color(display-p3 0 0 0)', 'color(display-p3 0.21092 0 0.21039)', 'color(display-p3 0.34434 0 0.34368)', 'color(display-p3 0.48744 0 0.48675)', 'color(display-p3 0.63855 0 0.63794)', 'color(display-p3 0.79657 0 0.79617)', 'color(display-p3 0.96066 0 0.96062)', 'color(display-p3 1 0.42437 0.97127)', 'color(display-p3 1 0.65324 0.95784)', 'color(display-p3 1 0.8358 0.96452)', 'color(display-p3 1 0.92002 0.97366)', 'color(display-p3 1 1 1)']\n>>> c2 = Color('rec2020', [0, 0, 1]).convert('hct')\n>>> Steps([c2.clone().set('tone', tone).convert('rec2020').to_string(fit='hct-chroma') for tone in tones])\n['color(rec2020 0 0 0)', 'color(rec2020 0 0.00078 0.41888)', 'color(rec2020 0 0.00088 0.70689)', 'color(rec2020 0.00881 0.01642 1)', 'color(rec2020 0.15806 0.21748 1)', 'color(rec2020 0.29634 0.36043 1)', 'color(rec2020 0.43177 0.49013 1)', 'color(rec2020 0.5696 0.61616 1)', 'color(rec2020 0.71119 0.74184 1)', 'color(rec2020 0.85694 0.86865 1)', 'color(rec2020 0.93139 0.93271 1)', 'color(rec2020 1 1 1)']\n

Due to differences in approximation techniques, general precision differences, and gamut mapping of the two implementations internally, ColorAide may return colors slightly different from Material Color Utilities. These differences are extremely small and not perceptible to the eye.

Below we have two examples. We've taken the results from Material's tests and we've generated the same tonal palettes and output both as HCT. We can compare which hues stay overall more constant, which chroma gets reduced more than others, and which hue and tone are less affected by the gamut mapping. Can you definitively say that one looks more correct than the other? Can you say there is notable, visual difference?

>>> def tonal_palette(c):\n...     tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100]\n...     return [c.clone().set('tone', tone).fit('srgb', method='hct-chroma') for tone in tones]\n... \n>>> material1 = ['#000000', '#00006e', '#0001ac',\n...              '#0000ef', '#343dff', '#5a64ff',\n...              '#7c84ff', '#9da3ff', '#bec2ff',\n...              '#e0e0ff', '#f1efff', '#ffffff']\n>>> c = Color('blue').convert('hct')\n>>> Steps([x.to_string() for x in tonal_palette(c)])\n['color(--hct 209.55 0 0)', 'color(--hct 282.79 51.624 10.006)', 'color(--hct 282.79 68.208 20.007)', 'color(--hct 282.77 83.8 30.018)', 'color(--hct 282.76 82.348 39.985)', 'color(--hct 282.76 73.352 49.989)', 'color(--hct 282.76 62.073 59.993)', 'color(--hct 282.76 49.091 69.995)', 'color(--hct 282.76 34.775 79.997)', 'color(--hct 282.75 19.213 89.998)', 'color(--hct 282.75 10.819 94.999)', 'color(--hct 209.54 2.8716 100)']\n>>> Steps([Color(x).convert('hct').to_string() for x in material1])\n['color(--hct 209.55 0 0)', 'color(--hct 282.84 51.709 9.9973)', 'color(--hct 282.74 68.127 20.044)', 'color(--hct 282.77 83.756 29.989)', 'color(--hct 282.81 82.297 40.059)', 'color(--hct 282.79 73.236 50.106)', 'color(--hct 283.04 62.214 59.895)', 'color(--hct 282.95 49.257 69.882)', 'color(--hct 282.15 34.694 80.039)', 'color(--hct 282.23 19.146 90.035)', 'color(--hct 282.07 10.786 95.015)', 'color(--hct 209.54 2.8716 100)']\n>>> material2 = ['#000000', '#191a2c', '#2e2f42',\n...              '#444559', '#5c5d72', '#75758b',\n...              '#8f8fa6', '#a9a9c1', '#c5c4dd',\n...              '#e1e0f9', '#f1efff', '#ffffff']\n>>> c['chroma'] = 16\n>>> Steps([x.to_string() for x in tonal_palette(c)])\n['color(--hct 209.55 0 0)', 'color(--hct 282.76 16 10)', 'color(--hct 282.76 16 20)', 'color(--hct 282.76 16 30)', 'color(--hct 282.76 16 40)', 'color(--hct 282.76 16 50)', 'color(--hct 282.76 16 60)', 'color(--hct 282.76 16 70)', 'color(--hct 282.76 16 80)', 'color(--hct 282.76 16 90)', 'color(--hct 282.75 10.819 94.999)', 'color(--hct 209.54 2.8716 100)']\n>>> Steps([Color(x).convert('hct').to_string() for x in material2])\n['color(--hct 209.55 0 0)', 'color(--hct 283.31 16.104 10.01)', 'color(--hct 282.9 16.074 20.073)', 'color(--hct 282.41 16.065 29.927)', 'color(--hct 281.87 16.078 40.112)', 'color(--hct 283.49 16.042 49.94)', 'color(--hct 282.85 16.13 60.103)', 'color(--hct 282.27 16.258 69.938)', 'color(--hct 283.58 16.297 79.937)', 'color(--hct 282.75 15.918 89.933)', 'color(--hct 282.07 10.786 95.015)', 'color(--hct 209.54 2.8716 100)']\n
"},{"location":"colors/hpluv/","title":"HPLuv","text":"

The HPLuv color space is not registered in Color by default

Properties

Name: hpluv

Color CSS ID: --hpluv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) p [0, 100] l [0, 100]

HPLuv color space in 3D

HPLuv is similar to HSLuv but takes as many colors as it can from CIELChuv without distorting the chroma. This ends up reducing the gamut to a subset of the sRGB gamut. In the end, only more pastel colors remain.

Learn about HPLuv

"},{"location":"colors/hpluv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s perpendiculars l lightness"},{"location":"colors/hpluv/#inputsoutput","title":"Inputs/Output","text":"

HPLuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hpluv:

color(--hpluv h p l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hpluv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--hpluv h p l / a) form.

>>> Color(\"hpluv\", [23.881, 100, 53.237])\ncolor(--hpluv 23.881 100 53.237 / 1)\n>>> Color(\"hpluv\", [49.45, 100, 74.934]).to_string()\n'color(--hpluv 49.45 100 74.934)'\n
"},{"location":"colors/hpluv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hpluv import HSPLuv\n\nclass Color(Base): ...\n\nColor.register(HPLuv())\n
"},{"location":"colors/hsi/","title":"HSI","text":"

The HSI color space is not registered in Color by default

Properties

Name: hsi

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] i [0, 1]

The sRGB gamut represented within the HSI color space.

The HSI model is similar to models like HSL and HSV except that it uses I for intensity instead of Lightness or Value. It does not attempt to \"fill\" a cylinder by its definition of saturation leading to a very different look when we plot it.

Learn more.

"},{"location":"colors/hsi/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation i intensity"},{"location":"colors/hsi/#inputoutput","title":"Input/Output","text":"

The HSI space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hsi:

color(--hsi h s i / a)  // Color function\n

The string representation of the color object and the default string output use the color(--hsi h s i / a) form.

>>> Color(\"hsi\", [0, 1, 0.33333])\ncolor(--hsi 0 1 0.33333 / 1)\n>>> Color(\"hsi\", [38.824, 1, 0.54902]).to_string()\n'color(--hsi 38.824 1 0.54902)'\n
"},{"location":"colors/hsi/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsi import HSI\n\nclass Color(Base): ...\n\nColor.register(HSI())\n
"},{"location":"colors/hsl/","title":"HSL","text":"

The HSL color space is registered in Color by default

Properties

Name: hsl

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] l [0, 1]

HSL color space in 3D

HSL is an alternative representations of the RGB color model, designed in the 1970s by computer graphics researchers to more closely align with the way human vision perceives color-making attributes. In these models, colors of each hue are arranged in a radial slice, around a central axis of neutral colors which ranges from black at the bottom to white at the top.

HSL models the way different paints mix together to create color in the real world, with the lightness dimension resembling the varying amounts of black or white paint in the mixture.

Learn about HSL

"},{"location":"colors/hsl/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/hsl/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --hsl:

hsl(h s l / a)          // HSL function\nhsl(h, s, l)            // Legacy HSL function\nhsla(h, s, l, a)        // Legacy HSLA function\ncolor(--hsl h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hsl\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--hsl h s l / a) form, but the default string output will be the hsl(h s l / a) form.

>>> Color(\"hsl\", [0, 1, 0.5])\ncolor(--hsl 0 1 0.5 / 1)\n>>> Color(\"hsl\", [38.824, 1, 0.5], ).to_string()\n'hsl(38.824 100% 50%)'\n>>> Color(\"hsl\", [60, 1, 0.5]).to_string(comma=True)\n'hsl(60, 100%, 50%)'\n>>> Color(\"hsl\", [120, 1, 0.25098]).to_string(color=True)\n'color(--hsl 120 1 0.25098)'\n>>> Color(\"hsl\", [240, 1, 0.5]).to_string(percent=False)\n'hsl(240 100 50)'\n
"},{"location":"colors/hsl/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsl import HSL\n\nclass Color(Base): ...\n\nColor.register(HSL())\n
"},{"location":"colors/hsluv/","title":"HSLuv","text":"

The HSLuv color space is not registered in Color by default

Properties

Name: hsluv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 100] l [0, 100]

HSLuv color space in 3D

HSLuv is a human-friendly alternative to HSL. It was formerly known as \"HUSL\" and is a variation of the CIELChuv color space, where the chroma component is replaced by a saturation component which allows you to span all the available chroma as a percentage. HSLuv is constrained to the sRGB gamut.

Learn about HSLuv

"},{"location":"colors/hsluv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/hsluv/#inputoutput","title":"Input/Output","text":"

HSLuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hsluv:

color(--hsluv h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hsluv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--hsluv h s l / a) form.

>>> Color(\"hsluv\", [12.177, 100, 53.237])\ncolor(--hsluv 12.177 100 53.237 / 1)\n>>> Color(\"hsluv\", [44.683, 100, 74.934]).to_string()\n'color(--hsluv 44.683 100 74.934)'\n
"},{"location":"colors/hsluv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsluv import HSLuv\n\nclass Color(Base): ...\n\nColor.register(HSLuv())\n
"},{"location":"colors/hsv/","title":"HSV","text":"

The HSV color space is registered in Color by default

Properties

Name: hsv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] v [0, 1]

HSV color space in 3D

HSV is a color space similar to the modern RGB and CMYK models. The HSV color space has three components: hue, saturation and value. 'Value' is sometimes substituted with 'brightness' and then it is known as HSB. HSV models how colors appear under light.

Learn about HSV

"},{"location":"colors/hsv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation v value"},{"location":"colors/hsv/#inputoutput","title":"Input/Output","text":"

HSV is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --hsv:

color(--hsv 0 0% 0% / 1)\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hsv\", [0, 0, 0], 1)\n

The string representation of the color object and default string output will always use the color(hsv h s v / a) form.

>>> Color(\"hsv\", [0, 1, 1])\ncolor(--hsv 0 1 1 / 1)\n>>> Color(\"hsv\", [38.824, 1, 1]).to_string()\n'color(--hsv 38.824 1 1)'\n
"},{"location":"colors/hsv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hsv import HSV\n\nclass Color(Base): ...\n\nColor.register(HSV())\n
"},{"location":"colors/hunter_lab/","title":"Hunter Lab","text":"

The Hunter Lab color space is not registered in Color by default

Properties

Name: hunter-lab

White Point: D65 / 2\u02da

Coordinates:

Name Range l [0, 100] a [-210, 210] b [-210, 210]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the Hunter Lab color space.

The Hunter Lab color space, defined in 1948 by Richard S. Hunter, is another color space referred to as \"Lab\". Like CIELab, it was also designed to be computed via simple formulas from the CIE XYZ space, but to be more perceptually uniform than CIE XYZ. Hunter named his coordinates L, a, and b. The CIE named the coordinates for CIELab as L, a, b* to distinguish them from Hunter's coordinates.

Learn more.

"},{"location":"colors/hunter_lab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/hunter_lab/#inputoutput","title":"Input/Output","text":"

The Hunter Lab space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --hunter-lab:

color(--hunter-lab l a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--hunter-lab l a b / a) form.

>>> Color(\"hunter-lab\", [46.113, 82.672, 28.408])\ncolor(--hunter-lab 46.113 82.672 28.408 / 1)\n>>> Color(\"hunter-lab\", [69.407, 23.266, 40.946]).to_string()\n'color(--hunter-lab 69.407 23.266 40.946)'\n
"},{"location":"colors/hunter_lab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hunter_lab import HunterLab\n\nclass Color(Base): ...\n\nColor.register(HunterLab())\n
"},{"location":"colors/hwb/","title":"HWB","text":"

The HWB color space is registered in Color by default

Properties

Name: hwb

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) w [0, 1] b [0, 1]

HWB color space in 3D

HWB is a cylindrical-coordinate representation of points in an RGB color model, similar to HSL and HSV. It was developed by HSV's creator Alvy Ray Smith in 1996 to address some of the issues with HSV. HWB was designed to be more intuitive for humans to use and slightly faster to compute. The first coordinate, H (Hue), is the same as the Hue coordinate in HSL and HSV. W and B stand for Whiteness and Blackness respectively and range from 0-100% (or 0-1). The mental model is that the user can pick a main hue and then \"mix\" it with white and/or black to produce the desired color.

Learn about HWB

"},{"location":"colors/hwb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue w whiteness b blackness"},{"location":"colors/hwb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --hwb:

hwb(h w b / a)          // HWB function\ncolor(--hwb h w b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"hwb\", [0, 0, 100], 1)\n

The string representation of the color object will always default to the color(--hwb h w b / a) form, but the default string output will be the hwb(h s l / a) form.

>>> Color(\"hwb\", [0, 0, 0])\ncolor(--hwb 0 0 0 / 1)\n>>> Color(\"hwb\", [38.824, 0, 0]).to_string()\n'hwb(38.824 0% 0%)'\n>>> Color(\"hwb\", [60, 0, 0]).to_string(percent=False)\n'hwb(60 0 0)'\n>>> Color(\"hwb\", [120, 0, 0]).to_string(color=True)\n'color(--hwb 120 0 0)'\n
"},{"location":"colors/hwb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.hwb import HWB\n\nclass Color(Base): ...\n\nColor.register(HWB())\n
"},{"location":"colors/ictcp/","title":"ICtCp","text":"

The ICtCp color space is not registered in Color by default

Properties

Name: ictcp

White Point: D65 / 2\u02da

Coordinates:

Name Range* i [0, 1] ct [-0.5, 0.5] cp [-0.5, 0.5]

* Space is not bound to the range but is specified to enclose the full range of an HDR BT.2020 gamut and is used to define percentage inputs/outputs.

The sRGB gamut represented within the ICtCp color space.

ICtCp is a color space format with better perceptual uniformity than CIELab and is used as a part of the color image pipeline in video and digital photography systems for high dynamic range (HDR) and wide color gamut (WCG) imagery. It was developed by Dolby Laboratories from the IPT color space by Ebner and Fairchild. It was designed with the intention to replace YCbCr.

Learn about ICtCp

"},{"location":"colors/ictcp/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases i intensity ct tritan cp protan"},{"location":"colors/ictcp/#inputoutput","title":"Input/Output","text":"

As ICtCp is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ictcp:

color(--ictcp i ct cp / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"ictcp\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--ictcp i ct cp / a) form.

>>> Color(\"ictcp\", [0.42785, -0.11574, 0.2788])\ncolor(--ictcp 0.42785 -0.11574 0.2788 / 1)\n>>> Color(\"ictcp\", [0.50497, -0.20797, 0.11077]).to_string()\n'color(--ictcp 0.50497 -0.20797 0.11077)'\n
"},{"location":"colors/ictcp/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ictcp import ICtCp\n\nclass Color(Base): ...\n\nColor.register(ICtCp())\n
"},{"location":"colors/igpgtg/","title":"IgPgTg","text":"

The IgPgTg color space is not registered in Color by default

Properties

Name: ipt

White Point: D65 / 2\u02da

Coordinates:

Name Range ig [0, 1] pg [-1, 1] tg [-1, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the IgPgTg color space.

IgPgTg uses the same structure as IPT, an established hue-uniform color space utilized in gamut mapping applications. While IPT was fit to visual data on the perceived hue, IgPgTg was optimized based on evidence linking the peak wavelength of Gaussian-shaped light spectra to their perceived hues.

Learn more.

"},{"location":"colors/igpgtg/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases ig intensity pg protan tg tritan"},{"location":"colors/igpgtg/#inputoutput","title":"Input/Output","text":"

The IgPgTg space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --igpgtg:

color(--igpgtg ig pg tg / a)  // Color function\n

The string representation of the color object and the default string output use the color(--igpgtg ig pg tg / a) form.

>>> Color(\"igpgtg\", [0.54834, 0.15366, 0.43674])\ncolor(--igpgtg 0.54834 0.15366 0.43674 / 1)\n>>> Color(\"igpgtg\", [0.73238, 0.0397, 0.32108]).to_string()\n'color(--igpgtg 0.73238 0.0397 0.32108)'\n
"},{"location":"colors/igpgtg/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.igpgtg import IgPgTg\n\nclass Color(Base): ...\n\nColor.register(IgPgTg())\n
"},{"location":"colors/ipt/","title":"IPT","text":"

The IPT color space is not registered in Color by default

Properties

Name: ipt

White Point: D65 / 2\u02da

Coordinates:

Name Range* i [0, 1] p [-1, 1] t [-1, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the IPT color space.

Ebner and Fairchild addressed the issue of non-constant lines of hue in their color space dubbed IPT. The IPT color space converts D65-adapted XYZ data (XD65, YD65, ZD65) to long-medium-short cone response data (LMS) using an adapted form of the Hunt-Pointer-Estevez matrix (MHPE(D65)).

The IPT color appearance model excels at providing a formulation for hue where a constant hue value equals a constant perceived hue independent of the values of lightness and chroma (which is the general ideal for any color appearance model, but hard to achieve). It is therefore well-suited for gamut mapping implementations.

Learn more.

"},{"location":"colors/ipt/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases i intensity p protan t tritan

Inputs

The IPT space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ipt:

color(--ipt i p t / a)  // Color function\n

The string representation of the color object and the default string output use the color(--ipt i p t / a) form.

>>> Color(\"ipt\", [0.45616, 0.62086, 0.44282])\ncolor(--ipt 0.45616 0.62086 0.44282 / 1)\n>>> Color(\"ipt\", [0.64877, 0.189, 0.5303]).to_string()\n'color(--ipt 0.64877 0.189 0.5303)'\n
"},{"location":"colors/ipt/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ipt import IPT\n\nclass Color(Base): ...\n\nColor.register(IPT())\n
"},{"location":"colors/jzazbz/","title":"Jzazbz","text":"

The Jzazbz color space is not registered in Color by default

Properties

Name: jzazbz

White Point: D65 / 2\u02da

Coordinates:

Name Range* jz [0, 1] az [-0.5, 0.5] bz [-0.5, 0.5]

* Space is not bound to the range but is specified to enclose the full range of an HDR BT.2020 gamut and is used to define percentage inputs/outputs.

The sRGB gamut represented within the Jzazbz color space.

Jzazbz is a a color space designed for perceptual uniformity in high dynamic range (HDR) and wide color gamut (WCG) applications. Conceptually it is similar to CIELab, but claims the following improvements:

  • Perceptual color difference is predicted by Euclidean distance.
  • Perceptually uniform: MacAdam ellipses of just-noticeable-difference (JND) are more circular, and closer to the same sizes.
  • Hue linearity: changing saturation or lightness has less shift in hue.

Learn about Jzazbz

"},{"location":"colors/jzazbz/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases jz lightness az a bz b"},{"location":"colors/jzazbz/#inputoutput","title":"Input/Output","text":"

As Jzazbz is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --jzazbz:

color(--jzazbz jz az bz / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"jzazbz\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--jzazbz jz az bz / a) form.

>>> Color(\"jzazbz\", [0.13438, 0.11789, 0.11188])\ncolor(--jzazbz 0.13438 0.11789 0.11188 / 1)\n>>> Color(\"jzazbz\", [0.16937, 0.0312, 0.12308]).to_string()\n'color(--jzazbz 0.16937 0.0312 0.12308)'\n
"},{"location":"colors/jzazbz/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.jzazbz import Jzazbz\n\nclass Color(Base): ...\n\nColor.register(Jzazbz())\n
"},{"location":"colors/jzczhz/","title":"JzCzhz","text":"

The JzCzhz color space is not registered in Color by default

Properties

Name: jzczhz

White Point: D65 / 2\u02da

Coordinates:

Name Range jz [0, 1] cz [0, 0.5] hz [0, 360)

* Space is not bound to the range but is specified to enclose the full range of an HDR BT.2020 gamut and is used to define percentage inputs/outputs.

The sRGB gamut represented within the JzCzhz color space.

JzCzhz is the cylindrical form of Jzazbz.

Learn about JzCzhz

"},{"location":"colors/jzczhz/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases jz lightness cz chroma hz hue"},{"location":"colors/jzczhz/#inputoutput","title":"Input/Output","text":"

As JzCzhz is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --jzczhz:

color(--jzczhz jz cz hz / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"jzczhz\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--jzczhz jz cz hz / a) form.

>>> Color(\"jzczhz\", [0.13438, 0.16252, 43.502])\ncolor(--jzczhz 0.13438 0.16252 43.502 / 1)\n>>> Color(\"jzczhz\", [0.16937, 0.12698, 75.776]).to_string()\n'color(--jzczhz 0.16937 0.12698 75.776)'\n
"},{"location":"colors/jzczhz/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.jzczhz import JzCzhz\n\nclass Color(Base): ...\n\nColor.register(JzCzhz())\n
"},{"location":"colors/lab/","title":"LAB D50","text":"

The Lab D50 color space is registered in Color by default

Properties

Name: lab

White Point: D50 / 2\u02da

Coordinates:

Name Range* l [0, 100] a [-125, 125] b [-125, 125]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELab D50 color space.

The CIELab color space also referred to as L*a*b* is a color space defined by the International Commission on Illumination (abbreviated CIE) in 1976. It expresses color as three values: L* for perceptual lightness, and a* and b* for the four unique colors of human vision: red, green, blue, and yellow. CIELab was intended as a perceptually uniform space, where a given numerical change corresponds to similar perceived change in color. While the CIELab space is not truly perceptually uniform, it nevertheless is useful in industry for detecting small differences in color.

Learn about CIELab

"},{"location":"colors/lab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/lab/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --lab:

lab(l a b / a)          // Lab function\ncolor(--lab l a b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lab\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--lab l a b / a) form, but the default string output will be the lab(l a b / a) form.

>>> Color(\"lab\", [54.291, 80.805, 69.891])\ncolor(--lab 54.291 80.805 69.891 / 1)\n>>> Color(\"lab\", [75.59, 27.516, 79.121]).to_string()\n'lab(75.59 27.516 79.121)'\n>>> Color(\"lab\", [97.607, -15.75, 93.394]).to_string(percent=True)\n'lab(97.607% -12.6% 74.715%)'\n>>> Color(\"lab\", [46.278, -47.552, 48.586]).to_string(color=True)\n'color(--lab 46.278 -47.552 48.586)'\n
"},{"location":"colors/lab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lab import Lab\n\nclass Color(Base): ...\n\nColor.register(Lab())\n
"},{"location":"colors/lab_d65/","title":"Lab D65","text":"

The Lab D65 color space is registered in Color by default

Properties

Name: lab-d65

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] a [-130, 130] b [-130, 130]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELab D50 color space.

CIELab D65 is the same as CIELab except it uses a D65 white point.

Learn about CIELab

"},{"location":"colors/lab_d65/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/lab_d65/#inputoutput","title":"Input/Output","text":"

As a D65 variant of CIELab is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lab-d65:

color(--lab-d65 l a b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lab-d65\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lab-d65 l a b / a) form.

>>> Color(\"lab-d65\", [53.237, 80.09, 67.203])\ncolor(--lab-d65 53.237 80.09 67.203 / 1)\n>>> Color(\"lab-d65\", [74.934, 23.927, 78.953]).to_string()\n'color(--lab-d65 74.934 23.927 78.953)'\n
"},{"location":"colors/lab_d65/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lab_d65 import LabD65\n\nclass Color(Base): ...\n\nColor.register(LabD65())\n
"},{"location":"colors/lch/","title":"LCh D50","text":"

The LCh D50 color space is registered in Color by default

Properties

Name: lch

White Point: D50 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 150] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELCh D50 color space.

The \"CIELCh\" space is a color space based on CIELab, which uses the polar coordinates C* (chroma, relative saturation) and h\u00b0 (hue angle, angle of the hue in the CIELab color wheel) instead of the Cartesian coordinates a* and b*. The CIELab lightness L* remains unchanged.

Learn about CIELCh

"},{"location":"colors/lch/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lch/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms. In addition, we also allow the color() function format using the custom name --lch:

lch(l c h / a)          // LCh function\ncolor(--lch l c h / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lch\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--lch l c h / a) form, but the default string output will be the lch(l c h / a) form.

>>> Color(\"lch\", [54.291, 106.84, 40.858])\ncolor(--lch 54.291 106.84 40.858 / 1)\n>>> Color(\"lch\", [75.59, 83.769, 70.824]).to_string()\n'lch(75.59 83.769 70.824)'\n>>> Color(\"lch\", [97.607, 94.712, 99.572]).to_string(percent=True)\n'lch(97.607% 63.141% 99.572)'\n>>> Color(\"lch\", [46.278, 67.984, 134.38]).to_string(color=True)\n'color(--lch 46.278 67.984 134.38)'\n
"},{"location":"colors/lch/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lch import LCh\n\nclass Color(Base): ...\n\nColor.register(LCh())\n
"},{"location":"colors/lch99o/","title":"DIN99o LCh","text":"

The DIN99o LCh color space is not registered in Color by default

Properties

Name: lch99o

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 60] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the DIN99o LCh color space.

DIN99o LCh is the cylindrical form of DIN99o.

Learn about DIN99o LCh

"},{"location":"colors/lch99o/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lch99o/#inputoutput","title":"Input/Output","text":"

As DIN99o LCh is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lch99o:

color(--lch99o jz cz hz / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lch99o\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lch99o jz cz hz / a) form.

>>> Color(\"lch99o\", [57.289, 49.915, 37.692])\ncolor(--lch99o 57.289 49.915 37.692 / 1)\n>>> Color(\"lch99o\", [77.855, 43.543, 67.811]).to_string()\n'color(--lch99o 77.855 43.543 67.811)'\n
"},{"location":"colors/lch99o/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lch99o import LCh99o\n\nclass Color(Base): ...\n\nColor.register(LCh99o())\n
"},{"location":"colors/lch_d65/","title":"LCh D65","text":"

The LCh D65 color space is registered in Color by default

Properties

Name: lch-d65

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 160] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELCh D65 color space.

CIELCh D65 is the same as CIELCh except it uses a D65 white point.

Learn about CIELCh

"},{"location":"colors/lch_d65/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lch_d65/#inputoutput","title":"Input/Output","text":"

As a D65 variant of CIELCh is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lch-d65:

color(--lch-d65 l c h / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lch-d65\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lch-d65 l c h / a) form.

>>> Color(\"lch-d65\", [53.237, 104.55, 40])\ncolor(--lch-d65 53.237 104.55 40 / 1)\n>>> Color(\"lch-d65\", [74.934, 82.499, 73.14]).to_string()\n'color(--lch-d65 74.934 82.499 73.14)'\n
"},{"location":"colors/lch_d65/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lch_d65 import LChD65\n\nclass Color(Base): ...\n\nColor.register(LChD65())\n
"},{"location":"colors/lchuv/","title":"LChuv","text":"

The LCHuv color space is not registered in Color by default

Properties

Name: lchuv

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] c [0, 220] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELChuv color space.

CIELuv is not an intuitive space to work with directly and instead is often converted to cylindrical coordinates with hues represented as degrees and a chroma and lightness channel. The shape of the color space doesn't really change, just how the colors are manipulated.

Learn about CIELChuv

"},{"location":"colors/lchuv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/lchuv/#inputoutput","title":"Input/Output","text":"

As CIELChuv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --lchuv:

color(--lchuv l c h / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"lchuv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--lchuv l c h / a) form.

>>> Color(\"lchuv\", [53.237, 179.04, 12.177])\ncolor(--lchuv 53.237 179.04 12.177 / 1)\n>>> Color(\"lchuv\", [74.934, 105.26, 44.683]).to_string()\n'color(--lchuv 74.934 105.26 44.683)'\n
"},{"location":"colors/lchuv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.lchuv import LChuv\n\nclass Color(Base): ...\n\nColor.register(LChuv())\n
"},{"location":"colors/luv/","title":"Luv","text":"

The Luv color space is not registered in Color by default

Properties

Name: luv

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 100] u [-215, 215] v [-215, 215]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the CIELuv D65 color space.

CIELuv is similar to CIELab as they were both developed in 1976 as perceptually uniform color spaces, both are derived from the color experiments in 1931 that brought us the XYZ color space, and neither are truly perceptually uniform.

The difference between the two comes from their intent. CIELab attempted to create a space that aligned well with human vision. CIELuv, on the other hand, was designed to be an easier-to-compute transformation of the 1931 CIE XYZ color space.

CIELab is more commonly used in subtractive color applications (printed pages, dyes, etc.), while CIELuv is better suited in additive color applications such as display colorimetry (monitors, TVs, etc.).

Learn about CIELuv

"},{"location":"colors/luv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness u v"},{"location":"colors/luv/#inputoutput","title":"Input/Output","text":"

As CIELuv D65 is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --luv:

color(--luv l u v / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"luv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--luv l u v / a) form.

>>> Color(\"luv\", [53.237, 175.01, 37.765])\ncolor(--luv 53.237 175.01 37.765 / 1)\n>>> Color(\"luv\", [74.934, 74.839, 74.014]).to_string()\n'color(--luv 74.934 74.839 74.014)'\n
"},{"location":"colors/luv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.luv import Luv\n\nclass Color(Base): ...\n\nColor.register(Luv())\n
"},{"location":"colors/okhsl/","title":"Okhsl","text":"

The Okhsl color space is not registered in Color by default

Properties

Name: okhsl

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] l [0, 1]

Okhsl color space in 3D

Okhsl is a another color space created by Bj\u00f6rn Ottosson. It is based off his early work and leverages the Oklab color space. The aim was to create a color space that was better suited for being used in color pickers than the current HSL.

Learn about Okhsv

"},{"location":"colors/okhsl/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation l lightness"},{"location":"colors/okhsl/#inputoutput","title":"Input/Output","text":"

Okhsl is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --okhsl:

color(--okhsl h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"okhsl\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--okhsl h s l / a) form.

>>> Color(\"okhsl\", [29.234, 1, 0.56808], 1)\ncolor(--okhsl 29.234 1 0.56808 / 1)\n>>> Color(\"okhsl\", [70.67, 1, 0.75883], 1).to_string()\n'color(--okhsl 70.67 1 0.75883)'\n
"},{"location":"colors/okhsl/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.okhsl import Okhsl\n\nclass Color(Base): ...\n\nColor.register(Okhsl())\n
"},{"location":"colors/okhsv/","title":"Okhsv","text":"

The Okhsv color space is not registered in Color by default

Properties

Name: okhsv

White Point: D65 / 2\u02da

Coordinates:

Name Range h [0, 360) s [0, 1] v [0, 1]

Okhsv color space in 3D

Okhsv is a color space created by Bj\u00f6rn Ottosson. It is based off his early work and leverages the Oklab color space. The aim was to create a color space that was better suited for being used in color pickers than the current HSV.

Learn about Okhsv

??? abstract \"ColorAide Details\"

"},{"location":"colors/okhsv/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases h hue s saturation v value"},{"location":"colors/okhsv/#inputoutput","title":"Input/Output","text":"

Okhsv is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --okhsv:

color(--okhsv h s l / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"okhsv\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output use the color(--okhsv h s l / a) form.

>>> Color(\"okhsv\", [29.234, 1, 1])\ncolor(--okhsv 29.234 1 1 / 1)\n>>> Color(\"okhsv\", [70.67, 1, 1]).to_string()\n'color(--okhsv 70.67 1 1)'\n
"},{"location":"colors/okhsv/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.okhsv import Okhsv\n\nclass Color(Base): ...\n\nColor.register(Okhsv())\n
"},{"location":"colors/oklab/","title":"Oklab","text":"

The Oklab color space is registered in Color by default

Properties

Name: oklab

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 1] a [-0.4, 0.4] b [-0.4, 0.4]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the Oklab color space.

A new perceptual color space that claims to be simple to use, while doing a good job at predicting perceived lightness, chroma and hue. It is called the Oklab color space, because it is an OK Lab color space.

Learn about Oklab

"},{"location":"colors/oklab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/oklab/#inputoutput","title":"Input/Output","text":"

Oklab is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --oklab:

oklab(l a b / a)          // Oklab function\ncolor(--oklab l a b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"oklab\", [0, 0, 0], 1)\n

The string representation of the color object will always default to the color(--oklab l a b / a) form, but the default string output will be the oklab(l a b / a) form.

>>> Color(\"oklab\", [0.62796, 0.22486, 0.12585])\ncolor(--oklab 0.62796 0.22486 0.12585 / 1)\n>>> Color(\"oklab\", [0.79269, 0.05661, 0.16138]).to_string()\n'oklab(0.79269 0.05661 0.16138)'\n>>> Color(\"oklab\", [0.96798, -0.07137, 0.19857]).to_string(percent=True)\n'oklab(96.798% -17.842% 49.643%)'\n>>> Color(\"oklab\", [0.51975, -0.1403, 0.10768]).to_string(color=True)\n'color(--oklab 0.51975 -0.1403 0.10768)'\n
"},{"location":"colors/oklab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.oklab import Oklab\n\nclass Color(Base): ...\n\nColor.register(Oklab())\n
"},{"location":"colors/oklch/","title":"OkLCh","text":"

The OkLCh color space is registered in Color by default

Properties

Name: oklch

White Point: D65 / 2\u02da / 2\u02da

Coordinates:

Name Range* l [0, 1] c [0, 0.4] h [0, 360)

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the OkLCh color space.

OkLCh is the cylindrical form of Oklab.

Learn about OkLCh

"},{"location":"colors/oklch/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness c chroma h hue"},{"location":"colors/oklch/#inputoutput","title":"Input/Output","text":"

Oklab is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --oklch:

oklch(l c h / a)          // OkLCh function\ncolor(--oklch l c h / a)  // Color function\n

The string representation of the color object will always default to the color(--oklch l c h / a) form, but the default string output will be the oklch(l a b / a) form.

>>> Color(\"oklch\", [0.62796, 0.25768, 29.234])\ncolor(--oklch 0.62796 0.25768 29.234 / 1)\n>>> Color(\"oklch\", [0.79269, 0.17103, 70.67]).to_string()\n'oklch(0.79269 0.17103 70.67)'\n>>> Color(\"oklch\", [0.96798, 0.21101, 109.77]).to_string(percent=True)\n'oklch(96.798% 52.753% 109.77)'\n>>> Color(\"oklch\", [0.51975, 0.17686, 142.5]).to_string(color=True)\n'color(--oklch 0.51975 0.17686 142.5)'\n
"},{"location":"colors/oklch/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.oklch import OkLCh\n\nclass Color(Base): ...\n\nColor.register(OkLCh())\n
"},{"location":"colors/orgb/","title":"oRGB","text":"

The oRGB color space is not registered in Color by default

Properties

Name: orgb

White Point: D65 / 2\u02da

Coordinates:

Name Range* l [0, 1] cyb [-1, 1] crg [-1, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

The sRGB gamut represented within the oRGB color space.

A new color model that is based on opponent color theory. Like HSV, it is designed specifically for computer graphics. However, it is also designed to work well for computational applications such as color transfer, where HSV falters. Despite being geared towards computation, oRGB's natural axes facilitate HSV-style color selection and manipulation. oRGB also allows for new applications such as a quantitative cool-to-warm metric, intuitive color manipulations and variations, and simple gamut mapping. This new color model strikes a balance between simplicity and the computational qualities of color spaces such as CIELab.

Learn more.

"},{"location":"colors/orgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l luma cyb crb"},{"location":"colors/orgb/#inputoutput","title":"Input/Output","text":"

The oRGB space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --orgb:

color(--orgb l cyb crb / a)  // Color function\n

The string representation of the color object and the default string output use the color(--orgb l cyb crg / a) form.

>>> Color(\"orgb\", [0.299, 0.00002, 0.99998])\ncolor(--orgb 0.299 2e-05 0.99998 / 1)\n>>> Color(\"orgb\", [0.67882, 0.75654, 0.4464]).to_string()\n'color(--orgb 0.67882 0.75654 0.4464)'\n
"},{"location":"colors/orgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.orgb import oRGB\n\nclass Color(Base): ...\n\nColor.register(oRGB())\n
"},{"location":"colors/prismatic/","title":"Prismatic","text":"

The oRGB color space is not registered in Color by default

Properties

Name: prismatic

White Point: D65 / 2\u02da

Coordinates:

Name Range l [0, 1] r [0, 1] g [0, 1] b [0, 1]

Prismatic Illustrations

The Prismatic model introduces a simple transform of the RGB color cube into a light/dark dimension and a 2D hue. The hue is a normalized (barycentric)triangle with pure red, green, and blue at the vertices, often called the Maxwell Color Triangle. Each cross section of the space is the same barycentric triangle, and the light/dark dimension runs zero to one for each hue so the whole color volume takes the form of a prism.

Learn more.

"},{"location":"colors/prismatic/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness r red g green b blue"},{"location":"colors/prismatic/#inputoutput","title":"Input/Output","text":"

The Prismatic space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --prismatic:

color(--prismatic l r g b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--prismatic l r g b / a) form.

>>> Color(\"prismatic\", [1, 1, 0, 0])\ncolor(--prismatic 1 1 0 0 / 1)\n>>> Color(\"prismatic\", [1, 0.60714, 0.39286, 0], 1).to_string()\n'color(--prismatic 1 0.60714 0.39286 0)'\n
"},{"location":"colors/prismatic/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.prismatic import Prismatic\n\nclass Color(Base): ...\n\nColor.register(Prismatic())\n
"},{"location":"colors/prophoto_rgb/","title":"ProPhoto","text":"

The ProPhoto color space is registered in Color by default

Properties

Name: prophoto-rgb

White Point: D50 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 ProPhoto RGB Chromaticities

The ProPhoto RGB color space, also known as ROMM RGB (Reference Output Medium Metric), is an output referred RGB color space developed by Kodak. It offers an especially large gamut designed for use with photographic output in mind. The ProPhoto RGB color space encompasses over 90% of possible surface colors in the CIE L*a*b* color space, and 100% of likely occurring real-world surface colors documented by Pointer in 1980.

Learn about ProPhoto

"},{"location":"colors/prophoto_rgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/prophoto_rgb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(prophoto-rgb r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"prophoto-rgb\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(prophoto-rgb r g b / a) form.

>>> Color(\"prophoto-rgb\", [0.78951, 0.62329, 0.21172], 1)\ncolor(prophoto-rgb 0.78951 0.62329 0.21172 / 1)\n>>> Color(\"prophoto-rgb\", [0.70225, 0.27572, 0.10355]).to_string()\n'color(prophoto-rgb 0.70225 0.27572 0.10355)'\n
"},{"location":"colors/prophoto_rgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.prophoto_rgb import ProPhotoRGB\n\nclass Color(Base): ...\n\nColor.register(ProPhotoRGB())\n
"},{"location":"colors/prophoto_rgb_linear/","title":"Linear ProPhoto","text":"

The Linear ProPhoto color space is registered in Color by default

Properties

Name: prophoto-rgb-linear

White Point: D50 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 ProPhoto RGB Chromaticities

The Linear ProPhoto space is the same as ProPhoto except that the transfer function is linear-light (there is no gamma-encoding).

Learn about ProPhoto

"},{"location":"colors/prophoto_rgb_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/prophoto_rgb_linear/#inputoutput","title":"Input/Output","text":"

Linear ProPhoto is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --prophoto-rgb-linear:

color(--prophoto-rgb-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"prophoto-rgb-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--prophoto-rgb-linear r g b / a) form.

>>> Color(\"prophoto-rgb-linear\", [0.52928, 0.09837, 0.01688])\ncolor(--prophoto-rgb-linear 0.52928 0.09837 0.01688 / 1)\n>>> Color(\"prophoto-rgb-linear\", [0.6535, 0.42702, 0.06115]).to_string()\n'color(--prophoto-rgb-linear 0.6535 0.42702 0.06115)'\n
"},{"location":"colors/prophoto_rgb_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.prophoto_rgb_linear import ProPhotoRGBLinear\n\nclass Color(Base): ...\n\nColor.register(ProPhotoRGBLinear())\n
"},{"location":"colors/rec2020/","title":"REC. 2020","text":"

The Rec. 2020 color space is registered in Color by default

Properties

Name: rec2020

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Rec. 2020 Chromaticities

ITU-R Recommendation BT.2020, more commonly known by the abbreviations Rec. 2020 or BT.2020, defines various aspects of ultra-high-definition television (UHDTV) with standard dynamic range (SDR) and wide color gamut (WCG), including picture resolutions, frame rates with progressive scan, bit depths, color primaries, RGB and luma-chroma color representations, chroma subsamplings, and an opto-electronic transfer function. The color is used in 4k and 8k UHDTV.

Learn about REC.2020

"},{"location":"colors/rec2020/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2020/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(rec2020 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2020\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(rec2020 r g b / a) form.

>>> Color(\"rec2020\", [0.79198, 0.23098, 0.07376])\ncolor(rec2020 0.79198 0.23098 0.07376 / 1)\n>>> Color(\"rec2020\", [0.86727, 0.64078, 0.18496]).to_string()\n'color(rec2020 0.86727 0.64078 0.18496)'\n
"},{"location":"colors/rec2020/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2020 import Rec2020\n\nclass Color(Base): ...\n\nColor.register(Rec2020())\n
"},{"location":"colors/rec2020_linear/","title":"Linear REC. 2020","text":"

The Linear Rec. 2020 color space is registered in Color by default

Properties

Name: rec2020-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Rec. 2020 Chromaticities

The Linear Rec. 2020 space is the same as Rec. 2020 except that the transfer function is linear-light (there is no gamma-encoding).

Learn about REC.2020

"},{"location":"colors/rec2020_linear/#channel-aliases","title":"Channel Aliases:**","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2020_linear/#inputoutput","title":"Input/Output","text":"

Linear Rec. 2020 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec2020-linear:

color(--rec2020-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2020-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec2020-linear r g b / a) form.

>>> Color(\"rec2020-linear\", [0.6274, 0.0691, 0.01639])\ncolor(--rec2020-linear 0.6274 0.0691 0.01639 / 1)\n>>> Color(\"rec2020-linear\", [0.7513, 0.41509, 0.04951]).to_string()\n'color(--rec2020-linear 0.7513 0.41509 0.04951)'\n
"},{"location":"colors/rec2020_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2020_linear import Rec2020Linear\n\nclass Color(Base): ...\n\nColor.register(Rec2020Linear())\n
"},{"location":"colors/rec2100_hlg/","title":"REC. 2100 HLG","text":"

The Rec. 2100 HLG is not registered in Color by default

Properties

Name: rec2100-hlg

White Point: D65 / 2\u02da

Coordinates:

Name Range r [0, 1] g [0, 1] b [0, 1]

CIE 1931 xy Chromaticity \u2013 Rec. 2100 Chromaticities (Same as Rec. 2020)

BT.2100, more commonly known by the abbreviations Rec. 2100 or BT.2100, introduced high-dynamic-range television (HDR-TV) by recommending the use of the perceptual quantizer (PQ) or hybrid log\u2013gamma (HLG) transfer functions instead of the traditional \"gamma\" previously used for SDR-TV. Rec. 2100 HLG specifically uses the hybrid log-gamma transfer function.

The actual gamut of Rec. 2100 uses the same wide color gamut of Rec. 2020, but the color space itself supports an HDR range.

Learn about REC.2100

"},{"location":"colors/rec2100_hlg/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2100_hlg/#inputoutput","title":"Input/Output","text":"

Rec. 2100 HLG is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec2100-hlg:

color(--rec2100-hlg r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2100-hlg\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec2100-hlg r g b / a) form.

>>> Color(\"rec2100-hlg\", [0.65587, 0.23436, 0.11415], 1)\ncolor(--rec2100-hlg 0.65587 0.23436 0.11415 / 1)\n>>> Color(\"rec2100-hlg\", [0.69294, 0.56608, 0.19838], 1).to_string()\n'color(--rec2100-hlg 0.69294 0.56608 0.19838)'\n
"},{"location":"colors/rec2100_hlg/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2100_hlg import Rec2100HLG\n\nclass Color(Base): ...\n\nColor.register(Rec2100HLG())\n
"},{"location":"colors/rec2100_pq/","title":"REC. 2100 PQ","text":"

The Rec. 2100 PQ is not registered in Color by default

Properties

Name: rec2100-pq

White Point: D65 / 2\u02da

Coordinates:

Name Range r [0, 1] g [0, 1] b [0, 1]

CIE 1931 xy Chromaticity \u2013 Rec. 2100 Chromaticities (Same as Rec. 2020)

BT.2100, more commonly known by the abbreviations Rec. 2100 or BT.2100, introduced high-dynamic-range television (HDR-TV) by recommending the use of the perceptual quantizer (PQ) or hybrid log\u2013gamma (HLG) transfer functions instead of the traditional \"gamma\" previously used for SDR-TV. Rec. 2100 PQ specifically uses the perceptual quantizer.

The actual gamut of Rec. 2100 uses the same wide color gamut of Rec. 2020, but the color space itself supports an HDR range.

Learn about REC.2100

"},{"location":"colors/rec2100_pq/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec2100_pq/#inputoutput","title":"Input/Output","text":"

Rec. 2100 PQ is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec2100-pq:

color(--rec2100-pq r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec2100-pq\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec2100-pq r g b / a) form.

>>> Color(\"rec2100-pq\", [0.53255, 0.32702, 0.22007], 1)\ncolor(--rec2100-pq 0.53255 0.32702 0.22007 / 1)\n>>> Color(\"rec2100-pq\", [0.55101, 0.49099, 0.30009], 1).to_string()\n'color(--rec2100-pq 0.55101 0.49099 0.30009)'\n
"},{"location":"colors/rec2100_pq/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec2100_pq import Rec2100PQ\n\nclass Color(Base): ...\n\nColor.register(Rec2100PQ())\n
"},{"location":"colors/rec709/","title":"Rec. 709","text":"

New 2.4

The Rec. 709 color space is not registered in Color by default

Properties

Name: rec709

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 Rec. 709 Chromaticities

Rec. 709 (also known as Rec.709, BT.709, and ITU 709) is a standard developed by ITU-R for image encoding and signal characteristics of high-definition television (HDTV). The color space is similar to sRGB in the fact that the primary chromaticities and white points are identical, the difference is the transfer function that more resembles Rec. 2020, though the precision of the constants are at 10 bit instead of 12 bit or greater.

Learn about Rec. 709

"},{"location":"colors/rec709/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/rec709/#inputoutput","title":"Input/Output","text":"

Rec. 709 is not supported via the CSS spec and the parser input and string output only supports the color() function format using the custom name --rec709:

color(--rec709 r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"rec709\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(--rec709 r g b / a) form.

>>> Color(\"rec709\", [1, 0, 0], 1)\ncolor(--rec709 1 0 0 / 1)\n>>> Color(\"rec709\", [1, 0.60879, 0], 1).to_string()\n'color(--rec709 1 0.60879 0)'\n
"},{"location":"colors/rec709/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rec709 import Rec709\n\nclass Color(Base): ...\n\nColor.register(Rec709())\n
"},{"location":"colors/rlab/","title":"RLAB","text":"

The RLAB color space is not registered in Color by default

Properties

Name: rlab

White Point: D65 / 2\u02da

Coordinates:

Name Range l [0, 100] a [-125, 125] b [-125, 125]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs in relation to the Display P3 color space.

The sRGB gamut represented within the RLAB color space.

The RLAB color-appearance space was developed by Fairchild and Berns for cross-media color reproduction applications in which images are reproduced with differing white points, luminance levels, and/or surrounds.

ColorAide provides RLAB by default with average surround, and discounting set to \"hard copy\" (or full discounting of the illuminant). It is also configured to use an absolute adapting luminance of 318 cd/m2.

Learn more.

"},{"location":"colors/rlab/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases l lightness a b"},{"location":"colors/rlab/#inputoutput","title":"Input/Output","text":"

The RLAB space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --rlab:

color(--rlab l a b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--rlab l a b / a) form.

>>> Color(\"rlab\", [51.012, 79.742, 57.26])\ncolor(--rlab 51.012 79.742 57.26 / 1)\n>>> Color(\"rlab\", [72.793, 25.151, 74.11]).to_string()\n'color(--rlab 72.793 25.151 74.11)'\n
"},{"location":"colors/rlab/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.rlab import RLAB\n\nclass Color(Base): ...\n\nColor.register(RLAB())\n
"},{"location":"colors/ryb/","title":"RYB","text":"

The RYB color space is not registered in Color by default

Properties

Name: ryb

White Point: D65 / 2\u02da

Coordinates:

Name Range r [0, 1] y [0, 1] b [0, 1]

The RYB color wheel.

The RYB color model is a subtractive color model in which red, yellow and blue pigments or dyes are added together in various ways to reproduce different colors. RYB is the classical way of thinking about colors. While in schools it is often still taught that red, yellow, and blue are the primary colors (for pigments), modern wisdom shows that these \"primaries\" are not sufficient. Spaces like CMYK which use cyan, magenta and yellow can create a much wider array of colors, even red, yellow, and blue. We've also since learned that the primary colors of light (which operates in an additive color space) are red, green, and blue.

Even though in modern day electronic screens and printing we rely on color models such as RGB and CMYK, RYB still holds a special place with artists. Color harmony is often used as the model when talking about color harmonies.

There is no standard RYB color model. No standard primaries. The RYB model that ColorAide implements is based on the work of Nathan Gossett and Baoquan Chen at the University of Minnesota at Twin Cities. Their paper devised an approach of using trilinear interpolation to create a transform from RYB to sRGB. The bases of the model uses colors similar to Johannes Itten's color wheel (which we showed an example of above), but in all the literature, there are variants of the same color wheel and no precise color codes.

ColorAide implements Gossett and Chen's algorithm to convert from RYB to sRGB, but also uses an algorithm that employs Newton's method to approximates the reverse transform. Additionally, their paper implements an easing function that biases the color transforms to corners of the cube. While we've also implemented this behavior to be true to the paper, the color spaces does not utilize this biasing by default, but we provide an alternative version does. The biasing does not improve the color space, but limits the intermediate colors, essentially clumping them near the corners of the RYB color cube which was a requirement for the types of data representations that they were performing.

Notice how the colors in the \"biased\" RYB are more concentrated at the corners while in the normal RYB they blend more.

RYBRYB Biased

While precisely emulating paint mixing was not entirely the focus, the RYB space does do a better job than other color spaces.

>>> Color.interpolate(['color(--ryb 0 0 1)', 'color(--ryb 0 1 0)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112e42290>\n>>> Color.interpolate(['color(--ryb 1 0 0)', 'color(--ryb 0 1 0)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112d85dd0>\n>>> Color.interpolate(['color(--ryb 1 0 1)', 'color(--ryb 0 1 0)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112ba9950>\n

It should also be noted that when mixing all the colors, you do not get black, but a muddy brown, much like with paint. To be precise, the color black is not defined within this color space.

>>> Color.interpolate(['color(--ryb 0 0 0)', 'color(--ryb 1 1 1)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1128f5850>\n>>> Color.interpolate(['color(--ryb 0 0 1)', 'color(--ryb 1 1 1)'], space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112c661d0>\n

It should be noted that the RYB model does a great job at translating colors within the RYB color gamut, but translation of colors outside the gamut will have poor conversions.

Learn more.

"},{"location":"colors/ryb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red y yellow b blue"},{"location":"colors/ryb/#inputoutput","title":"Input/Output","text":"

RYB is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ryb:

color(--ryb r y b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--ryb r y b / a) form.

>>> Color(\"ryb\", [1, 0, 0])\ncolor(--ryb 1 0 0 / 1)\n>>> Color(\"ryb\", [1, 1, 0]).to_string()\n'color(--ryb 1 1 0)'\n
"},{"location":"colors/ryb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ryb import RYB\n\nclass Color(Base): ...\n\nColor.register(RYB())\n
"},{"location":"colors/ryb/#ryb-biased","title":"RYB Biased","text":"

The biased RYB from Gosset and Chen's paper can be used via the ryb-biased color space, CSS custom name --ryb-biased. It has the same channel ranges as ryb, but conversions will be biased to the corners of the RYB cube.

>>> Color.interpolate(Color('ryb', [1, 0, 0]).harmony('wheel', space='ryb'), space='ryb')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x1126fc650>\n>>> Color.interpolate(Color('ryb-biased', [1, 0, 0]).harmony('wheel', space='ryb-biased'), space='ryb-biased')\n<coloraide.interpolate.linear.InterpolatorLinear object at 0x112f5ef50>\n

Space can be registered via:

from coloraide import Color as Base\nfrom coloraide.spaces.ryb import RYBBiased\n\nclass Color(Base): ...\n\nColor.register(RYBBiased())\n
"},{"location":"colors/srgb/","title":"sRGB","text":"

The sRGB color space is registered in Color by default

Properties

Name: srgb

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 sRGB Chromaticities

The sRGB space is a standard RGB (red, green, blue) color space that HP and Microsoft created cooperatively in 1996 to use on monitors, printers, and the Web. sRGB stands for \"Standard RGB\". It is the most widely used color space and is supported by most operating systems, software programs, monitors, and printers.

Learn about sRGB

"},{"location":"colors/srgb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/srgb/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

black                  // Color name\n#RRGGBBAA              // Hex\nrgb(r g b / a)         // RGB function\nrgb(r, g, b)           // Legacy RGB Function\nrgba(r, g, b, a)       // Legacy RGBA function\ncolor(srgb r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"srgb\", [0, 0, 0], 1)\n
The string representation of the color object will always default to the color(srgb r g b / a) form, but the default string output will be the rgb(r g b / a) form.

>>> Color('red').to_string()\n'rgb(255 0 0)'\n>>> Color('orange').to_string(comma=True)\n'rgb(255, 165, 0)'\n>>> Color('yellow').to_string(percent=True)\n'rgb(100% 100% 0%)'\n>>> Color('green').to_string(names=True)\n'green'\n>>> Color('blue').to_string(hex=True)\n'#0000ff'\n>>> Color('indigo').to_string(color=True)\n'color(srgb 0.29412 0 0.5098)'\n
"},{"location":"colors/srgb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.srgb.css import sRGB\n\nclass Color(Base): ...\n\nColor.register(sRGB())\n
"},{"location":"colors/srgb_linear/","title":"Linear sRGB","text":"

The Linear sRGB color space is registered in Color by default

Properties

Name: srgb-linear

White Point: D65 / 2\u02da

Coordinates:

Name Range* r [0, 1] g [0, 1] b [0, 1]

* Range denotes in gamut colors, but the color space supports an extended range beyond the gamut.

CIE 1931 xy Chromaticity \u2013 sRGB Chromaticities

The sRGB Linear space is the same as sRGB except that the transfer function is linear-light (there is no gamma-encoding).

Learn about sRGB

"},{"location":"colors/srgb_linear/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases r red g green b blue"},{"location":"colors/srgb_linear/#inputsoutput","title":"Inputs/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(srgb-linear r g b / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"srgb-linear\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(srgb-linear r g b / a) form.

>>> Color(\"srgb\", [1, 0, 0])\ncolor(srgb 1 0 0 / 1)\n>>> Color(\"srgb\", [1, 0.37626, 0]).to_string()\n'rgb(255 95.946 0)'\n
"},{"location":"colors/srgb_linear/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.srgb_linear import sRGBLinear\n\nclass Color(Base): ...\n\nColor.register(sRGB())\n
"},{"location":"colors/ucs/","title":"CIE 1960 UCS","text":"

New 2.4

The CIE 1960 UCS color space is not registered in Color by default

Properties

Name: ucs

White Point: D65 / 2\u02da

Coordinates:

Name Range* u [0.0, 1.0] v [0.0, 1.0] w [0.0, 1.0]

* Space is not bound to the range and is used to define percentage inputs/outputs.

The sRGB gamut represented within the CIE 1960 UCS color space.

The CIE 1960 color space (\"CIE 1960 UCS\", variously expanded Uniform Color Space, Uniform Color Scale, Uniform Chromaticity Scale, Uniform Chromaticity Space) is another name for the (u, v) chromaticity space devised by David MacAdam. The color space is implemented using the relation between this space and the XYZ space as coordinates U, V, and W.

Learn more.

"},{"location":"colors/ucs/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases u v w"},{"location":"colors/ucs/#inputoutput","title":"Input/Output","text":"

The UCS space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --ucs:

color(--ucs u v w / a)  // Color function\n

The string representation of the color object and the default string output use the color(--ucs u v w / a) form.

>>> Color(\"ucs\", [0.27493, 0.21264, 0.12243])\ncolor(--ucs 0.27493 0.21264 0.12243 / 1)\n>>> Color(\"ucs\", [0.36462, 0.48173, 0.48122]).to_string()\n'color(--ucs 0.36462 0.48173 0.48122)'\n
"},{"location":"colors/ucs/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.ucs import UCS\n\nclass Color(Base): ...\n\nColor.register(UCS())\n
"},{"location":"colors/xyb/","title":"XYB","text":"

The XYB color space is not registered in Color by default

Properties

Name: xyb

White Point: D65 / 2\u02da

Coordinates:

Name Range* x [-0.05, 0.05] y [0.0, 0.845] b [-0.45, 0.45]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the XYB color space.

XYB is a color space that was designed for use with the JPEG XL Image Coding System. It is an LMS-based color model inspired by the human visual system, facilitating perceptually uniform quantization. It uses a gamma of 3 for computationally efficient decoding.

Chroma/Luma Adjustments

Per the creator, the default subtracts the Y component from the B component which makes Y function as lightness and X and Y like Lab a and b. When X=Y=0, the color is achromatic. You may find other implementations do not do this only because it is not documented well.

Learn more.

"},{"location":"colors/xyb/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y b

Inputs

The XYB space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --xyb:

color(--xyb x y b / a)  // Color function\n

The string representation of the color object and the default string output use the color(--xyb x y b / a) form.

>>> Color(\"xyb\", [0.0281, 0.48819, 0.01157])\ncolor(--xyb 0.0281 0.48819 0.01157 / 1)\n>>> Color(\"xyb\", [0.01132, 0.64596, -0.10359]).to_string()\n'color(--xyb 0.01132 0.64596 -0.10359)'\n
"},{"location":"colors/xyb/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyb import XYB\n\nclass Color(Base): ...\n\nColor.register(XYB())\n
"},{"location":"colors/xyy/","title":"xyY","text":"

The xyY color space is not registered in Color by default

Properties

Name: xyy

White Point: D65 / 2\u02da

Coordinates:

Name Range* x [0, 1] y [0, 1] Y [0, 1]

* Space is not bound to the range and is used to define percentage inputs/outputs.

The sRGB gamut represented within the xyY color space.

A derivative of the CIE 1931 XYZ space, the CIE xyY color space, is often used as a way to graphically present the chromaticity of colors.

Tip

The color space, as implemented, is relative to the D65 white point, meaning it is created from XYZ D65. If colors are needed relative to different white points, the color space can be subclassed. If proper chromaticity coordinates are desired for a given color, you can checkout the API for chromaticity coordinates.

Learn more.

"},{"location":"colors/xyy/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y Y"},{"location":"colors/xyy/#inputoutput","title":"Input/Output","text":"

The xyY space is not currently supported in the CSS spec, the parsed input and string output formats use the color() function format using the custom name --xyy:

color(--xyy x y Y / a)  // Color function\n

The string representation of the color object and the default string output use the color(--xyy x y Y / a) form.

>>> Color(\"xyy\", [0.64, 0.33, 0.21264])\ncolor(--xyy 0.64 0.33 0.21264 / 1)\n>>> Color(\"xyy\", [0.50047, 0.4408, 0.48173]).to_string()\n'color(--xyy 0.50047 0.4408 0.48173)'\n
"},{"location":"colors/xyy/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyy import xyY\n\nclass Color(Base): ...\n\nColor.register(xyY())\n
"},{"location":"colors/xyz_d50/","title":"XYZ D50","text":"

The XYZ D50 color space is registered in Color by default

Properties

Name: xyz-d50

White Point: D50 / 2\u02da

Coordinates:

Name Range* x [0, 1] y [0, 1] z [0, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the XYZ D50 color space.

XYZ D50 is the same as XYZ D65 except it uses a D50 white point.

Learn about XYZ

"},{"location":"colors/xyz_d50/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y z"},{"location":"colors/xyz_d50/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats support all valid CSS forms:

color(xyz-d50 x y z / a)  // Color function\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"xyz-d50\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(xyz x y z / a) form.

>>> Color(\"xyz-d50\", [0.43607, 0.22249, 0.01392])\ncolor(xyz-d50 0.43607 0.22249 0.01392 / 1)\n>>> Color(\"xyz-d50\", [0.58098, 0.49223, 0.05045]).to_string()\n'color(xyz-d50 0.58098 0.49223 0.05045)'\n
"},{"location":"colors/xyz_d50/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyz_d50 import XYZD50\n\nclass Color(Base): ...\n\nColor.register(XYZD50())\n
"},{"location":"colors/xyz_d65/","title":"XYZ D65","text":"

The XYZ D65 color space is registered in Color by default

Properties

Name: xyz-d65

White Point: D65 / 2\u02da

Coordinates:

Name Range* x [0, 1] y [0, 1] z [0, 1]

* Space is not bound to the range and is only used as a reference to define percentage inputs/outputs.

The sRGB gamut represented within the XYZ D65 color space.

The CIE 1931 RGB color space and CIE 1931 XYZ color space were created by the International Commission on Illumination (CIE) in 1931. They resulted from a series of experiments done in the late 1920s by William David Wright using ten observers and John Guild using seven observers. The experimental results were combined into the specification of the CIE RGB color space, from which the CIE XYZ color space was derived. The CIE 1931 color spaces are the first defined quantitative links between distributions of wavelengths in the electromagnetic visible spectrum, and physiologically perceived colors in human color vision.

Learn about XYZ

"},{"location":"colors/xyz_d65/#channel-aliases","title":"Channel Aliases","text":"Channels Aliases x y z"},{"location":"colors/xyz_d65/#inputoutput","title":"Input/Output","text":"

Parsed input and string output formats use the color() format with either xyz-d65 or xyz as the identifier with the latter being an alias of the former.

color(xyz x y z / a)      // Color function\ncolor(xyz-d65 x y z / a)  // Color function alternate name\n

When manually creating a color via raw data or specifying a color space as a parameter in a function, the color space name is always used:

Color(\"xyz-d65\", [0, 0, 0], 1)\n

The string representation of the color object and the default string output will be in the color(xyz-d65 x y z / a) form.

>>> Color(\"xyz-d65\", [0.41239, 0.21264, 0.01933])\ncolor(xyz-d65 0.41239 0.21264 0.01933 / 1)\n>>> Color(\"xyz-d65\", [0.54694, 0.48173, 0.06418]).to_string()\n'color(xyz-d65 0.54694 0.48173 0.06418)'\n
"},{"location":"colors/xyz_d65/#registering","title":"Registering","text":"
from coloraide import Color as Base\nfrom coloraide.spaces.xyz_d65 import XYZD65\n\nclass Color(Base): ...\n\nColor.register(XYZD65())\n
"},{"location":"demos/","title":"ColorAide Demos","text":""},{"location":"demos/#online-color-picker","title":"Online Color Picker","text":"

Use ColorAide to pick a color in any of the color spaces available. ACES color spaces have been arbitrarily limited has they have ginormous headroom.

Try it out

"},{"location":"demos/#interactive-3d-color-space-models","title":"Interactive 3D Color Space Models","text":"

Generate interactive 3D color models in the browser using ColorAide and Plotly! Most color spaces are supported, but color spaces with more than 3 color components (not including alpha) are not supported. Colors can be generated in a number of color gamuts, though a few models are restricted to their own color space for practical reasons.

Try it out

"},{"location":"plugins/","title":"ColorAide Plugins","text":"

ColorAide implements extendable portions of the Color object as plugins. This makes adding things such as new \u2206E methods or even new color spaces quite easy. Currently, ColorAide implements the following areas as plugins:

  • \u2206E methods
  • Fit/Gamut mapping
  • Chromatic adaptation
  • Filters
  • Contrast
  • Color spaces
  • Interpolation
  • CCT

While these documents will touch on each plugin, looking at the source code will provide a better view on how plugins are actually used as all functionality for all of these categories are implemented as plugins in ColorAide.

"},{"location":"plugins/cat/","title":"Chromatic Adaptation","text":""},{"location":"plugins/cat/#description","title":"Description","text":"

CAT plugins chromatically adapt a given XYZ coordinate from its current reference white point to a new desired white point. This is useful during conversion when one color space is converted to another color space that uses a difference reference white.

"},{"location":"plugins/cat/#plugin-class","title":"Plugin Class","text":"

Plugins are are created by subclassing coloraide.cat.CAT.

class CAT(Plugin, metaclass=ABCMeta):\n\"\"\"Chromatic adaptation.\"\"\"\n\n    NAME = \"\"\n\n    @abstractmethod\n    def adapt(self, w1: Tuple[float, float], w2: Tuple[float, float], xyz: VectorLike) -> Vector:\n\"\"\"Adapt a given XYZ color using the provided white points.\"\"\"\n

Once registered, the plugin can then be used via chromatic_adaptation by passing its NAME via the the method along two white points (as XYZ values): w1 as the current white point and w2 as the target white point.

It should be noted that chromatic_adaptation is not usually directly used by the user, so a more likely approach is to override the DELTA_E parameter of a subclassed Color object to specify the plugin as the default for chromatic adaptation.

"},{"location":"plugins/cat/#von-kries-cat","title":"Von Kries CAT","text":"

Currently, ColorAide only ships with Von Kries based adaptation methods. If it is desired to create a Von Kries based plugin, it is recommended to subclass the VonKries class which is based on CAT. When subclassing a VonKries based CAT, the NAME and a MATRIX must be provided. The general calculations related to the source and target white point, will automatically be calculated and an appropriate matrix and inverted matrix will be returned to perform the adaptation without any additional logic.

class Bradford(VonKries):\n\"\"\"\n    Bradford CAT.\n\n    http://brucelindbloom.com/Eqn_ChromAdapt.html\n    https://hrcak.srce.hr/file/95370\n    \"\"\"\n\n    NAME = \"bradford\"\n\n    MATRIX = [\n        [0.8951000, 0.2664000, -0.1614000],\n        [-0.7502000, 1.7135000, 0.0367000],\n        [0.0389000, -0.0685000, 1.0296000]\n    ]\n
"},{"location":"plugins/cct/","title":"CCT","text":""},{"location":"plugins/cct/#description","title":"Description","text":"

CCT plugins allow you to calculate a color from a CCT and \u2206uv or calculate a CCT and \u2206uv from a color.

"},{"location":"plugins/cct/#plugin-class","title":"Plugin Class","text":"
class CCT(Plugin, metaclass=ABCMeta):\n\"\"\"Delta E plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def to_cct(self, color: Color, **kwargs: Any) -> Vector:\n\"\"\"Calculate a color's CCT.\"\"\"\n\n    @abstractmethod\n    def from_cct(\n        self,\n        color: type[Color],\n        space: str,\n        kelvin: float,\n        duv: float,\n        scale: bool,\n        scale_space: str | None,\n        **kwargs: Any\n    ) -> Color:\n\"\"\"Calculate a color that satisfies the CCT.\"\"\"\n

Once registered, the plugin can then be used via the cct() or blackbody() methods by passing its NAME via the method parameter.

Color('orange').cct(method=NAME, **kwargs)\nColor.blackbody(space, kelvin, duv, method=NAME, **kwargs)\n

If you'd like the user to configure the plugin on registration, you can define an __init__ method. To register the plugin, just the register() method.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/contrast/","title":"Contrast","text":""},{"location":"plugins/contrast/#description","title":"Description","text":"

Contrast returns a numerical value that is meant to determine how much visual contrast exists between two colors. While the current default is agnostic to ordering of the colors, some algorithms can be sensitive to order.

"},{"location":"plugins/contrast/#plugin-class","title":"Plugin Class","text":"
class ColorContrast(Plugin, metaclass=ABCMeta):\n\"\"\"Color contrast plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def contrast(self, color1: 'Color', color2: 'Color', **kwargs: Any) -> float:\n\"\"\"Get the contrast of the two provided colors.\"\"\"\n

Once registered, the plugin can then be used via contrast by passing its NAME via the method parameter along with a secondary color (color2) representing the background color. The calling color (color1) will be considered the text color. Any additional key word arguments can be specified to override default behavior. The \"contrast\" will be returned as a float.

color1.contrast(color2, method=NAME, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/delta_e/","title":"Delta E","text":""},{"location":"plugins/delta_e/#description","title":"Description","text":"

\u2206E plugins allow for getting color differences with different methods. ColorAide provides a number of methods by default which are documented under Color Distance and Delta E. All of the default \u2206E methods are provided as plugins, and users can create their own as well.

"},{"location":"plugins/delta_e/#plugin-class","title":"Plugin Class","text":"

\u2206E plugins are subclassed from coloraide.distance.DeltaE.

class DeltaE(Plugin, metaclass=ABCMeta):\n\"\"\"Delta E plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def distance(self, color: 'Color', sample: 'Color', **kwargs: Any) -> float:\n\"\"\"Get distance between color and sample.\"\"\"\n

Once registered, the plugin can then be used via delta_e by passing its NAME via the method parameter along with any additional key word arguments to override default behavior. The return will be a float indicating the distance.

color.delta_e(sample, method=NAME, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/filter/","title":"Filters","text":""},{"location":"plugins/filter/#description","title":"Description","text":"

Filter plugins allow you to apply a filter to a given color, altering its appearance.

"},{"location":"plugins/filter/#plugin-class","title":"Plugin Class","text":"
class Filter(Plugin, metaclass=ABCMeta):\n\"\"\"Filter plugin.\"\"\"\n\n    NAME = \"\"\n    DEFAULT_SPACE = 'srgb-linear'\n    ALLOWED_SPACES = ('srgb-linear', 'srgb')\n\n    @abstractmethod\n    def filter(self, color: 'Color', amount: float | None, **kwargs: Any) -> None:\n\"\"\"Filter the given color.\"\"\"\n

Once registered, the plugin can then be used via filter by passing its NAME via the method parameter along with the amount specifying to what magnitude the filter is applied. Any additional key word arguments also be specified to allow for overriding default behaviors. The current color will then be altered by the filter.

DEFAULT_SPACE describes the default color space under which the filter is applied, while ALLOWED_SPACES defines optional, allowed spaces that can be specified. Color space conversion is handled before passing the color to the plugin.

color.filter(NAME, amount, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n
"},{"location":"plugins/fit/","title":"Fit/Gamut Mapping","text":""},{"location":"plugins/fit/#description","title":"Description","text":"

Fit plugins (or gamut mapping plugins) allow for mapping an out of gamut color to be within the current color space's gamut. All default gamut mapping methods provided by ColorAide are provided via plugins.

"},{"location":"plugins/fit/#plugin-class","title":"Plugin Class","text":"

Plugins are are created by subclassing coloraide.gamut.Fit.

class Fit(Plugin, metaclass=ABCMeta):\n\"\"\"Fit plugin class.\"\"\"\n\"\"\"Fit plugin class.\"\"\"\n\n    NAME = ''\n\n    @abstractmethod\n    def fit(self, color: 'Color', **kwargs) -> None:\n\"\"\"Get coordinates of the new gamut mapped color.\"\"\"\n

Once registered, the plugin can then be used via fit (and in some places like convert) by passing its NAME via the method parameter along with any additional key word arguments to override default behavior. The current color will be gamut mapped accordingly.

color.fit(method=NAME, **kwargs)\n

If you'd like the user to be able to set specific defaults, you can define an __init__ method and manage defaults accordingly. Defaults can be passed in when instantiating a new plugin.

Color.register(Plugin(**kwargs))\n

Reserved Name

clip is a special, reserved name and the associated plugin cannot be overridden. Another clip plugin can be written, but it cannot override the original.

"},{"location":"plugins/interpolate/","title":"Interpolation","text":""},{"location":"plugins/interpolate/#description","title":"Description","text":"

Interpolation plugins allow for interpolation between one or more colors. All interpolation in ColorAide is provided via plugins.

"},{"location":"plugins/interpolate/#plugin-class","title":"Plugin Class","text":"

Plugins are are created by subclassing coloraide.interpolate.Interpolate.

class Interpolate(Plugin, metaclass=ABCMeta):\n\"\"\"Interpolation plugin.\"\"\"\n\n    NAME = \"\"\n\n    @abstractmethod\n    def interpolator(\n        self,\n        coordinates: List[Vector],\n        channel_names: Sequence[str],\n        create: Type['Color'],\n        easings: List[Callable[..., float] | None],\n        stops: Dict[int, float],\n        space: str,\n        out_space: str,\n        progress: Union[Mapping[str, Callable[..., float]], Callable[..., float]] | None,\n        premultiplied: bool,\n        extrapolate: bool = False,\n        domain: List[float] | None = None,\n        **kwargs: Any\n    ) -> Interpolator:\n\"\"\"Get the interpolator object.\"\"\"\n

Once registered, the plugin can then be used via interpolate, steps, or mix by passing its NAME via the method parameter along with any additional key word arguments to override default behavior. An Interpolator object will be returned which allows for interpolating between the given list of colors.

color.interpolate(colors, method=NAME)\n

In general, the Interpolate plugin is mainly a wrapper to ensure the interpolation setup uses an appropriate Interpolator object which does the actual work. An interpolation plugin should derive their Interpolator class from coloraide.interpolate.Interpolator. While we won't show all the methods of the class, we will show the two functions that must be defined.

class Interpolator(metaclass=ABCMeta):\n\"\"\"Interpolator.\"\"\"\n\n    @abstractmethod\n    def setup(self) --> None:\n\"\"\"Setup.\"\"\"\n\n    @abstractmethod\n    def interpolate(\n        self,\n        point: float,\n        index: int,\n    ) -> Vector:\n\"\"\"Interpolate.\"\"\"\n

__init__ usually shouldn't be changed as it handles the general initialization for all interpolations. It could be extended with super() to set some class specific initialization flags for specific features, but generally, interpolation specific setup logic should be done in Interpolator.setup(). This is often used to restructure data points to a more agreeable format for a given interpolation method, precalculate premultiplication, or normalize undefined values when required. There are cases where ColorAide may update data points and re-call setup() directly. As an example. setup() can be recalled when a continuous interpolation is converted to a discretized one.

Interpolator.interpolate is where the actual interpolation takes place. It expects an index from [1, n], the index referencing the second color out of the two colors to be interpolated. point, usually between [0, 1], represents the point on the interpolation line between the two colors under evaluation.

While point is usually a value between [0, 1], where 0 would be the color stop to the left, and 1 would be the color stop to the right, if point exceeds the range of [0, 1], it can be assumed that the request is on the far left or far right of all color stops and could be beyond the absolute range of the entire color interpolation chain.

By default, extrapolation is disabled between all colors in an interpolation chain, and any point that exceeds the range of [0, 1], before easing functions are applied, will be clamped. If extrapolate is set to $!py True, the points will not be clamped between any colors, in which case, it is an easing functions responsibility to ensure a value between 0 or 1 if extreme values are not desired.

Check out the source code to see some example plugins.

"},{"location":"plugins/space/","title":"Color Space","text":""},{"location":"plugins/space/#description","title":"Description","text":"

All color spaces supported by ColorAide are specified via color space plugins. These Space objects specify color channel properties, gamut bounds, input matching/parsing logic, string output logic, conversion to and from a specified base color, etc.

Color space plugins are a little more complex compared to Delta E, Fit, and other plugins.

"},{"location":"plugins/space/#plugin-class","title":"Plugin Class","text":"

In general, a color space plugin is created by subclassing from coloraide.spaces.Space. When defining a color space, there are a couple things that must be defined. Using XYZ as an example, we will go over them.

from coloraide import cat\nfrom coloraide.channels import Channel\n\n\nclass XYZD65(Space):\n\"\"\"XYZ D65 class.\"\"\"\n\n    # A base color though which a color is converted through.\n    # XYZ is our absolute base, so it doesn't have a real base,\n    # but something like HSL might have a base color of `srgb`.\n    BASE = \"xyz-d65\"\n\n    # The name of the color space.\n    NAME = \"xyz-d65\"\n\n    # One or more accepted identifiers that are allowed for the `color(space ...)` format.\n    # For this this specific color space, both `color(xyz x y z / a)` and `color(xyz-d65 x y z / a)` are accepted.\n    # As `xyz` is listed first, `xyz` is the default used when printing in this format.\n    SERIALIZE = (\"xyz-d65\", \"xyz\")\n\n    # Specify channel attributes, bounds, etc. of the non-alpha color channels.\n    # Each channel is defined via a `Channel` object\n    #\n    #```\n    #class Channel(str):\n    #    \"\"\"Channel.\"\"\"\n    #\n    #    def __new__(\n    #        cls,\n    #        name: str,\n    #        low: float,\n    #        high: float,\n    #        bound: bool = False,\n    #        flags: int = 0,\n    #        limit: Tuple[float | None, float | None] = (None, None),\n    #        nans: float = 0.0\n    #    ) -> 'Channel':\n    #```\n    #\n    # - `name`: The name of the channel.\n    # - `low`: Lower limit of the channel, for unbound channels, the value will be arbitrary.\n    # - `high`: Upper limit of the channel, for unbound channels, the value will be arbitrary.\n    # - `bound`: Whether the channel enforces the gamut range.\n    # - `limit`: Optional upper and lower limit. Used to define a hard limit for the channel that is clamped\n    #            when the channel is set. This differs from gamut boundaries which can be exceeded until gamut\n    #            mapping occurs. For instance, `chroma` often enforces no values below zero as these values\n    #            do not naturally occur, not even with normal out of gamut colors. So, we could clamp the lower\n    #            bound: `(0, None)`.\n    # - `flags`: Flags used to provide additional context for the channel.\n    # - `nans`: Default value to use for a given channel when it is undefined. More advanced handling can be\n    #           done by overriding `resolve_channel()` on the color space object.\n    #\n    # The following flags are supported:\n    # - FLG_ANGLE: denotes that channel is a angle or degree value.\n    # - FLG_PERCENT: denotes the value is considered a percent input. This is usually used in named CSS functions\n    #                like `hsl()` which require string inputs for saturation and lightness to always be in a\n    #                percentage format. The CSS `color()` function ignores this flags as no channels are always\n    #                required to be percentages. Percentage range will be determined by `high` and `low`.\n    # - FLG_OPT_PERCENT: denotes the value can optionally be considered as a percent.\n    #                    This is also only used for CSS string input and output. CSS `oklab()`, `lab()`, `oklch()`,\n    #                    `lch()`, and `srgb()` allow for channels to be provided as percentages or normal\n    #                    numbers in certain cases. This tells the parser and serializer which channels allow this.\n    #                    Percentage range will be determined by `high` and `low`.\n    # - FLG_MIRROR_PERCENT: The channel, when importing or exporting to a percent should mirror the percentage\n    #                       for negative values. This is used mainly in Lab and Lab like spaces which have `a`\n    #                       `b` channels that allow for both negative and positive values. If set, `high` and `low`\n    #                       should fulfill `abs(low) == high`.\n    CHANNELS = (\n        Channel(\"x\", 0.0, 1.0),\n        Channel(\"y\", 0.0, 1.0),\n        Channel(\"z\", 0.0, 1.0)\n    )\n\n    # A dictionary containing a mapping of aliases to `name` attribute of `CHANNELS` found above.\n    CHANNEL_ALIASES = {}\n\n    # If you'd like this color space to parse as and export a `color(space ...)` format.\n    # If set to `False` the space will not recognize the color format as an input.\n    # This only affects input matching. To override output of the color format, you will also\n    # need to override the `to_string` method.\n    COLOR_FORMAT = True\n\n    # Specify the white point that the color space uses\n    # White point should be a `tuple` containing the x and y chromaticity points.\n    # Some basic ones are provided in the `cat` module for both 2 degree and 10 degree observer.\n    WHITE = cat.WHITES['2deg']['D65']\n\n    # If `GAMUT_CHECK` is set to a color space name, the provided color space will be used to verify the an \"in gamut\"\n    # check in addition to the current color space's channel ranges. This is often used with color spaces such as:\n    # HSL, HSV, and HWB where `GAMUT_CHECK` will be set to `srgb`.\n    #\n    # Gamut checking:\n    #   The specified color space will be checked first followed by the original. Assuming the parent color space fits,\n    #   the original should fit as well, but there are some cases when a parent color space that is slightly out of\n    #   gamut, when evaluated with a threshold, may appear to be in gamut enough, but when checking the original color\n    #   space, the values can be greatly out of specification (looking at you HSL).\n    GAMUT_CHECK = None\n\n    # What is the color space's dynamic range\n    DYNAMIC_RANGE = 'sdr'\n\n    ############################\n    # To and from conversion functions that transform the color to and from the `BASE` color.\n    ############################\n    def to_base(self, coords: Vector) -> Vector:\n\"\"\"\n        To XYZ (no change).\n\n        Any needed chromatic adaptation is handled in the parent Color object.\n        \"\"\"\n\n        return coords\n\n    def from_base(self, coords: Vector) -> Vector:\n\"\"\"\n        From XYZ (no change).\n\n        Any needed chromatic adaptation is handled in the parent Color object.\n        \"\"\"\n\n        return coords\n

Once registered, colors can be created using the NAME via normal instantiation methods or conversions:

Color(NAME, [...])\nColor(red).convert(NAME)\n

By default, assuming COLOR_FORMAT is True, color strings will be parsed in the following format, where SERIALIZE is one one of the IDs specified via the SERIALIZE plugin property.

Color('color(SERIALIZE ...)')\n
"},{"location":"plugins/space/#plugin-defaults","title":"Plugin Defaults","text":"

It is important to note that color space plugins are often not isolated. They are convert to from some BASE color and may be a BASE color for some other color space. Essentially, color spaces are chained together via the BASE property to ensure proper conversion to and from the color space. Because of this, it is not advisable to have any configurable defaults that would fundamentally change how the color coordinates are calculated, as such a change could affect not only the targeted color space, but other color spaces up and down the color conversion change.

If configuration of a color space's fundamental calculations of coordinates is desired, it is recommended that the given Space plugin gets subclassed and provided a new NAME, along with SERIALIZE IDs that do not conflict with other spaces. Such changes would include changing a white point, changing viewing conditions, and even changing the algorithm for color space conversion.

Defaults can be provided and configured via an __init__ method, but it is strongly recommended that only superficial things are controlled by such options, like controlling recognized input/output string formats.

Additionally, if provided an __init__, it is required that super().__init__() also gets called.

"},{"location":"plugins/space/#chromatic-adaptation","title":"Chromatic Adaptation","text":"

Chromatic adaptation is usually applied to a color when it is passing from one XYZ color space to another XYZ color space that has a different white point. In ColorAide, any time XYZ D65 is either the target or origin color, and the other color space has a different white point, the XYZ coordinates, will either be adapted to XYZ D65 or XYZ (new white point) respectively. This all happens without The Space plugin needing to do anything additional.

White points are specified via the WHITE property, and should contain a tuple of xy coordinates of the white point.

"},{"location":"plugins/space/#achromatic-rules","title":"Achromatic Rules","text":"

A given color space can define its rules for determining whether a color is achromatic. If one is not defined, the color will be convert to XYZ D65 and its achromatic method will be used. In order to have reasonably fast checks, it is better to evaluate the achromatic state without converting to another color. In order to define achromatic rules, simply override is_achromatic(). For instance, LCh is achromatic when chroma is very close to zero:

    def is_achromatic(self, coords: Vector) -> bool | None:\n\"\"\"Check if color is achromatic.\"\"\"\n\n        return coords[1] < ACHROMATIC_THRESHOLD\n
"},{"location":"plugins/space/#resolve-undefined-values","title":"Resolve Undefined Values","text":"

By default, undefined color channels are resolved as 0, but there are color spaces where zero just does not work well for a given channel. Such color spaces can choose a different default for undefined values. Generally, 0 is encouraged, but if zero is fundamentally a bad value for a color space and/or can decrease accuracy of colors, a different default can be specified when defining a channel on the color space via the nans parameter.

class ACEScct(sRGB):\n\"\"\"The ACEScct color class.\"\"\"\n\n    BASE = \"acescg\"\n    NAME = \"acescct\"\n    SERIALIZE = (\"--acescct\",)\n    WHITE = (0.32168, 0.33767)\n    CHANNELS = (\n        Channel(\"r\", CCT_MIN, CCT_MAX, bound=True, nans=CCT_MIN),\n        Channel(\"g\", CCT_MIN, CCT_MAX, bound=True, nans=CCT_MIN),\n        Channel(\"b\", CCT_MIN, CCT_MAX, bound=True, nans=CCT_MIN)\n    )\n

If the channel requires more advanced handling, you can override resolve_channel() on the color space itself:

    def resolve_channel(self, index: int, coords: Vector) -> float:\n\"\"\"Resove channels.\"\"\"\n\n        if index in (1, 2):\n            if not math.isnan(coords[index]):\n                return coords[index]\n\n            return self.ACHROMATIC.get_ideal_ab(coords[0])[index - 1]\n\n        value = coords[index]\n        return self.channels[index].nans if math.isnan(value) else value\n
"},{"location":"plugins/space/#mix-ins","title":"Mix-ins","text":"

ColorAide provides some various mixins for some common color space types. It should be noted that all cylindrical type color mixins are derived from Cylindrical. Regular is used for normal, 3 channel color spaces usually with ranges of [0, 1], CMY and sRGB as examples.

CylindricalRegularRGBishHSLishHSVishHWBishLabishLChish
class Cylindrical:\n\"\"\"Cylindrical space.\"\"\"\n\n    def hue_name(self) -> str:\n\"\"\"Hue channel name.\"\"\"\n\n        return \"h\"\n\n    def hue_index(self) -> int:\n\"\"\"Get hue index.\"\"\"\n\n        return cast('Space', self).get_channel_index(self.hue_name())\n
class Regular:\n    \"\"\"Regular, 3 channel color space usually with range of [0, 1].\n
class RGBish(Regular):\n\"\"\"RGB-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return RGB-ish names in order R G B.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of RGB-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class HSLish(Cylindrical):\n\"\"\"HSL-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return HSL-ish names in order H S L.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of HSL-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class HSVish(Cylindrical):\n\"\"\"HSV-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return HSV-ish names in order H S V.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of HSV-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class HWBish(Cylindrical):\n\"\"\"HWB-ish space.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return HWB-ish names in order H W B.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of HWB-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class Labish:\n\"\"\"Lab-ish color spaces.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return Lab-ish names in the order L a b.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of the Lab-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n
class LChish(Cylindrical):\n\"\"\"LCh-ish color spaces.\"\"\"\n\n    def names(self) -> tuple[str, ...]:\n\"\"\"Return LCh-ish names in the order L c h.\"\"\"\n\n        return self.channels[:-1]\n\n    def indexes(self) -> list[int]:\n\"\"\"Return the index of the Lab-ish channels.\"\"\"\n\n        return [self.get_channel_index(name) for name in self.names()]\n

Mix-in classes are mainly available so that a color space can be inspected to see if it falls into a specific generic color space type in order to allow for some generic handling of the color. For instance, you may not care specifically what color space you are dealing with, but you may want to extract the hue from all cylindrical spaces, or grab the lightness (or lightness equivalent) from all Lab-ish color spaces.

The mix-in classes provide methods mainly to extract expected channels on color spaces that may use different names for similar channels or to determine the index of a specific channel type. Occasionally, these methods may need to be overridden for a color space.

Below, we can see that both jzazbz and ictcp identify as Lab-ish spaces. If we just care about accessing the equivalent of Lab lightness on these spaces, we can simply can access them with the following logic.

>>> from coloraide.spaces import Labish\n>>> srgb = Color('red')\n>>> jzazbz = srgb.convert('jzazbz')\n>>> ictcp = srgb.convert('ictcp')\n>>> for c in (srgb, jzazbz, ictcp):\n...     if isinstance(c._space, Labish):\n...         print('color: ', c)\n...         l = c._space.names()[0]\n...         print('channel: ', l)\n...         print('value: ', c.get(l))\n... \ncolor:  color(--jzazbz 0.13438 0.11789 0.11188 / 1)\nchannel:  jz\nvalue:  0.13438473104350065\ncolor:  color(--ictcp 0.42785 -0.11574 0.2788 / 1)\nchannel:  i\nvalue:  0.4278524836087273\n
"},{"location":"plugins/space/#adding-new-inputoutput-formats","title":"Adding New Input/Output Formats","text":"

One common thing that may be desired is altering an existing color space to accept and output a specialized format. While using hex color codes or rgb() formats are fairly common, there are many places were other forms are used to represent colors. It may be beneficial for a user working with colors in some more obscure form to repurpose a color space to handle different input/output formats.

The base of every color space is defined to accept and output the color(space ...) format. As this is a common input form across all color spaces, it is handled generically for all spaces in one action for performance reasons. Iterating each color space to perform the same match with a different color spaces name is obviously slower. A color can opt out of this input format by simply setting COLOR_FORMAT to False. This only disables input parsing. In order to disable this format during serialization the color space's #py3 to_string() method would need to be overridden.

New, per color space matching logic can be achieved by simply by overriding the match() method. If it is desired to also accept the color(space ...) format, just keep the COLOR_FORMAT flag enabled; otherwise, disable it.

As an example, let's consider the default sRGB space. We wanted to add additional CSS formats in addition to the color(space ...) format. While we won't go into the specific parsing logic, the general top-level logic can be seen below.

We simply override the match() method and call into our CSS parser. The parser will handle the appropriate syntax for our color spaces. It is not configured to process the color(space ...) format as that is already handled more efficiently when with COLOR_FORMAT enabled. Also, notice that match() is expected to return two things: a tuple containing the color channel coordinates and the alpha value, and the end position (([r, g, b], a), end). If the match fails, it simply returns None.

from coloraide.spaces import srgb as base\nfrom coloraide.css import parse\n\n\nclass sRGB(base.sRGB):\n\"\"\"sRGB class.\"\"\"\n\n    # This color class should opt into the generic `color(space ...)` input format.\n    # This is `True` by default, but shown for demonstration purposes.\n    COLOR_FORMAT: True\n\n    # If the color format above is not found, continue with our custom match to handle all other formats.\n    def match(\n        self,\n        string: str,\n        start: int = 0,\n        fullmatch: bool = True\n    ) -> Tuple[Tuple[Vector, float], int] | None:\n\"\"\"Match a CSS color string.\"\"\"\n\n        return parse.parse_css(self, string, start, fullmatch)\n

Additionally, we control the output formats by overriding the to_string() function. We ensure that it accepts all the parameters we need, in our case we accept the common parameters and later check for our special inputs in kwargs.

    def to_string(\n        self,\n        parent: 'Color',\n        *,\n        alpha: bool | None = None,\n        precision: int | None = None,\n        fit: Union[bool, str] = True,\n        none: bool = False,\n        color: bool = False,\n        hex: bool = False,\n        names: bool = False,\n        comma: bool = False,\n        upper: bool = False,\n        percent: bool = False,\n        compress: bool = False,\n        **kwargs: Any\n    ) -> str:\n\"\"\"Convert to CSS.\"\"\"\n\n        return serialize.serialize_css(\n            parent,\n            func='rgb',\n            alpha=alpha,\n            precision=precision,\n            fit=fit,\n            none=none,\n            color=color,\n            hexa=hex,\n            name=names,\n            legacy=comma,\n            upper=upper,\n            percent=percent,\n            compress=compress,\n            scale=255\n        )\n

As all ColorAide color spaces are defined as plugins, there should be ample examples to help someone start writing a new color space.

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 434cea2e9..103361d77 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,467 +2,467 @@ https://facelessuser.github.io/coloraide/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/advanced/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/average/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/cat/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/chromaticity/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/color/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/compositing/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/contrast/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/distance/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/filters/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/gamut/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/harmonies/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/interpolation/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/manipulation/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/playground/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/strings/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/temperature/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/about/acknowledgments/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/about/changelog/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/about/contributing/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/about/license/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/about/releases/1.0/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/api/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/a98_rgb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/a98_rgb_linear/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/aces2065_1/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/acescc/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/acescct/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/acescg/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cam16/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cam16_jmh/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cam16_lcd/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cam16_scd/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cam16_ucs/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cmy/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cmyk/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/cubehelix/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/din99o/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/display_p3/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/display_p3_linear/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hct/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hpluv/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hsi/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hsl/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hsluv/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hsv/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hunter_lab/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/hwb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/ictcp/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/igpgtg/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/ipt/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/jzazbz/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/jzczhz/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/lab/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/lab_d65/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/lch/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/lch99o/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/lch_d65/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/lchuv/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/luv/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/okhsl/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/okhsv/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/oklab/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/oklch/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/orgb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/prismatic/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/prophoto_rgb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/prophoto_rgb_linear/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/rec2020/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/rec2020_linear/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/rec2100_hlg/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/rec2100_pq/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/rec709/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/rlab/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/ryb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/srgb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/srgb_linear/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/ucs/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/xyb/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/xyy/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/xyz_d50/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/colors/xyz_d65/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/demos/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/cat/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/cct/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/contrast/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/delta_e/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/filter/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/fit/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/interpolate/ - 2023-09-12 + 2023-09-17 daily https://facelessuser.github.io/coloraide/plugins/space/ - 2023-09-12 + 2023-09-17 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index c87f32910..6bcb698da 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/strings/index.html b/strings/index.html index caf1501f1..8723c6e37 100644 --- a/strings/index.html +++ b/strings/index.html @@ -61,4 +61,4 @@
Last update: August 12, 2023
\ No newline at end of file +
Last update: August 12, 2023
\ No newline at end of file diff --git a/temperature/index.html b/temperature/index.html index eea19989f..8aa8a9f88 100644 --- a/temperature/index.html +++ b/temperature/index.html @@ -118,4 +118,4 @@ Steps([Custom.blackbody('srgb-linear', t, method='ohno-2013') for t in range(1000, 15000, 50)])
Last update: June 27, 2023
\ No newline at end of file +
Last update: June 27, 2023
\ No newline at end of file