├── .eslintrc.json
├── .git-hooks
└── pre-commit
├── .gitignore
├── .prettierignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── benchmark
├── README.md
├── index.html
├── index.js
├── package-lock.json
├── package.json
├── tests
│ ├── ciede2000-vs-din99o.js
│ ├── interpolate-speed.js
│ ├── namedcolors-parse-speed.js
│ └── rgb-parse-speed.js
└── util
│ └── benchmark.js
├── build.js
├── docs
├── _includes
│ └── default.njk
├── api.md
├── colophon.md
├── color-spaces.md
├── css
│ └── main.css
├── getting-started.md
├── guides
│ ├── guides.json
│ ├── index.md
│ ├── migration.md
│ └── tree-shaking.md
├── img
│ ├── culori.svg
│ ├── evenly-spaced-vs-positions.png
│ ├── interpolator-basis-closed.svg
│ ├── interpolator-basis.svg
│ ├── interpolator-linear.svg
│ ├── interpolator-monotone-2.svg
│ ├── interpolator-monotone-closed.svg
│ ├── interpolator-monotone.svg
│ ├── interpolator-natural-closed.svg
│ ├── interpolator-natural.svg
│ └── red-blue.png
├── index.md
├── resources.md
└── static
│ ├── .nojekyll
│ ├── CNAME
│ └── favicon.png
├── eleventy.config.cjs
├── package-lock.json
├── package.json
├── src
├── _prepare.js
├── a98
│ ├── convertA98ToXyz65.js
│ ├── convertXyz65ToA98.js
│ └── definition.js
├── average.js
├── blend.js
├── bootstrap
│ ├── all.js
│ └── css.js
├── clamp.js
├── colors
│ └── named.js
├── constants.js
├── converter.js
├── cubehelix
│ ├── constants.js
│ ├── convertCubehelixToRgb.js
│ ├── convertRgbToCubehelix.js
│ └── definition.js
├── deficiency.js
├── difference.js
├── dlab
│ └── definition.js
├── dlch
│ ├── constants.js
│ ├── convertDlchToLab65.js
│ ├── convertLab65ToDlch.js
│ └── definition.js
├── easing
│ ├── gamma.js
│ ├── inOutSine.js
│ ├── midpoint.js
│ ├── smootherstep.js
│ └── smoothstep.js
├── filter.js
├── fixup
│ ├── alpha.js
│ └── hue.js
├── formatter.js
├── hdr
│ ├── constants.js
│ └── transfer.js
├── hsi
│ ├── convertHsiToRgb.js
│ ├── convertRgbToHsi.js
│ └── definition.js
├── hsl
│ ├── convertHslToRgb.js
│ ├── convertRgbToHsl.js
│ ├── definition.js
│ ├── parseHsl.js
│ └── parseHslLegacy.js
├── hsv
│ ├── convertHsvToRgb.js
│ ├── convertRgbToHsv.js
│ └── definition.js
├── hwb
│ ├── convertHwbToRgb.js
│ ├── convertRgbToHwb.js
│ ├── definition.js
│ └── parseHwb.js
├── index-fn.js
├── index.js
├── interpolate
│ ├── interpolate.js
│ ├── lerp.js
│ ├── linear.js
│ ├── piecewise.js
│ ├── splineBasis.js
│ ├── splineMonotone.js
│ └── splineNatural.js
├── itp
│ ├── convertItpToXyz65.js
│ ├── convertXyz65ToItp.js
│ └── definition.js
├── jab
│ ├── convertJabToRgb.js
│ ├── convertJabToXyz65.js
│ ├── convertRgbToJab.js
│ ├── convertXyz65ToJab.js
│ └── definition.js
├── jch
│ ├── convertJabToJch.js
│ ├── convertJchToJab.js
│ └── definition.js
├── lab
│ ├── convertLabToRgb.js
│ ├── convertLabToXyz50.js
│ ├── convertRgbToLab.js
│ ├── convertXyz50ToLab.js
│ ├── definition.js
│ └── parseLab.js
├── lab65
│ ├── convertLab65ToRgb.js
│ ├── convertLab65ToXyz65.js
│ ├── convertRgbToLab65.js
│ ├── convertXyz65ToLab65.js
│ └── definition.js
├── lch
│ ├── convertLabToLch.js
│ ├── convertLchToLab.js
│ ├── definition.js
│ └── parseLch.js
├── lch65
│ └── definition.js
├── lchuv
│ ├── convertLchuvToLuv.js
│ ├── convertLuvToLchuv.js
│ └── definition.js
├── lrgb
│ ├── convertLrgbToRgb.js
│ ├── convertRgbToLrgb.js
│ └── definition.js
├── luv
│ ├── convertLuvToXyz50.js
│ ├── convertXyz50ToLuv.js
│ └── definition.js
├── map.js
├── modes.js
├── nearest.js
├── okhsl
│ ├── LICENSE
│ ├── convertOkhslToOklab.js
│ ├── convertOklabToOkhsl.js
│ ├── helpers.js
│ └── modeOkhsl.js
├── okhsv
│ ├── convertOkhsvToOklab.js
│ ├── convertOklabToOkhsv.js
│ └── modeOkhsv.js
├── oklab
│ ├── convertLrgbToOklab.js
│ ├── convertOklabToLrgb.js
│ ├── convertOklabToRgb.js
│ ├── convertRgbToOklab.js
│ ├── definition.js
│ └── parseOklab.js
├── oklch
│ ├── definition.js
│ └── parseOklch.js
├── p3
│ ├── convertP3ToXyz65.js
│ ├── convertXyz65ToP3.js
│ └── definition.js
├── parse.js
├── prophoto
│ ├── convertProphotoToXyz50.js
│ ├── convertXyz50ToProphoto.js
│ └── definition.js
├── random.js
├── rec2020
│ ├── convertRec2020ToXyz65.js
│ ├── convertXyz65ToRec2020.js
│ └── definition.js
├── rgb
│ ├── definition.js
│ ├── parseHex.js
│ ├── parseNamed.js
│ ├── parseNumber.js
│ ├── parseRgb.js
│ ├── parseRgbLegacy.js
│ └── parseTransparent.js
├── round.js
├── samples.js
├── util
│ ├── hue.js
│ ├── normalizeHue.js
│ ├── normalizePositions.js
│ └── regex.js
├── wcag.js
├── xyb
│ ├── constants.js
│ ├── convertRgbToXyb.js
│ ├── convertXybToRgb.js
│ └── definition.js
├── xyz50
│ ├── constants.js
│ ├── convertRgbToXyz50.js
│ ├── convertXyz50ToRgb.js
│ └── definition.js
├── xyz65
│ ├── constants.js
│ ├── convertRgbToXyz65.js
│ ├── convertXyz50ToXyz65.js
│ ├── convertXyz65ToRgb.js
│ ├── convertXyz65ToXyz50.js
│ └── definition.js
└── yiq
│ ├── convertRgbToYiq.js
│ ├── convertYiqToRgb.js
│ └── definition.js
├── test
├── a98.test.js
├── api.test.js
├── average.test.js
├── blend.test.js
├── cat.test.js
├── clamp.test.js
├── color-syntax.test.js
├── css.test.js
├── cubehelix.test.js
├── deficiency.test.js
├── difference.test.js
├── dlab.test.js
├── dlch.test.js
├── easing.test.js
├── filter.test.js
├── fixupAlpha.test.js
├── fixupHue.test.js
├── formatter.test.js
├── hsi.test.js
├── hsl.test.js
├── hsv.test.js
├── hwb.test.js
├── interpolate.test.js
├── interpolatorLinear.test.js
├── interpolatorSplineBasis.test.js
├── interpolatorSplineMonotone.test.js
├── interpolatorSplineNatural.test.js
├── itp.test.js
├── jab.test.js
├── jch.test.js
├── lab.test.js
├── lab65.test.js
├── lch.test.js
├── lch65.test.js
├── lchuv.test.js
├── lerp.test.js
├── lrgb.test.js
├── luv.test.js
├── map.test.js
├── nearest.test.js
├── none.test.js
├── normalizePositions.test.js
├── okhsl.test.js
├── okhsv.test.js
├── oklab.test.js
├── oklch.test.js
├── p3.test.js
├── parse.test.js
├── prophoto.test.js
├── random.test.js
├── rec2020.test.js
├── rgb.test.js
├── samples.test.js
├── tree-shaking
│ ├── not-tree-shaken.js
│ └── tree-shaken.js
├── visual
│ ├── gamut-map-newton.js
│ ├── gamut-mapping-algorithms.js
│ ├── gamut-mapping.html
│ └── utils.js
├── wcag.test.js
├── xyb.test.js
├── xyz50.test.js
├── xyz65.test.js
└── yiq.test.js
└── tools
├── math
├── itp-matrices.py
├── rec2020-matrices.py
└── requirements.txt
└── ranges.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": ["eslint:recommended", "plugin:import/recommended"],
8 | "parserOptions": {
9 | "ecmaVersion": 12,
10 | "sourceType": "module"
11 | },
12 | "plugins": ["import"],
13 | "rules": {
14 | "constructor-super": 1,
15 | "import/no-anonymous-default-export": 1,
16 | "import/no-unused-modules": 1,
17 | "no-const-assign": 1,
18 | "no-loss-of-precision": 0,
19 | "no-mixed-spaces-and-tabs": 0,
20 | "no-this-before-super": 1,
21 | "no-undef": 2,
22 | "no-unused-vars": [1, { "args": "none" }],
23 | "valid-typeof": 1
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.git-hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | npx pretty-quick --staged
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sublime-workspace
2 | .DS_Store
3 | build/
4 | bundled/
5 | node_modules
6 | *.log
7 | .nyc_output/
8 | coverage/
9 | yarn-error.log
10 |
11 | # Website
12 | www/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.md
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Development
4 |
5 | Tests are defined using the `node:test` and `node:assert` modules. Running the tests require Node 21 or newer:
6 |
7 | ```bash
8 | npm run test
9 | ```
10 |
11 | We use ESLint to lint JavaScript code.
12 |
13 |
14 | ```bash
15 | npm run lint
16 | ```
17 |
18 | While Culori can be used from source in environments supporting ES Modules, we also use esbuild to build bundles in CommonJS, ESM, and UMD formats.
19 |
20 | ```bash
21 | npm run build
22 | ```
23 |
24 | ## Documentation
25 |
26 | The documentation website [culorijs.org] is built with Eleventy out of the `www/` folder, and deployed on GitHub pages via the `gh-pages` branch.
27 |
28 | The following scripts are available:
29 |
30 | ```bash
31 | npm run docs:start
32 | npm run docs:build
33 | npm run docs:deploy
34 | ```
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Dan Burzo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 |
5 | Culori is a comprehensive color library for JavaScript that works across many color spaces to offer conversion, interpolation, color difference formulas, blending functions, and more. It provides up-to-date support for the color spaces defined in [CSS Color Module Level 4](https://drafts.csswg.org/css-color/) specification.
6 |
7 | ```bash
8 | npm install culori
9 | ```
10 |
11 | The full documentation is published on [culorijs.org](https://culorijs.org). Some quick entry points:
12 |
13 | - [Getting started](https://culorijs.org/getting-started)
14 | - [API References](https://culorijs.org/api/)
15 | - [Supported color spaces](https://culorijs.org/color-spaces/)
16 |
17 | ## Contributing
18 |
19 | Contributions of all kinds (feedback, ideas, bug fixes, documentation) are welcome.
20 |
21 | Please open a GitHub issue/discussion before putting in any work that’s not straightforward.
22 |
23 | More in [CONTRIBUTING.md](./CONTRIBUTING.md).
24 |
--------------------------------------------------------------------------------
/benchmark/README.md:
--------------------------------------------------------------------------------
1 | # Culori Benchmarks
2 |
3 | In the `culori/benchmark` folder, run `npm install` to fetch all the dependencies. With `node index.js` you can then execute all the benchmarks.
4 |
--------------------------------------------------------------------------------
/benchmark/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | culori browser playground
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/benchmark/index.js:
--------------------------------------------------------------------------------
1 | import './tests/rgb-parse-speed.js';
2 | import './tests/namedcolors-parse-speed.js';
3 | import './tests/interpolate-speed.js';
4 |
--------------------------------------------------------------------------------
/benchmark/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "culori-benchmarks",
3 | "type": "module",
4 | "version": "1.0.0",
5 | "main": "index.js",
6 | "license": "MIT",
7 | "devDependencies": {
8 | "chroma-js": "^2.4.2",
9 | "d3-color": "^3.1.0",
10 | "d3-interpolate": "^3.0.1",
11 | "tinycolor2": "^1.5.2",
12 | "colorjs.io": "^0.4.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/benchmark/tests/ciede2000-vs-din99o.js:
--------------------------------------------------------------------------------
1 | import {
2 | nearest,
3 | differenceCiede2000,
4 | differenceDin99o
5 | } from '../../src/index.js';
6 | import benchmark from '../util/benchmark.js';
7 |
8 | let colors = Object.keys(culori.colorsNamed);
9 |
10 | let ciede2000 = nearest(colors, differenceCiede2000());
11 | let din99o = nearest(colors, differenceDin99o());
12 |
13 | benchmark('ciede2000', () => {
14 | for (var k = 0; k < 100; k++) {
15 | for (var i = 0; i < colors.length; i++) {
16 | ciede2000(colors[i]);
17 | }
18 | }
19 | });
20 |
21 | benchmark('din99o', () => {
22 | for (var k = 0; k < 100; k++) {
23 | for (var i = 0; i < colors.length; i++) {
24 | din99o(colors[i]);
25 | }
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/benchmark/tests/namedcolors-parse-speed.js:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js';
2 | import { color } from 'd3-color';
3 | import tinycolor from 'tinycolor2';
4 | import { rgb, colorsNamed } from '../../src/index.js';
5 | import benchmark from '../util/benchmark.js';
6 |
7 | console.log(`
8 | Benchmark: named colors parse speed
9 | ===================================
10 | `);
11 |
12 | let colors = Object.keys(colorsNamed);
13 | let iterations = 10000;
14 |
15 | benchmark('chroma: chroma("colorname")', () => {
16 | for (var it = 0; it < iterations; it++) {
17 | for (var i = 0; i < colors.length; i++) {
18 | chroma(colors[i]);
19 | }
20 | }
21 | });
22 |
23 | benchmark('d3-color: color("colorname")', () => {
24 | for (var it = 0; it < iterations; it++) {
25 | for (var i = 0; i < colors.length; i++) {
26 | color(colors[i]);
27 | }
28 | }
29 | });
30 |
31 | benchmark('tinycolor: tinycolor("colorname")', () => {
32 | for (var it = 0; it < iterations; it++) {
33 | for (var i = 0; i < colors.length; i++) {
34 | tinycolor(colors[i]);
35 | }
36 | }
37 | });
38 |
39 | benchmark('culori: culori("colorname")', () => {
40 | for (var it = 0; it < iterations; it++) {
41 | for (var i = 0; i < colors.length; i++) {
42 | rgb(colors[i]);
43 | }
44 | }
45 | });
46 |
--------------------------------------------------------------------------------
/benchmark/tests/rgb-parse-speed.js:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js';
2 | import { color } from 'd3-color';
3 | import tinycolor from 'tinycolor2';
4 | import { ColorSpace, sRGB, parse } from 'colorjs.io/fn';
5 | import { rgb } from '../../src/index.js';
6 | import benchmark from '../util/benchmark.js';
7 |
8 | console.log(`
9 | Benchmark: RGB parse speed
10 | ==========================
11 | `);
12 |
13 | let increment = 2;
14 |
15 | benchmark('chroma: chroma("rgb(r,g,b)")', () => {
16 | for (var r = 0; r <= 255; r += increment) {
17 | for (var g = 0; g <= 255; g += increment) {
18 | for (var b = 0; b <= 255; b += increment) {
19 | chroma(`rgb(${r},${g},${b})`);
20 | }
21 | }
22 | }
23 | });
24 |
25 | benchmark('d3-color: color("rgb(r,g,b)")', () => {
26 | for (var r = 0; r <= 255; r += increment) {
27 | for (var g = 0; g <= 255; g += increment) {
28 | for (var b = 0; b <= 255; b += increment) {
29 | color(`rgb(${r},${g},${b})`);
30 | }
31 | }
32 | }
33 | });
34 |
35 | benchmark('tinycolor: tinycolor("rgb(r,g,b)")', () => {
36 | for (var r = 0; r <= 255; r += increment) {
37 | for (var g = 0; g <= 255; g += increment) {
38 | for (var b = 0; b <= 255; b += increment) {
39 | tinycolor(`rgb(${r},${g},${b})`);
40 | }
41 | }
42 | }
43 | });
44 |
45 | benchmark('culori: culori("rgb(r,g,b)")', () => {
46 | for (var r = 0; r <= 255; r += increment) {
47 | for (var g = 0; g <= 255; g += increment) {
48 | for (var b = 0; b <= 255; b += increment) {
49 | rgb(`rgb(${r},${g},${b})`);
50 | }
51 | }
52 | }
53 | });
54 |
55 | benchmark('culori: culori("rgb(r g b)")', () => {
56 | for (var r = 0; r <= 255; r += increment) {
57 | for (var g = 0; g <= 255; g += increment) {
58 | for (var b = 0; b <= 255; b += increment) {
59 | rgb(`rgb(${r} ${g} ${b})`);
60 | }
61 | }
62 | }
63 | });
64 |
65 | ColorSpace.register(sRGB);
66 | benchmark('colorjs.io: parse("rgb(r g b)")', () => {
67 | for (var r = 0; r <= 255; r += increment) {
68 | for (var g = 0; g <= 255; g += increment) {
69 | for (var b = 0; b <= 255; b += increment) {
70 | parse(`rgb(${r} ${g} ${b})`);
71 | }
72 | }
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/benchmark/util/benchmark.js:
--------------------------------------------------------------------------------
1 | var start, end;
2 | function startBench() {
3 | start = process.hrtime();
4 | }
5 |
6 | function endBench() {
7 | end = process.hrtime(start);
8 | return end[0] + 's ' + end[1] / 1000000 + 'ms';
9 | }
10 |
11 | export default function (name, fn) {
12 | startBench();
13 | fn();
14 | console.log(name, endBench());
15 | }
16 |
--------------------------------------------------------------------------------
/docs/colophon.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default.njk
3 | title: Colophon
4 | ---
5 |
6 | Culori is developed by [Dan Burzo](http://danburzo.ro) with the help of many collaborators, and released under the MIT license.
7 |
8 | The library has been inspired by Mike Bostock's [D3.js](https://github.com/d3) and Gregor Aisch's [chroma.js](https://github.com/gka/chroma.js): they have popularized color science in the browser and provided a blueprint for a color manipulation API. I have learned a tremendous amount by reading through the documentation the source code while developing Culori. D3, in particular, has been a treasure trove of ideas and pointers to academic references.
9 |
10 | I pronounce the name of the library as [[kuːlori]](http://ipa-reader.xyz/?text=ku%CB%90lori). Culori is the Romanian word for ‘colors’. The logo is typeset in [Hatch](https://pstypelab.com/hatchfont) by Mark Caneso.
11 |
12 | The code is formatted with [Prettier](https://prettier.io), upkept with [ESLint](https://eslint.org/), tested with [tape](https://github.com/ljharb/tape), and bundled with [esbuild](https://esbuild.github.io/). This website is statically generated using [Eleventy](https://11ty.dev) and published to GitHub Pages.
13 |
--------------------------------------------------------------------------------
/docs/guides/guides.json:
--------------------------------------------------------------------------------
1 | {
2 | "layout": "default.njk"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/guides/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Guides'
3 | ---
4 |
5 | ## Working with Culori
6 |
7 | ### [Optimize bundle size with tree-shaking](./tree-shaking/)
8 |
9 | Once you're done prototyping and you're ready to ship an optimized bundle, switch your imports over to the fully tree-shakeable version.
10 |
11 | ### [Migration guide](./migration/)
12 |
13 | This guide documents the breaking changes in each new major release of the library and how to address them in your code.
14 |
15 | ## Elsewhere
16 |
17 | [Coloring With Code — A Programmatic Approach To Design](https://tympanus.net/codrops/2021/12/07/coloring-with-code-a-programmatic-approach-to-design/), a tutorial by George Francis.
18 |
--------------------------------------------------------------------------------
/docs/img/evenly-spaced-vs-positions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evercoder/culori/2bedb8f0507116e75f844a705d0b45cf279b15d0/docs/img/evenly-spaced-vs-positions.png
--------------------------------------------------------------------------------
/docs/img/interpolator-basis-closed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/interpolator-basis.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/interpolator-linear.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/interpolator-monotone-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/interpolator-monotone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/interpolator-natural-closed.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/interpolator-natural.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/img/red-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evercoder/culori/2bedb8f0507116e75f844a705d0b45cf279b15d0/docs/img/red-blue.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Color functions for JavaScript
3 | homepage: true
4 | layout: default.njk
5 | ---
6 |
7 | [css4-colors]: https://drafts.csswg.org/css-color/
8 | [css4-named-colors]: https://drafts.csswg.org/css-color/#named-colors
9 | [din99o]: https://de.wikipedia.org/wiki/DIN99-Farbraum
10 | [hex-colors]: https://drafts.csswg.org/css-color/#hex-notation
11 | [rgb-colors]: https://drafts.csswg.org/css-color/#rgb-functions
12 | [hsl-colors]: https://drafts.csswg.org/css-color/#the-hsl-notation
13 | [hwb-colors]: https://drafts.csswg.org/css-color/#the-hwb-notation
14 | [lab-colors]: https://drafts.csswg.org/css-color/#lab-colors
15 |
16 | Culori is a JavaScript color library that supports the conversion and manipulation of all formats defined in the [CSS Colors Level 4][css4-colors] specification, plus [additional color spaces](./color-spaces). It handles [color differences](https://en.wikipedia.org/wiki/Color_difference), interpolation, gradients, blend modes [and much more](/api/).
17 |
18 | ```bash
19 | npm install culori
20 | ```
21 |
22 | Get started
23 |
24 | ## What sets Culori apart?
25 |
26 | __A function-oriented API.__ Colors are represented as plain JavaScript objects you pass through a series of [functions](./api), which makes it super easy to extend.
27 |
28 | __Accurate alpha.__ On the `alpha` channel, the library doesn't equate an `undefined` value with an opaque color, but rather with a color for which we don't care about the opacity. This gives you the opportunity to interpret `undefined` as you see fit. The hex string #ff0000 _should_ probably be rendered as fully opaque red, but for running functions on colors it's useful to discern #ff0000 from #ff0000ff — the former has an implicit alpha of 1, while for the latter it's explicit.
29 |
30 | __Comprehensive functionality.__ Build advanced color tools with Culori's rich collection of color spaces and functions.
31 |
32 | __Tree-shakeable version available.__ When you're ready to optimize for bundle size, switch to [a tree-shakeable version of the library](./guides/tree-shaking).
33 |
--------------------------------------------------------------------------------
/docs/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evercoder/culori/2bedb8f0507116e75f844a705d0b45cf279b15d0/docs/static/.nojekyll
--------------------------------------------------------------------------------
/docs/static/CNAME:
--------------------------------------------------------------------------------
1 | culorijs.org
--------------------------------------------------------------------------------
/docs/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Evercoder/culori/2bedb8f0507116e75f844a705d0b45cf279b15d0/docs/static/favicon.png
--------------------------------------------------------------------------------
/eleventy.config.cjs:
--------------------------------------------------------------------------------
1 | const highlight = require('@11ty/eleventy-plugin-syntaxhighlight');
2 |
3 | module.exports = function (env) {
4 | env.addPassthroughCopy('docs/img');
5 | env.addPassthroughCopy('docs/css');
6 | env.addPassthroughCopy({ 'docs/static': '.' });
7 |
8 | env.addFilter('sortby', function (arr, key) {
9 | return arr.slice().sort((a, b) => {
10 | if (a.data[key] > b.data[key]) {
11 | return 1;
12 | }
13 | if (a.data[key] < b.data[key]) {
14 | return -1;
15 | }
16 | return 0;
17 | });
18 | });
19 | env.addPlugin(highlight);
20 | return {
21 | pathPrefix: '/',
22 | dir: {
23 | input: 'docs',
24 | output: 'www'
25 | },
26 | htmlTemplateEngine: 'njk'
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/src/_prepare.js:
--------------------------------------------------------------------------------
1 | import parse from './parse.js';
2 |
3 | const prepare = (color, mode) =>
4 | color === undefined
5 | ? undefined
6 | : typeof color !== 'object'
7 | ? parse(color)
8 | : color.mode !== undefined
9 | ? color
10 | : mode
11 | ? { ...color, mode }
12 | : undefined;
13 |
14 | export default prepare;
15 |
--------------------------------------------------------------------------------
/src/a98/convertA98ToXyz65.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert A98 RGB values to CIE XYZ D65
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | * https://www.adobe.com/digitalimag/pdfs/AdobeRGB1998.pdf
8 | */
9 |
10 | const linearize = (v = 0) => Math.pow(Math.abs(v), 563 / 256) * Math.sign(v);
11 |
12 | const convertA98ToXyz65 = a98 => {
13 | let r = linearize(a98.r);
14 | let g = linearize(a98.g);
15 | let b = linearize(a98.b);
16 | let res = {
17 | mode: 'xyz65',
18 | x:
19 | 0.5766690429101305 * r +
20 | 0.1855582379065463 * g +
21 | 0.1882286462349947 * b,
22 | y:
23 | 0.297344975250536 * r +
24 | 0.6273635662554661 * g +
25 | 0.0752914584939979 * b,
26 | z:
27 | 0.0270313613864123 * r +
28 | 0.0706888525358272 * g +
29 | 0.9913375368376386 * b
30 | };
31 | if (a98.alpha !== undefined) {
32 | res.alpha = a98.alpha;
33 | }
34 | return res;
35 | };
36 |
37 | export default convertA98ToXyz65;
38 |
--------------------------------------------------------------------------------
/src/a98/convertXyz65ToA98.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert CIE XYZ D65 values to A98 RGB
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | */
8 |
9 | const gamma = v => Math.pow(Math.abs(v), 256 / 563) * Math.sign(v);
10 |
11 | const convertXyz65ToA98 = ({ x, y, z, alpha }) => {
12 | if (x === undefined) x = 0;
13 | if (y === undefined) y = 0;
14 | if (z === undefined) z = 0;
15 | let res = {
16 | mode: 'a98',
17 | r: gamma(
18 | x * 2.0415879038107465 -
19 | y * 0.5650069742788597 -
20 | 0.3447313507783297 * z
21 | ),
22 | g: gamma(
23 | x * -0.9692436362808798 +
24 | y * 1.8759675015077206 +
25 | 0.0415550574071756 * z
26 | ),
27 | b: gamma(
28 | x * 0.0134442806320312 -
29 | y * 0.1183623922310184 +
30 | 1.0151749943912058 * z
31 | )
32 | };
33 | if (alpha !== undefined) {
34 | res.alpha = alpha;
35 | }
36 | return res;
37 | };
38 |
39 | export default convertXyz65ToA98;
40 |
--------------------------------------------------------------------------------
/src/a98/definition.js:
--------------------------------------------------------------------------------
1 | import rgb from '../rgb/definition.js';
2 |
3 | import convertA98ToXyz65 from './convertA98ToXyz65.js';
4 | import convertXyz65ToA98 from './convertXyz65ToA98.js';
5 | import convertRgbToXyz65 from '../xyz65/convertRgbToXyz65.js';
6 | import convertXyz65ToRgb from '../xyz65/convertXyz65ToRgb.js';
7 |
8 | const definition = {
9 | ...rgb,
10 | mode: 'a98',
11 | parse: ['a98-rgb'],
12 | serialize: 'a98-rgb',
13 |
14 | fromMode: {
15 | rgb: color => convertXyz65ToA98(convertRgbToXyz65(color)),
16 | xyz65: convertXyz65ToA98
17 | },
18 |
19 | toMode: {
20 | rgb: color => convertXyz65ToRgb(convertA98ToXyz65(color)),
21 | xyz65: convertA98ToXyz65
22 | }
23 | };
24 |
25 | export default definition;
26 |
--------------------------------------------------------------------------------
/src/average.js:
--------------------------------------------------------------------------------
1 | import converter from './converter.js';
2 | import { getMode } from './modes.js';
3 |
4 | const averageAngle = val => {
5 | // See: https://en.wikipedia.org/wiki/Mean_of_circular_quantities
6 | let sum = val.reduce(
7 | (sum, val) => {
8 | if (val !== undefined) {
9 | let rad = (val * Math.PI) / 180;
10 | sum.sin += Math.sin(rad);
11 | sum.cos += Math.cos(rad);
12 | }
13 | return sum;
14 | },
15 | { sin: 0, cos: 0 }
16 | );
17 | let angle = (Math.atan2(sum.sin, sum.cos) * 180) / Math.PI;
18 | return angle < 0 ? 360 + angle : angle;
19 | };
20 |
21 | const averageNumber = val => {
22 | let a = val.filter(v => v !== undefined);
23 | return a.length ? a.reduce((sum, v) => sum + v, 0) / a.length : undefined;
24 | };
25 |
26 | const isfn = o => typeof o === 'function';
27 |
28 | function average(colors, mode = 'rgb', overrides) {
29 | let def = getMode(mode);
30 | let cc = colors.map(converter(mode));
31 | return def.channels.reduce(
32 | (res, ch) => {
33 | let arr = cc.map(c => c[ch]).filter(val => val !== undefined);
34 | if (arr.length) {
35 | let fn;
36 | if (isfn(overrides)) {
37 | fn = overrides;
38 | } else if (overrides && isfn(overrides[ch])) {
39 | fn = overrides[ch];
40 | } else if (def.average && isfn(def.average[ch])) {
41 | fn = def.average[ch];
42 | } else {
43 | fn = averageNumber;
44 | }
45 | res[ch] = fn(arr, ch);
46 | }
47 | return res;
48 | },
49 | { mode }
50 | );
51 | }
52 |
53 | export { average, averageAngle, averageNumber };
54 |
--------------------------------------------------------------------------------
/src/bootstrap/css.js:
--------------------------------------------------------------------------------
1 | // Color space definitions
2 | import modeA98 from '../a98/definition.js';
3 | import modeHsl from '../hsl/definition.js';
4 | import modeHsv from '../hsv/definition.js';
5 | import modeHwb from '../hwb/definition.js';
6 | import modeLab from '../lab/definition.js';
7 | import modeLab65 from '../lab65/definition.js';
8 | import modeLch from '../lch/definition.js';
9 | import modeLch65 from '../lch65/definition.js';
10 | import modeLrgb from '../lrgb/definition.js';
11 | import modeOklab from '../oklab/definition.js';
12 | import modeOklch from '../oklch/definition.js';
13 | import modeP3 from '../p3/definition.js';
14 | import modeProphoto from '../prophoto/definition.js';
15 | import modeRec2020 from '../rec2020/definition.js';
16 | import modeRgb from '../rgb/definition.js';
17 | import modeXyz50 from '../xyz50/definition.js';
18 | import modeXyz65 from '../xyz65/definition.js';
19 | import { useMode } from '../modes.js';
20 |
21 | export const a98 = useMode(modeA98);
22 | export const hsl = useMode(modeHsl);
23 | export const hsv = useMode(modeHsv);
24 | export const hwb = useMode(modeHwb);
25 | export const lab = useMode(modeLab);
26 | export const lab65 = useMode(modeLab65);
27 | export const lch = useMode(modeLch);
28 | export const lch65 = useMode(modeLch65);
29 | export const lrgb = useMode(modeLrgb);
30 | export const oklab = useMode(modeOklab);
31 | export const oklch = useMode(modeOklch);
32 | export const p3 = useMode(modeP3);
33 | export const prophoto = useMode(modeProphoto);
34 | export const rec2020 = useMode(modeRec2020);
35 | export const rgb = useMode(modeRgb);
36 | export const xyz50 = useMode(modeXyz50);
37 | export const xyz65 = useMode(modeXyz65);
38 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | The XYZ tristimulus values (white point)
3 | of standard illuminants for the CIE 1931 2°
4 | standard observer.
5 |
6 | See: https://en.wikipedia.org/wiki/Standard_illuminant
7 | */
8 |
9 | export const D50 = {
10 | X: 0.3457 / 0.3585,
11 | Y: 1,
12 | Z: (1 - 0.3457 - 0.3585) / 0.3585
13 | };
14 |
15 | export const D65 = {
16 | X: 0.3127 / 0.329,
17 | Y: 1,
18 | Z: (1 - 0.3127 - 0.329) / 0.329
19 | };
20 |
21 | export const k = Math.pow(29, 3) / Math.pow(3, 3);
22 | export const e = Math.pow(6, 3) / Math.pow(29, 3);
23 |
--------------------------------------------------------------------------------
/src/converter.js:
--------------------------------------------------------------------------------
1 | import { converters } from './modes.js';
2 | import prepare from './_prepare.js';
3 |
4 | const converter =
5 | (target_mode = 'rgb') =>
6 | color =>
7 | (color = prepare(color, target_mode)) !== undefined
8 | ? // if the color's mode corresponds to our target mode
9 | color.mode === target_mode
10 | ? // then just return the color
11 | color
12 | : // otherwise check to see if we have a dedicated
13 | // converter for the target mode
14 | converters[color.mode][target_mode]
15 | ? // and return its result...
16 | converters[color.mode][target_mode](color)
17 | : // ...otherwise pass through RGB as an intermediary step.
18 | // if the target mode is RGB...
19 | target_mode === 'rgb'
20 | ? // just return the RGB
21 | converters[color.mode].rgb(color)
22 | : // otherwise convert color.mode -> RGB -> target_mode
23 | converters.rgb[target_mode](converters[color.mode].rgb(color))
24 | : undefined;
25 |
26 | export default converter;
27 |
--------------------------------------------------------------------------------
/src/cubehelix/constants.js:
--------------------------------------------------------------------------------
1 | export const M = [-0.14861, 1.78277, -0.29227, -0.90649, 1.97294, 0];
2 |
3 | export const degToRad = Math.PI / 180;
4 | export const radToDeg = 180 / Math.PI;
5 |
--------------------------------------------------------------------------------
/src/cubehelix/convertCubehelixToRgb.js:
--------------------------------------------------------------------------------
1 | import { degToRad, M } from './constants.js';
2 |
3 | const convertCubehelixToRgb = ({ h, s, l, alpha }) => {
4 | let res = { mode: 'rgb' };
5 |
6 | h = (h === undefined ? 0 : h + 120) * degToRad;
7 | if (l === undefined) l = 0;
8 |
9 | let amp = s === undefined ? 0 : s * l * (1 - l);
10 |
11 | let cosh = Math.cos(h);
12 | let sinh = Math.sin(h);
13 |
14 | res.r = l + amp * (M[0] * cosh + M[1] * sinh);
15 | res.g = l + amp * (M[2] * cosh + M[3] * sinh);
16 | res.b = l + amp * (M[4] * cosh + M[5] * sinh);
17 |
18 | if (alpha !== undefined) res.alpha = alpha;
19 | return res;
20 | };
21 |
22 | export default convertCubehelixToRgb;
23 |
--------------------------------------------------------------------------------
/src/cubehelix/convertRgbToCubehelix.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert a RGB color to the Cubehelix HSL color space.
3 |
4 | This computation is not present in Green's paper:
5 | https://arxiv.org/pdf/1108.5083.pdf
6 |
7 | ...but can be derived from the inverse, HSL to RGB conversion.
8 |
9 | It matches the math in Mike Bostock's D3 implementation:
10 |
11 | https://github.com/d3/d3-color/blob/master/src/cubehelix.js
12 | */
13 |
14 | import { radToDeg, M } from './constants.js';
15 |
16 | let DE = M[3] * M[4];
17 | let BE = M[1] * M[4];
18 | let BCAD = M[1] * M[2] - M[0] * M[3];
19 |
20 | const convertRgbToCubehelix = ({ r, g, b, alpha }) => {
21 | if (r === undefined) r = 0;
22 | if (g === undefined) g = 0;
23 | if (b === undefined) b = 0;
24 | let l = (BCAD * b + r * DE - g * BE) / (BCAD + DE - BE);
25 | let x = b - l;
26 | let y = (M[4] * (g - l) - M[2] * x) / M[3];
27 |
28 | let res = {
29 | mode: 'cubehelix',
30 | l: l,
31 | s:
32 | l === 0 || l === 1
33 | ? undefined
34 | : Math.sqrt(x * x + y * y) / (M[4] * l * (1 - l))
35 | };
36 |
37 | if (res.s) res.h = Math.atan2(y, x) * radToDeg - 120;
38 | if (alpha !== undefined) res.alpha = alpha;
39 |
40 | return res;
41 | };
42 |
43 | export default convertRgbToCubehelix;
44 |
--------------------------------------------------------------------------------
/src/dlab/definition.js:
--------------------------------------------------------------------------------
1 | import convertLabToLch from '../lch/convertLabToLch.js';
2 | import convertLchToLab from '../lch/convertLchToLab.js';
3 | import convertLab65ToRgb from '../lab65/convertLab65ToRgb.js';
4 | import convertRgbToLab65 from '../lab65/convertRgbToLab65.js';
5 | import convertDlchToLab65 from '../dlch/convertDlchToLab65.js';
6 | import convertLab65ToDlch from '../dlch/convertLab65ToDlch.js';
7 | import { interpolatorLinear } from '../interpolate/linear.js';
8 | import { fixupAlpha } from '../fixup/alpha.js';
9 |
10 | const convertDlabToLab65 = c => convertDlchToLab65(convertLabToLch(c, 'dlch'));
11 | const convertLab65ToDlab = c => convertLchToLab(convertLab65ToDlch(c), 'dlab');
12 |
13 | const definition = {
14 | mode: 'dlab',
15 |
16 | parse: ['--din99o-lab'],
17 | serialize: '--din99o-lab',
18 |
19 | toMode: {
20 | lab65: convertDlabToLab65,
21 | rgb: c => convertLab65ToRgb(convertDlabToLab65(c))
22 | },
23 |
24 | fromMode: {
25 | lab65: convertLab65ToDlab,
26 | rgb: c => convertLab65ToDlab(convertRgbToLab65(c))
27 | },
28 |
29 | channels: ['l', 'a', 'b', 'alpha'],
30 |
31 | ranges: {
32 | l: [0, 100],
33 | a: [-40.09, 45.501],
34 | b: [-40.469, 44.344]
35 | },
36 |
37 | interpolate: {
38 | l: interpolatorLinear,
39 | a: interpolatorLinear,
40 | b: interpolatorLinear,
41 | alpha: {
42 | use: interpolatorLinear,
43 | fixup: fixupAlpha
44 | }
45 | }
46 | };
47 |
48 | export default definition;
49 |
--------------------------------------------------------------------------------
/src/dlch/constants.js:
--------------------------------------------------------------------------------
1 | export const kE = 1;
2 | export const kCH = 1;
3 | export const θ = (26 / 180) * Math.PI;
4 | export const cosθ = Math.cos(θ);
5 | export const sinθ = Math.sin(θ);
6 | export const factor = 100 / Math.log(139 / 100); // ~ 303.67
7 |
--------------------------------------------------------------------------------
/src/dlch/convertDlchToLab65.js:
--------------------------------------------------------------------------------
1 | import { kCH, kE, sinθ, cosθ, θ, factor } from './constants.js';
2 |
3 | /*
4 | Convert DIN99o LCh to CIELab D65
5 | --------------------------------
6 | */
7 |
8 | const convertDlchToLab65 = ({ l, c, h, alpha }) => {
9 | if (l === undefined) l = 0;
10 | if (c === undefined) c = 0;
11 | if (h === undefined) h = 0;
12 | let res = {
13 | mode: 'lab65',
14 | l: (Math.exp((l * kE) / factor) - 1) / 0.0039
15 | };
16 |
17 | let G = (Math.exp(0.0435 * c * kCH * kE) - 1) / 0.075;
18 | let e = G * Math.cos((h / 180) * Math.PI - θ);
19 | let f = G * Math.sin((h / 180) * Math.PI - θ);
20 | res.a = e * cosθ - (f / 0.83) * sinθ;
21 | res.b = e * sinθ + (f / 0.83) * cosθ;
22 |
23 | if (alpha !== undefined) res.alpha = alpha;
24 | return res;
25 | };
26 |
27 | export default convertDlchToLab65;
28 |
--------------------------------------------------------------------------------
/src/dlch/convertLab65ToDlch.js:
--------------------------------------------------------------------------------
1 | import { kCH, kE, sinθ, cosθ, θ, factor } from './constants.js';
2 | import normalizeHue from '../util/normalizeHue.js';
3 |
4 | /*
5 | Convert CIELab D65 to DIN99o LCh
6 | ================================
7 | */
8 |
9 | const convertLab65ToDlch = ({ l, a, b, alpha }) => {
10 | if (l === undefined) l = 0;
11 | if (a === undefined) a = 0;
12 | if (b === undefined) b = 0;
13 | let e = a * cosθ + b * sinθ;
14 | let f = 0.83 * (b * cosθ - a * sinθ);
15 | let G = Math.sqrt(e * e + f * f);
16 | let res = {
17 | mode: 'dlch',
18 | l: (factor / kE) * Math.log(1 + 0.0039 * l),
19 | c: Math.log(1 + 0.075 * G) / (0.0435 * kCH * kE)
20 | };
21 |
22 | if (res.c) {
23 | res.h = normalizeHue(((Math.atan2(f, e) + θ) / Math.PI) * 180);
24 | }
25 |
26 | if (alpha !== undefined) res.alpha = alpha;
27 | return res;
28 | };
29 |
30 | export default convertLab65ToDlch;
31 |
--------------------------------------------------------------------------------
/src/dlch/definition.js:
--------------------------------------------------------------------------------
1 | import convertLabToLch from '../lch/convertLabToLch.js';
2 | import convertLchToLab from '../lch/convertLchToLab.js';
3 | import convertDlchToLab65 from './convertDlchToLab65.js';
4 | import convertLab65ToDlch from './convertLab65ToDlch.js';
5 | import convertLab65ToRgb from '../lab65/convertLab65ToRgb.js';
6 | import convertRgbToLab65 from '../lab65/convertRgbToLab65.js';
7 |
8 | import { fixupHueShorter } from '../fixup/hue.js';
9 | import { fixupAlpha } from '../fixup/alpha.js';
10 | import { interpolatorLinear } from '../interpolate/linear.js';
11 | import { differenceHueChroma } from '../difference.js';
12 | import { averageAngle } from '../average.js';
13 |
14 | const definition = {
15 | mode: 'dlch',
16 |
17 | parse: ['--din99o-lch'],
18 | serialize: '--din99o-lch',
19 |
20 | toMode: {
21 | lab65: convertDlchToLab65,
22 | dlab: c => convertLchToLab(c, 'dlab'),
23 | rgb: c => convertLab65ToRgb(convertDlchToLab65(c))
24 | },
25 |
26 | fromMode: {
27 | lab65: convertLab65ToDlch,
28 | dlab: c => convertLabToLch(c, 'dlch'),
29 | rgb: c => convertLab65ToDlch(convertRgbToLab65(c))
30 | },
31 |
32 | channels: ['l', 'c', 'h', 'alpha'],
33 |
34 | ranges: {
35 | l: [0, 100],
36 | c: [0, 51.484],
37 | h: [0, 360]
38 | },
39 |
40 | interpolate: {
41 | l: interpolatorLinear,
42 | c: interpolatorLinear,
43 | h: {
44 | use: interpolatorLinear,
45 | fixup: fixupHueShorter
46 | },
47 | alpha: {
48 | use: interpolatorLinear,
49 | fixup: fixupAlpha
50 | }
51 | },
52 |
53 | difference: {
54 | h: differenceHueChroma
55 | },
56 |
57 | average: {
58 | h: averageAngle
59 | }
60 | };
61 |
62 | export default definition;
63 |
--------------------------------------------------------------------------------
/src/easing/gamma.js:
--------------------------------------------------------------------------------
1 | const gamma = (γ = 1) => (γ === 1 ? t => t : t => Math.pow(t, γ));
2 |
3 | export default gamma;
4 |
--------------------------------------------------------------------------------
/src/easing/inOutSine.js:
--------------------------------------------------------------------------------
1 | /*
2 | Sinusoidal (cosine) in-out easing
3 | */
4 | const inOutSine = t => (1 - Math.cos(t * Math.PI)) / 2;
5 |
6 | export default inOutSine;
7 |
--------------------------------------------------------------------------------
/src/easing/midpoint.js:
--------------------------------------------------------------------------------
1 | // Color interpolation hint exponential function
2 | const midpoint = (H = 0.5) => t =>
3 | H <= 0 ? 1 : H >= 1 ? 0 : Math.pow(t, Math.log(0.5) / Math.log(H));
4 |
5 | export default midpoint;
6 |
--------------------------------------------------------------------------------
/src/easing/smootherstep.js:
--------------------------------------------------------------------------------
1 | /*
2 | Smootherstep easing function proposed by K. Perlin
3 | Reference: https://en.wikipedia.org/wiki/Smoothstep
4 | */
5 | const smootherstep = t => t * t * t * (t * (t * 6 - 15) + 10);
6 |
7 | export default smootherstep;
8 |
--------------------------------------------------------------------------------
/src/easing/smoothstep.js:
--------------------------------------------------------------------------------
1 | /*
2 | Smoothstep easing function and its inverse
3 | Reference: https://en.wikipedia.org/wiki/Smoothstep
4 | */
5 | const easingSmoothstep = t => t * t * (3 - 2 * t);
6 | const easingSmoothstepInverse = t => 0.5 - Math.sin(Math.asin(1 - 2 * t) / 3);
7 |
8 | export { easingSmoothstep, easingSmoothstepInverse };
9 |
--------------------------------------------------------------------------------
/src/fixup/alpha.js:
--------------------------------------------------------------------------------
1 | const fixupAlpha = arr => {
2 | let some_defined = false;
3 | let res = arr.map(v => {
4 | if (v !== undefined) {
5 | some_defined = true;
6 | return v;
7 | }
8 | return 1;
9 | });
10 | return some_defined ? res : arr;
11 | };
12 |
13 | export { fixupAlpha };
14 |
--------------------------------------------------------------------------------
/src/fixup/hue.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 |
3 | const hue = (hues, fn) => {
4 | return hues
5 | .map((hue, idx, arr) => {
6 | if (hue === undefined) {
7 | return hue;
8 | }
9 | let normalized = normalizeHue(hue);
10 | if (idx === 0 || hues[idx - 1] === undefined) {
11 | return normalized;
12 | }
13 | return fn(normalized - normalizeHue(arr[idx - 1]));
14 | })
15 | .reduce((acc, curr) => {
16 | if (
17 | !acc.length ||
18 | curr === undefined ||
19 | acc[acc.length - 1] === undefined
20 | ) {
21 | acc.push(curr);
22 | return acc;
23 | }
24 | acc.push(curr + acc[acc.length - 1]);
25 | return acc;
26 | }, []);
27 | };
28 |
29 | const fixupHueShorter = arr =>
30 | hue(arr, d => (Math.abs(d) <= 180 ? d : d - 360 * Math.sign(d)));
31 | const fixupHueLonger = arr =>
32 | hue(arr, d => (Math.abs(d) >= 180 || d === 0 ? d : d - 360 * Math.sign(d)));
33 | const fixupHueIncreasing = arr => hue(arr, d => (d >= 0 ? d : d + 360));
34 | const fixupHueDecreasing = arr => hue(arr, d => (d <= 0 ? d : d - 360));
35 |
36 | export {
37 | fixupHueShorter,
38 | fixupHueLonger,
39 | fixupHueIncreasing,
40 | fixupHueDecreasing
41 | };
42 |
--------------------------------------------------------------------------------
/src/hdr/constants.js:
--------------------------------------------------------------------------------
1 | /*
2 | Relative XYZ has Y=1 for media white,
3 | BT.2048 says media white Y=203 (at PQ 58).
4 | See: https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2408-3-2019-PDF-E.pdf
5 | */
6 | export const YW = 203;
7 |
--------------------------------------------------------------------------------
/src/hdr/transfer.js:
--------------------------------------------------------------------------------
1 | /*
2 | https://en.wikipedia.org/wiki/Transfer_functions_in_imaging
3 | */
4 |
5 | export const M1 = 0.1593017578125;
6 | export const M2 = 78.84375;
7 | export const C1 = 0.8359375;
8 | export const C2 = 18.8515625;
9 | export const C3 = 18.6875;
10 |
11 | /*
12 | Perceptual Quantizer, as defined in Rec. BT 2100-2 (2018)
13 |
14 | * https://www.itu.int/rec/R-REC-BT.2100-2-201807-I/en
15 | * https://en.wikipedia.org/wiki/Perceptual_quantizer
16 | */
17 |
18 | /* PQ EOTF, defined for `v` in [0,1]. */
19 | export function transferPqDecode(v) {
20 | if (v < 0) return 0;
21 | const c = Math.pow(v, 1 / M2);
22 | return 1e4 * Math.pow(Math.max(0, c - C1) / (C2 - C3 * c), 1 / M1);
23 | }
24 |
25 | /* PQ EOTF^-1, defined for `v` in [0, 1e4]. */
26 | export function transferPqEncode(v) {
27 | if (v < 0) return 0;
28 | const c = Math.pow(v / 1e4, M1);
29 | return Math.pow((C1 + C2 * c) / (1 + C3 * c), M2);
30 | }
31 |
--------------------------------------------------------------------------------
/src/hsi/convertHsiToRgb.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 |
3 | // Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB
4 |
5 | export default function convertHsiToRgb({ h, s, i, alpha }) {
6 | h = normalizeHue(h !== undefined ? h : 0);
7 | if (s === undefined) s = 0;
8 | if (i === undefined) i = 0;
9 | let f = Math.abs(((h / 60) % 2) - 1);
10 | let res;
11 | switch (Math.floor(h / 60)) {
12 | case 0:
13 | res = {
14 | r: i * (1 + s * (3 / (2 - f) - 1)),
15 | g: i * (1 + s * ((3 * (1 - f)) / (2 - f) - 1)),
16 | b: i * (1 - s)
17 | };
18 | break;
19 | case 1:
20 | res = {
21 | r: i * (1 + s * ((3 * (1 - f)) / (2 - f) - 1)),
22 | g: i * (1 + s * (3 / (2 - f) - 1)),
23 | b: i * (1 - s)
24 | };
25 | break;
26 | case 2:
27 | res = {
28 | r: i * (1 - s),
29 | g: i * (1 + s * (3 / (2 - f) - 1)),
30 | b: i * (1 + s * ((3 * (1 - f)) / (2 - f) - 1))
31 | };
32 | break;
33 | case 3:
34 | res = {
35 | r: i * (1 - s),
36 | g: i * (1 + s * ((3 * (1 - f)) / (2 - f) - 1)),
37 | b: i * (1 + s * (3 / (2 - f) - 1))
38 | };
39 | break;
40 | case 4:
41 | res = {
42 | r: i * (1 + s * ((3 * (1 - f)) / (2 - f) - 1)),
43 | g: i * (1 - s),
44 | b: i * (1 + s * (3 / (2 - f) - 1))
45 | };
46 | break;
47 | case 5:
48 | res = {
49 | r: i * (1 + s * (3 / (2 - f) - 1)),
50 | g: i * (1 - s),
51 | b: i * (1 + s * ((3 * (1 - f)) / (2 - f) - 1))
52 | };
53 | break;
54 | default:
55 | res = { r: i * (1 - s), g: i * (1 - s), b: i * (1 - s) };
56 | }
57 |
58 | res.mode = 'rgb';
59 | if (alpha !== undefined) res.alpha = alpha;
60 | return res;
61 | }
62 |
--------------------------------------------------------------------------------
/src/hsi/convertRgbToHsi.js:
--------------------------------------------------------------------------------
1 | // Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
2 |
3 | export default function convertRgbToHsi({ r, g, b, alpha }) {
4 | if (r === undefined) r = 0;
5 | if (g === undefined) g = 0;
6 | if (b === undefined) b = 0;
7 | let M = Math.max(r, g, b),
8 | m = Math.min(r, g, b);
9 | let res = {
10 | mode: 'hsi',
11 | s: r + g + b === 0 ? 0 : 1 - (3 * m) / (r + g + b),
12 | i: (r + g + b) / 3
13 | };
14 | if (M - m !== 0)
15 | res.h =
16 | (M === r
17 | ? (g - b) / (M - m) + (g < b) * 6
18 | : M === g
19 | ? (b - r) / (M - m) + 2
20 | : (r - g) / (M - m) + 4) * 60;
21 | if (alpha !== undefined) res.alpha = alpha;
22 | return res;
23 | }
24 |
--------------------------------------------------------------------------------
/src/hsi/definition.js:
--------------------------------------------------------------------------------
1 | import convertHsiToRgb from './convertHsiToRgb.js';
2 | import convertRgbToHsi from './convertRgbToHsi.js';
3 | import { fixupHueShorter } from '../fixup/hue.js';
4 | import { fixupAlpha } from '../fixup/alpha.js';
5 | import { interpolatorLinear } from '../interpolate/linear.js';
6 | import { differenceHueSaturation } from '../difference.js';
7 | import { averageAngle } from '../average.js';
8 |
9 | const definition = {
10 | mode: 'hsi',
11 |
12 | toMode: {
13 | rgb: convertHsiToRgb
14 | },
15 |
16 | parse: ['--hsi'],
17 | serialize: '--hsi',
18 |
19 | fromMode: {
20 | rgb: convertRgbToHsi
21 | },
22 |
23 | channels: ['h', 's', 'i', 'alpha'],
24 |
25 | ranges: {
26 | h: [0, 360]
27 | },
28 |
29 | gamut: 'rgb',
30 |
31 | interpolate: {
32 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
33 | s: interpolatorLinear,
34 | i: interpolatorLinear,
35 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
36 | },
37 |
38 | difference: {
39 | h: differenceHueSaturation
40 | },
41 |
42 | average: {
43 | h: averageAngle
44 | }
45 | };
46 |
47 | export default definition;
48 |
--------------------------------------------------------------------------------
/src/hsl/convertHslToRgb.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 | // Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB
3 |
4 | export default function convertHslToRgb({ h, s, l, alpha }) {
5 | h = normalizeHue(h !== undefined ? h : 0);
6 | if (s === undefined) s = 0;
7 | if (l === undefined) l = 0;
8 | let m1 = l + s * (l < 0.5 ? l : 1 - l);
9 | let m2 = m1 - (m1 - l) * 2 * Math.abs(((h / 60) % 2) - 1);
10 | let res;
11 | switch (Math.floor(h / 60)) {
12 | case 0:
13 | res = { r: m1, g: m2, b: 2 * l - m1 };
14 | break;
15 | case 1:
16 | res = { r: m2, g: m1, b: 2 * l - m1 };
17 | break;
18 | case 2:
19 | res = { r: 2 * l - m1, g: m1, b: m2 };
20 | break;
21 | case 3:
22 | res = { r: 2 * l - m1, g: m2, b: m1 };
23 | break;
24 | case 4:
25 | res = { r: m2, g: 2 * l - m1, b: m1 };
26 | break;
27 | case 5:
28 | res = { r: m1, g: 2 * l - m1, b: m2 };
29 | break;
30 | default:
31 | res = { r: 2 * l - m1, g: 2 * l - m1, b: 2 * l - m1 };
32 | }
33 | res.mode = 'rgb';
34 | if (alpha !== undefined) res.alpha = alpha;
35 | return res;
36 | }
37 |
--------------------------------------------------------------------------------
/src/hsl/convertRgbToHsl.js:
--------------------------------------------------------------------------------
1 | // Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
2 |
3 | export default function convertRgbToHsl({ r, g, b, alpha }) {
4 | if (r === undefined) r = 0;
5 | if (g === undefined) g = 0;
6 | if (b === undefined) b = 0;
7 | let M = Math.max(r, g, b),
8 | m = Math.min(r, g, b);
9 | let res = {
10 | mode: 'hsl',
11 | s: M === m ? 0 : (M - m) / (1 - Math.abs(M + m - 1)),
12 | l: 0.5 * (M + m)
13 | };
14 | if (M - m !== 0)
15 | res.h =
16 | (M === r
17 | ? (g - b) / (M - m) + (g < b) * 6
18 | : M === g
19 | ? (b - r) / (M - m) + 2
20 | : (r - g) / (M - m) + 4) * 60;
21 | if (alpha !== undefined) res.alpha = alpha;
22 | return res;
23 | }
24 |
--------------------------------------------------------------------------------
/src/hsl/definition.js:
--------------------------------------------------------------------------------
1 | import convertHslToRgb from './convertHslToRgb.js';
2 | import convertRgbToHsl from './convertRgbToHsl.js';
3 | import parseHslLegacy from './parseHslLegacy.js';
4 | import parseHsl from './parseHsl.js';
5 | import { fixupHueShorter } from '../fixup/hue.js';
6 | import { fixupAlpha } from '../fixup/alpha.js';
7 | import { interpolatorLinear } from '../interpolate/linear.js';
8 | import { differenceHueSaturation } from '../difference.js';
9 | import { averageAngle } from '../average.js';
10 |
11 | const definition = {
12 | mode: 'hsl',
13 |
14 | toMode: {
15 | rgb: convertHslToRgb
16 | },
17 |
18 | fromMode: {
19 | rgb: convertRgbToHsl
20 | },
21 |
22 | channels: ['h', 's', 'l', 'alpha'],
23 |
24 | ranges: {
25 | h: [0, 360]
26 | },
27 |
28 | gamut: 'rgb',
29 |
30 | parse: [parseHsl, parseHslLegacy],
31 | serialize: c =>
32 | `hsl(${c.h !== undefined ? c.h : 'none'} ${
33 | c.s !== undefined ? c.s * 100 + '%' : 'none'
34 | } ${c.l !== undefined ? c.l * 100 + '%' : 'none'}${
35 | c.alpha < 1 ? ` / ${c.alpha}` : ''
36 | })`,
37 |
38 | interpolate: {
39 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
40 | s: interpolatorLinear,
41 | l: interpolatorLinear,
42 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
43 | },
44 |
45 | difference: {
46 | h: differenceHueSaturation
47 | },
48 |
49 | average: {
50 | h: averageAngle
51 | }
52 | };
53 |
54 | export default definition;
55 |
--------------------------------------------------------------------------------
/src/hsl/parseHsl.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function parseHsl(color, parsed) {
4 | if (!parsed || (parsed[0] !== 'hsl' && parsed[0] !== 'hsla')) {
5 | return undefined;
6 | }
7 | const res = { mode: 'hsl' };
8 | const [, h, s, l, alpha] = parsed;
9 |
10 | if (h.type !== Tok.None) {
11 | if (h.type === Tok.Percentage) {
12 | return undefined;
13 | }
14 | res.h = h.value;
15 | }
16 |
17 | if (s.type !== Tok.None) {
18 | if (s.type === Tok.Hue) {
19 | return undefined;
20 | }
21 | res.s = s.value / 100;
22 | }
23 |
24 | if (l.type !== Tok.None) {
25 | if (l.type === Tok.Hue) {
26 | return undefined;
27 | }
28 | res.l = l.value / 100;
29 | }
30 |
31 | if (alpha.type !== Tok.None) {
32 | res.alpha = Math.min(
33 | 1,
34 | Math.max(
35 | 0,
36 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
37 | )
38 | );
39 | }
40 |
41 | return res;
42 | }
43 |
44 | export default parseHsl;
45 |
--------------------------------------------------------------------------------
/src/hsl/parseHslLegacy.js:
--------------------------------------------------------------------------------
1 | import hueToDeg from '../util/hue.js';
2 | import { hue, per, num_per, c } from '../util/regex.js';
3 |
4 | /*
5 | hsl() regular expressions for legacy format
6 | Reference: https://drafts.csswg.org/css-color/#the-hsl-notation
7 | */
8 | const hsl_old = new RegExp(
9 | `^hsla?\\(\\s*${hue}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
10 | );
11 |
12 | const parseHslLegacy = color => {
13 | let match = color.match(hsl_old);
14 | if (!match) return;
15 | let res = { mode: 'hsl' };
16 |
17 | if (match[3] !== undefined) {
18 | res.h = +match[3];
19 | } else if (match[1] !== undefined && match[2] !== undefined) {
20 | res.h = hueToDeg(match[1], match[2]);
21 | }
22 |
23 | if (match[4] !== undefined) {
24 | res.s = Math.min(Math.max(0, match[4] / 100), 1);
25 | }
26 |
27 | if (match[5] !== undefined) {
28 | res.l = Math.min(Math.max(0, match[5] / 100), 1);
29 | }
30 |
31 | if (match[6] !== undefined) {
32 | res.alpha = Math.max(0, Math.min(1, match[6] / 100));
33 | } else if (match[7] !== undefined) {
34 | res.alpha = Math.max(0, Math.min(1, +match[7]));
35 | }
36 | return res;
37 | };
38 |
39 | export default parseHslLegacy;
40 |
--------------------------------------------------------------------------------
/src/hsv/convertHsvToRgb.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 |
3 | // Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#Converting_to_RGB
4 |
5 | export default function convertHsvToRgb({ h, s, v, alpha }) {
6 | h = normalizeHue(h !== undefined ? h : 0);
7 | if (s === undefined) s = 0;
8 | if (v === undefined) v = 0;
9 | let f = Math.abs(((h / 60) % 2) - 1);
10 | let res;
11 | switch (Math.floor(h / 60)) {
12 | case 0:
13 | res = { r: v, g: v * (1 - s * f), b: v * (1 - s) };
14 | break;
15 | case 1:
16 | res = { r: v * (1 - s * f), g: v, b: v * (1 - s) };
17 | break;
18 | case 2:
19 | res = { r: v * (1 - s), g: v, b: v * (1 - s * f) };
20 | break;
21 | case 3:
22 | res = { r: v * (1 - s), g: v * (1 - s * f), b: v };
23 | break;
24 | case 4:
25 | res = { r: v * (1 - s * f), g: v * (1 - s), b: v };
26 | break;
27 | case 5:
28 | res = { r: v, g: v * (1 - s), b: v * (1 - s * f) };
29 | break;
30 | default:
31 | res = { r: v * (1 - s), g: v * (1 - s), b: v * (1 - s) };
32 | }
33 | res.mode = 'rgb';
34 | if (alpha !== undefined) res.alpha = alpha;
35 | return res;
36 | }
37 |
--------------------------------------------------------------------------------
/src/hsv/convertRgbToHsv.js:
--------------------------------------------------------------------------------
1 | // Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
2 |
3 | export default function convertRgbToHsv({ r, g, b, alpha }) {
4 | if (r === undefined) r = 0;
5 | if (g === undefined) g = 0;
6 | if (b === undefined) b = 0;
7 | let M = Math.max(r, g, b),
8 | m = Math.min(r, g, b);
9 | let res = {
10 | mode: 'hsv',
11 | s: M === 0 ? 0 : 1 - m / M,
12 | v: M
13 | };
14 | if (M - m !== 0)
15 | res.h =
16 | (M === r
17 | ? (g - b) / (M - m) + (g < b) * 6
18 | : M === g
19 | ? (b - r) / (M - m) + 2
20 | : (r - g) / (M - m) + 4) * 60;
21 | if (alpha !== undefined) res.alpha = alpha;
22 | return res;
23 | }
24 |
--------------------------------------------------------------------------------
/src/hsv/definition.js:
--------------------------------------------------------------------------------
1 | import convertHsvToRgb from './convertHsvToRgb.js';
2 | import convertRgbToHsv from './convertRgbToHsv.js';
3 | import { fixupHueShorter } from '../fixup/hue.js';
4 | import { fixupAlpha } from '../fixup/alpha.js';
5 | import { interpolatorLinear } from '../interpolate/linear.js';
6 | import { differenceHueSaturation } from '../difference.js';
7 | import { averageAngle } from '../average.js';
8 |
9 | const definition = {
10 | mode: 'hsv',
11 |
12 | toMode: {
13 | rgb: convertHsvToRgb
14 | },
15 |
16 | parse: ['--hsv'],
17 | serialize: '--hsv',
18 |
19 | fromMode: {
20 | rgb: convertRgbToHsv
21 | },
22 |
23 | channels: ['h', 's', 'v', 'alpha'],
24 |
25 | ranges: {
26 | h: [0, 360]
27 | },
28 |
29 | gamut: 'rgb',
30 |
31 | interpolate: {
32 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
33 | s: interpolatorLinear,
34 | v: interpolatorLinear,
35 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
36 | },
37 |
38 | difference: {
39 | h: differenceHueSaturation
40 | },
41 |
42 | average: {
43 | h: averageAngle
44 | }
45 | };
46 |
47 | export default definition;
48 |
--------------------------------------------------------------------------------
/src/hwb/convertHwbToRgb.js:
--------------------------------------------------------------------------------
1 | /*
2 | HWB to RGB converter
3 | --------------------
4 |
5 | References:
6 | * https://drafts.csswg.org/css-color/#hwb-to-rgb
7 | * https://en.wikipedia.org/wiki/HWB_color_model
8 | * http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
9 | */
10 |
11 | import convertHsvToRgb from '../hsv/convertHsvToRgb.js';
12 |
13 | export default function convertHwbToRgb({ h, w, b, alpha }) {
14 | if (w === undefined) w = 0;
15 | if (b === undefined) b = 0;
16 | // normalize w + b to 1
17 | if (w + b > 1) {
18 | let s = w + b;
19 | w /= s;
20 | b /= s;
21 | }
22 | return convertHsvToRgb({
23 | h: h,
24 | s: b === 1 ? 1 : 1 - w / (1 - b),
25 | v: 1 - b,
26 | alpha: alpha
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/src/hwb/convertRgbToHwb.js:
--------------------------------------------------------------------------------
1 | /*
2 | RGB to HWB converter
3 | --------------------
4 |
5 | References:
6 | * https://drafts.csswg.org/css-color/#hwb-to-rgb
7 | * https://en.wikipedia.org/wiki/HWB_color_model
8 | * http://alvyray.com/Papers/CG/HWB_JGTv208.pdf
9 | */
10 |
11 | import convertRgbToHsv from '../hsv/convertRgbToHsv.js';
12 |
13 | export default function convertRgbToHwb(rgba) {
14 | let hsv = convertRgbToHsv(rgba);
15 | if (hsv === undefined) return undefined;
16 | let s = hsv.s !== undefined ? hsv.s : 0;
17 | let v = hsv.v !== undefined ? hsv.v : 0;
18 | let res = {
19 | mode: 'hwb',
20 | w: (1 - s) * v,
21 | b: 1 - v
22 | };
23 | if (hsv.h !== undefined) res.h = hsv.h;
24 | if (hsv.alpha !== undefined) res.alpha = hsv.alpha;
25 | return res;
26 | }
27 |
--------------------------------------------------------------------------------
/src/hwb/definition.js:
--------------------------------------------------------------------------------
1 | import convertHwbToRgb from './convertHwbToRgb.js';
2 | import convertRgbToHwb from './convertRgbToHwb.js';
3 | import parseHwb from './parseHwb.js';
4 | import { fixupHueShorter } from '../fixup/hue.js';
5 | import { fixupAlpha } from '../fixup/alpha.js';
6 | import { interpolatorLinear } from '../interpolate/linear.js';
7 | import { differenceHueNaive } from '../difference.js';
8 | import { averageAngle } from '../average.js';
9 |
10 | const definition = {
11 | mode: 'hwb',
12 |
13 | toMode: {
14 | rgb: convertHwbToRgb
15 | },
16 |
17 | fromMode: {
18 | rgb: convertRgbToHwb
19 | },
20 |
21 | channels: ['h', 'w', 'b', 'alpha'],
22 |
23 | ranges: {
24 | h: [0, 360]
25 | },
26 |
27 | gamut: 'rgb',
28 |
29 | parse: [parseHwb],
30 | serialize: c =>
31 | `hwb(${c.h !== undefined ? c.h : 'none'} ${
32 | c.w !== undefined ? c.w * 100 + '%' : 'none'
33 | } ${c.b !== undefined ? c.b * 100 + '%' : 'none'}${
34 | c.alpha < 1 ? ` / ${c.alpha}` : ''
35 | })`,
36 |
37 | interpolate: {
38 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
39 | w: interpolatorLinear,
40 | b: interpolatorLinear,
41 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
42 | },
43 |
44 | difference: {
45 | h: differenceHueNaive
46 | },
47 |
48 | average: {
49 | h: averageAngle
50 | }
51 | };
52 |
53 | export default definition;
54 |
--------------------------------------------------------------------------------
/src/hwb/parseHwb.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function ParseHwb(color, parsed) {
4 | if (!parsed || parsed[0] !== 'hwb') {
5 | return undefined;
6 | }
7 | const res = { mode: 'hwb' };
8 | const [, h, w, b, alpha] = parsed;
9 |
10 | if (h.type !== Tok.None) {
11 | if (h.type === Tok.Percentage) {
12 | return undefined;
13 | }
14 | res.h = h.value;
15 | }
16 |
17 | if (w.type !== Tok.None) {
18 | if (w.type === Tok.Hue) {
19 | return undefined;
20 | }
21 | res.w = w.value / 100;
22 | }
23 |
24 | if (b.type !== Tok.None) {
25 | if (b.type === Tok.Hue) {
26 | return undefined;
27 | }
28 | res.b = b.value / 100;
29 | }
30 |
31 | if (alpha.type !== Tok.None) {
32 | res.alpha = Math.min(
33 | 1,
34 | Math.max(
35 | 0,
36 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
37 | )
38 | );
39 | }
40 |
41 | return res;
42 | }
43 |
44 | export default ParseHwb;
45 |
--------------------------------------------------------------------------------
/src/interpolate/lerp.js:
--------------------------------------------------------------------------------
1 | const lerp = (a, b, t) => a + t * (b - a);
2 | const unlerp = (a, b, v) => (v - a) / (b - a);
3 |
4 | const blerp = (a00, a01, a10, a11, tx, ty) => {
5 | return lerp(lerp(a00, a01, tx), lerp(a10, a11, tx), ty);
6 | };
7 |
8 | const trilerp = (
9 | a000,
10 | a010,
11 | a100,
12 | a110,
13 | a001,
14 | a011,
15 | a101,
16 | a111,
17 | tx,
18 | ty,
19 | tz
20 | ) => {
21 | return lerp(
22 | blerp(a000, a010, a100, a110, tx, ty),
23 | blerp(a001, a011, a101, a111, tx, ty),
24 | tz
25 | );
26 | };
27 |
28 | export { lerp, blerp, trilerp, unlerp };
29 |
--------------------------------------------------------------------------------
/src/interpolate/linear.js:
--------------------------------------------------------------------------------
1 | import { lerp } from './lerp.js';
2 | import { interpolatorPiecewise } from './piecewise.js';
3 |
4 | export const interpolatorLinear = interpolatorPiecewise(lerp);
5 |
--------------------------------------------------------------------------------
/src/interpolate/piecewise.js:
--------------------------------------------------------------------------------
1 | const get_classes = arr => {
2 | let classes = [];
3 | for (let i = 0; i < arr.length - 1; i++) {
4 | let a = arr[i];
5 | let b = arr[i + 1];
6 | if (a === undefined && b === undefined) {
7 | classes.push(undefined);
8 | } else if (a !== undefined && b !== undefined) {
9 | classes.push([a, b]);
10 | } else {
11 | classes.push(a !== undefined ? [a, a] : [b, b]);
12 | }
13 | }
14 | return classes;
15 | };
16 |
17 | const interpolatorPiecewise = interpolator => arr => {
18 | let classes = get_classes(arr);
19 | return t => {
20 | let cls = t * classes.length;
21 | let idx = t >= 1 ? classes.length - 1 : Math.max(Math.floor(cls), 0);
22 | let pair = classes[idx];
23 | return pair === undefined
24 | ? undefined
25 | : interpolator(pair[0], pair[1], cls - idx);
26 | };
27 | };
28 |
29 | export { interpolatorPiecewise };
30 |
--------------------------------------------------------------------------------
/src/interpolate/splineBasis.js:
--------------------------------------------------------------------------------
1 | /*
2 | Basis spline
3 | ------------
4 |
5 | Given control points V0...Vn (our values)
6 |
7 | S0 = V0
8 | ...
9 | Si = 1/6 * Vi-1 + 2/3 * Vi + 1/6 * Vi+1
10 | ...
11 | Sn = Vn
12 |
13 | The Bézier curve has control points:
14 |
15 | Bi = Si-1, 2/3 * Vi-1 + 1/3 * Vi, 1/3 * Vi-1 + 2/3 * Vi, Si
16 |
17 | Which we can then factor into the Bezier's explicit form:
18 |
19 | B(t) = (1-t)^3 * P0 + 3 * (1-t)^2 * t * P1 + (1-t) * t^2 * P2 + t^3 * P3
20 |
21 | */
22 | const mod = (v, l) => (v + l) % l;
23 |
24 | const bspline = (Vim2, Vim1, Vi, Vip1, t) => {
25 | let t2 = t * t;
26 | let t3 = t2 * t;
27 | return (
28 | ((1 - 3 * t + 3 * t2 - t3) * Vim2 +
29 | (4 - 6 * t2 + 3 * t3) * Vim1 +
30 | (1 + 3 * t + 3 * t2 - 3 * t3) * Vi +
31 | t3 * Vip1) /
32 | 6
33 | );
34 | };
35 |
36 | export const interpolatorSplineBasis = arr => t => {
37 | let classes = arr.length - 1;
38 | let i = t >= 1 ? classes - 1 : Math.max(0, Math.floor(t * classes));
39 | return bspline(
40 | i > 0 ? arr[i - 1] : 2 * arr[i] - arr[i + 1],
41 | arr[i],
42 | arr[i + 1],
43 | i < classes - 1 ? arr[i + 2] : 2 * arr[i + 1] - arr[i],
44 | (t - i / classes) * classes
45 | );
46 | };
47 |
48 | export const interpolatorSplineBasisClosed = arr => t => {
49 | const classes = arr.length - 1;
50 | const i = Math.floor(t * classes);
51 | return bspline(
52 | arr[mod(i - 1, arr.length)],
53 | arr[mod(i, arr.length)],
54 | arr[mod(i + 1, arr.length)],
55 | arr[mod(i + 2, arr.length)],
56 | (t - i / classes) * classes
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/interpolate/splineNatural.js:
--------------------------------------------------------------------------------
1 | import {
2 | interpolatorSplineBasisClosed,
3 | interpolatorSplineBasis
4 | } from './splineBasis.js';
5 |
6 | const solve = v => {
7 | let i;
8 | let n = v.length - 1;
9 | let c = new Array(n);
10 | let _v = new Array(n);
11 | let sol = new Array(n);
12 |
13 | c[1] = 1 / 4;
14 | _v[1] = (6 * v[1] - v[0]) / 4;
15 |
16 | for (i = 2; i < n; ++i) {
17 | c[i] = 1 / (4 - c[i - 1]);
18 | _v[i] = (6 * v[i] - (i == n - 1 ? v[n] : 0) - _v[i - 1]) * c[i];
19 | }
20 |
21 | sol[0] = v[0];
22 | sol[n] = v[n];
23 | if (n - 1 > 0) {
24 | sol[n - 1] = _v[n - 1];
25 | }
26 |
27 | for (i = n - 2; i > 0; --i) {
28 | sol[i] = _v[i] - c[i] * sol[i + 1];
29 | }
30 |
31 | return sol;
32 | };
33 |
34 | export const interpolatorSplineNatural = arr =>
35 | interpolatorSplineBasis(solve(arr));
36 | export const interpolatorSplineNaturalClosed = arr =>
37 | interpolatorSplineBasisClosed(solve(arr));
38 |
--------------------------------------------------------------------------------
/src/itp/convertItpToXyz65.js:
--------------------------------------------------------------------------------
1 | import { YW } from '../hdr/constants.js';
2 | import { transferPqDecode } from '../hdr/transfer.js';
3 |
4 | const toRel = c => Math.max(c / YW, 0);
5 |
6 | const convertItpToXyz65 = ({ i, t, p, alpha }) => {
7 | if (i === undefined) i = 0;
8 | if (t === undefined) t = 0;
9 | if (p === undefined) p = 0;
10 |
11 | const l = transferPqDecode(
12 | i + 0.008609037037932761 * t + 0.11102962500302593 * p
13 | );
14 | const m = transferPqDecode(
15 | i - 0.00860903703793275 * t - 0.11102962500302599 * p
16 | );
17 | const s = transferPqDecode(
18 | i + 0.5600313357106791 * t - 0.32062717498731885 * p
19 | );
20 |
21 | const res = {
22 | mode: 'xyz65',
23 | x: toRel(
24 | 2.0701522183894219 * l -
25 | 1.3263473389671556 * m +
26 | 0.2066510476294051 * s
27 | ),
28 | y: toRel(
29 | 0.3647385209748074 * l +
30 | 0.680566024947227 * m -
31 | 0.0453045459220346 * s
32 | ),
33 | z: toRel(
34 | -0.049747207535812 * l -
35 | 0.0492609666966138 * m +
36 | 1.1880659249923042 * s
37 | )
38 | };
39 |
40 | if (alpha !== undefined) {
41 | res.alpha = alpha;
42 | }
43 |
44 | return res;
45 | };
46 |
47 | export default convertItpToXyz65;
48 |
--------------------------------------------------------------------------------
/src/itp/convertXyz65ToItp.js:
--------------------------------------------------------------------------------
1 | import { YW } from '../hdr/constants.js';
2 | import { transferPqEncode } from '../hdr/transfer.js';
3 |
4 | const toAbs = (c = 0) => Math.max(c * YW, 0);
5 |
6 | const convertXyz65ToItp = ({ x, y, z, alpha }) => {
7 | const absX = toAbs(x);
8 | const absY = toAbs(y);
9 | const absZ = toAbs(z);
10 | const l = transferPqEncode(
11 | 0.3592832590121217 * absX +
12 | 0.6976051147779502 * absY -
13 | 0.0358915932320289 * absZ
14 | );
15 | const m = transferPqEncode(
16 | -0.1920808463704995 * absX +
17 | 1.1004767970374323 * absY +
18 | 0.0753748658519118 * absZ
19 | );
20 | const s = transferPqEncode(
21 | 0.0070797844607477 * absX +
22 | 0.0748396662186366 * absY +
23 | 0.8433265453898765 * absZ
24 | );
25 |
26 | const i = 0.5 * l + 0.5 * m;
27 | const t = 1.61376953125 * l - 3.323486328125 * m + 1.709716796875 * s;
28 | const p = 4.378173828125 * l - 4.24560546875 * m - 0.132568359375 * s;
29 |
30 | const res = { mode: 'itp', i, t, p };
31 | if (alpha !== undefined) {
32 | res.alpha = alpha;
33 | }
34 |
35 | return res;
36 | };
37 |
38 | export default convertXyz65ToItp;
39 |
--------------------------------------------------------------------------------
/src/itp/definition.js:
--------------------------------------------------------------------------------
1 | import { interpolatorLinear } from '../interpolate/linear.js';
2 | import { fixupAlpha } from '../fixup/alpha.js';
3 | import convertItpToXyz65 from './convertItpToXyz65.js';
4 | import convertXyz65ToItp from './convertXyz65ToItp.js';
5 | import convertRgbToXyz65 from '../xyz65/convertRgbToXyz65.js';
6 | import convertXyz65ToRgb from '../xyz65/convertXyz65ToRgb.js';
7 |
8 | /*
9 | ICtCp (or ITP) color space, as defined in ITU-R Recommendation BT.2100.
10 |
11 | ICtCp is drafted to be supported in CSS within
12 | [CSS Color HDR Module Level 1](https://drafts.csswg.org/css-color-hdr/#ICtCp) spec.
13 | */
14 |
15 | const definition = {
16 | mode: 'itp',
17 | channels: ['i', 't', 'p', 'alpha'],
18 | parse: ['--ictcp'],
19 | serialize: '--ictcp',
20 |
21 | toMode: {
22 | xyz65: convertItpToXyz65,
23 | rgb: color => convertXyz65ToRgb(convertItpToXyz65(color))
24 | },
25 |
26 | fromMode: {
27 | xyz65: convertXyz65ToItp,
28 | rgb: color => convertXyz65ToItp(convertRgbToXyz65(color))
29 | },
30 |
31 | ranges: {
32 | i: [0, 0.581],
33 | t: [-0.369, 0.272],
34 | p: [-0.164, 0.331]
35 | },
36 |
37 | interpolate: {
38 | i: interpolatorLinear,
39 | t: interpolatorLinear,
40 | p: interpolatorLinear,
41 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
42 | }
43 | };
44 |
45 | export default definition;
46 |
--------------------------------------------------------------------------------
/src/jab/convertJabToRgb.js:
--------------------------------------------------------------------------------
1 | import convertXyz65ToRgb from '../xyz65/convertXyz65ToRgb.js';
2 | import convertJabToXyz65 from './convertJabToXyz65.js';
3 |
4 | const convertJabToRgb = color => convertXyz65ToRgb(convertJabToXyz65(color));
5 |
6 | export default convertJabToRgb;
7 |
--------------------------------------------------------------------------------
/src/jab/convertJabToXyz65.js:
--------------------------------------------------------------------------------
1 | import { M1 as n, C1, C2, C3 } from '../hdr/transfer.js';
2 | const p = 134.03437499999998; // = 1.7 * 2523 / Math.pow(2, 5);
3 | const d0 = 1.6295499532821566e-11;
4 |
5 | /*
6 | The encoding function is derived from Perceptual Quantizer.
7 | */
8 | const jabPqDecode = v => {
9 | if (v < 0) return 0;
10 | let vp = Math.pow(v, 1 / p);
11 | return 10000 * Math.pow((C1 - vp) / (C3 * vp - C2), 1 / n);
12 | };
13 |
14 | const rel = v => v / 203;
15 |
16 | const convertJabToXyz65 = ({ j, a, b, alpha }) => {
17 | if (j === undefined) j = 0;
18 | if (a === undefined) a = 0;
19 | if (b === undefined) b = 0;
20 | let i = (j + d0) / (0.44 + 0.56 * (j + d0));
21 |
22 | let l = jabPqDecode(i + 0.13860504 * a + 0.058047316 * b);
23 | let m = jabPqDecode(i - 0.13860504 * a - 0.058047316 * b);
24 | let s = jabPqDecode(i - 0.096019242 * a - 0.8118919 * b);
25 |
26 | let res = {
27 | mode: 'xyz65',
28 | x: rel(
29 | 1.661373024652174 * l -
30 | 0.914523081304348 * m +
31 | 0.23136208173913045 * s
32 | ),
33 | y: rel(
34 | -0.3250758611844533 * l +
35 | 1.571847026732543 * m -
36 | 0.21825383453227928 * s
37 | ),
38 | z: rel(-0.090982811 * l - 0.31272829 * m + 1.5227666 * s)
39 | };
40 |
41 | if (alpha !== undefined) {
42 | res.alpha = alpha;
43 | }
44 |
45 | return res;
46 | };
47 |
48 | export default convertJabToXyz65;
49 |
--------------------------------------------------------------------------------
/src/jab/convertRgbToJab.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert sRGB to JzAzBz.
3 |
4 | For achromatic sRGB colors, adjust the equivalent JzAzBz color
5 | to be achromatic as well, insteading of having a very slight chroma.
6 | */
7 |
8 | import convertXyz65ToJab from './convertXyz65ToJab.js';
9 | import convertRgbToXyz65 from '../xyz65/convertRgbToXyz65.js';
10 |
11 | const convertRgbToJab = rgb => {
12 | let res = convertXyz65ToJab(convertRgbToXyz65(rgb));
13 | if (rgb.r === rgb.b && rgb.b === rgb.g) {
14 | res.a = res.b = 0;
15 | }
16 | return res;
17 | };
18 |
19 | export default convertRgbToJab;
20 |
--------------------------------------------------------------------------------
/src/jab/convertXyz65ToJab.js:
--------------------------------------------------------------------------------
1 | import { M1 as n, C1, C2, C3 } from '../hdr/transfer.js';
2 | const p = 134.03437499999998; // = 1.7 * 2523 / Math.pow(2, 5);
3 | const d0 = 1.6295499532821566e-11;
4 |
5 | /*
6 | The encoding function is derived from Perceptual Quantizer.
7 | */
8 | const jabPqEncode = v => {
9 | if (v < 0) return 0;
10 | let vn = Math.pow(v / 10000, n);
11 | return Math.pow((C1 + C2 * vn) / (1 + C3 * vn), p);
12 | };
13 |
14 | // Convert to Absolute XYZ
15 | const abs = (v = 0) => Math.max(v * 203, 0);
16 |
17 | const convertXyz65ToJab = ({ x, y, z, alpha }) => {
18 | x = abs(x);
19 | y = abs(y);
20 | z = abs(z);
21 |
22 | let xp = 1.15 * x - 0.15 * z;
23 | let yp = 0.66 * y + 0.34 * x;
24 |
25 | let l = jabPqEncode(0.41478972 * xp + 0.579999 * yp + 0.014648 * z);
26 | let m = jabPqEncode(-0.20151 * xp + 1.120649 * yp + 0.0531008 * z);
27 | let s = jabPqEncode(-0.0166008 * xp + 0.2648 * yp + 0.6684799 * z);
28 |
29 | let i = (l + m) / 2;
30 |
31 | let res = {
32 | mode: 'jab',
33 | j: (0.44 * i) / (1 - 0.56 * i) - d0,
34 | a: 3.524 * l - 4.066708 * m + 0.542708 * s,
35 | b: 0.199076 * l + 1.096799 * m - 1.295875 * s
36 | };
37 |
38 | if (alpha !== undefined) {
39 | res.alpha = alpha;
40 | }
41 |
42 | return res;
43 | };
44 |
45 | export default convertXyz65ToJab;
46 |
--------------------------------------------------------------------------------
/src/jab/definition.js:
--------------------------------------------------------------------------------
1 | /*
2 | The JzAzBz color space.
3 |
4 | Based on:
5 |
6 | Muhammad Safdar, Guihua Cui, Youn Jin Kim, and Ming Ronnier Luo,
7 | "Perceptually uniform color space for image signals
8 | including high dynamic range and wide gamut,"
9 | Opt. Express 25, 15131-15151 (2017)
10 |
11 | https://doi.org/10.1364/OE.25.015131
12 | */
13 |
14 | import convertXyz65ToJab from './convertXyz65ToJab.js';
15 | import convertJabToXyz65 from './convertJabToXyz65.js';
16 | import convertRgbToJab from './convertRgbToJab.js';
17 | import convertJabToRgb from './convertJabToRgb.js';
18 |
19 | import { interpolatorLinear } from '../interpolate/linear.js';
20 | import { fixupAlpha } from '../fixup/alpha.js';
21 |
22 | const definition = {
23 | mode: 'jab',
24 | channels: ['j', 'a', 'b', 'alpha'],
25 |
26 | parse: ['--jzazbz'],
27 | serialize: '--jzazbz',
28 |
29 | fromMode: {
30 | rgb: convertRgbToJab,
31 | xyz65: convertXyz65ToJab
32 | },
33 |
34 | toMode: {
35 | rgb: convertJabToRgb,
36 | xyz65: convertJabToXyz65
37 | },
38 |
39 | ranges: {
40 | j: [0, 0.222],
41 | a: [-0.109, 0.129],
42 | b: [-0.185, 0.134]
43 | },
44 |
45 | interpolate: {
46 | j: interpolatorLinear,
47 | a: interpolatorLinear,
48 | b: interpolatorLinear,
49 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
50 | }
51 | };
52 |
53 | export default definition;
54 |
--------------------------------------------------------------------------------
/src/jch/convertJabToJch.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 |
3 | const convertJabToJch = ({ j, a, b, alpha }) => {
4 | if (a === undefined) a = 0;
5 | if (b === undefined) b = 0;
6 | let c = Math.sqrt(a * a + b * b);
7 | let res = {
8 | mode: 'jch',
9 | j,
10 | c
11 | };
12 | if (c) {
13 | res.h = normalizeHue((Math.atan2(b, a) * 180) / Math.PI);
14 | }
15 | if (alpha !== undefined) {
16 | res.alpha = alpha;
17 | }
18 | return res;
19 | };
20 |
21 | export default convertJabToJch;
22 |
--------------------------------------------------------------------------------
/src/jch/convertJchToJab.js:
--------------------------------------------------------------------------------
1 | const convertJchToJab = ({ j, c, h, alpha }) => {
2 | if (h === undefined) h = 0;
3 | let res = {
4 | mode: 'jab',
5 | j,
6 | a: c ? c * Math.cos((h / 180) * Math.PI) : 0,
7 | b: c ? c * Math.sin((h / 180) * Math.PI) : 0
8 | };
9 | if (alpha !== undefined) res.alpha = alpha;
10 | return res;
11 | };
12 |
13 | export default convertJchToJab;
14 |
--------------------------------------------------------------------------------
/src/jch/definition.js:
--------------------------------------------------------------------------------
1 | import convertJabToJch from './convertJabToJch.js';
2 | import convertJchToJab from './convertJchToJab.js';
3 | import convertJabToRgb from '../jab/convertJabToRgb.js';
4 | import convertRgbToJab from '../jab/convertRgbToJab.js';
5 |
6 | import { fixupHueShorter } from '../fixup/hue.js';
7 | import { fixupAlpha } from '../fixup/alpha.js';
8 | import { interpolatorLinear } from '../interpolate/linear.js';
9 | import { differenceHueChroma } from '../difference.js';
10 | import { averageAngle } from '../average.js';
11 |
12 | const definition = {
13 | mode: 'jch',
14 |
15 | parse: ['--jzczhz'],
16 | serialize: '--jzczhz',
17 |
18 | toMode: {
19 | jab: convertJchToJab,
20 | rgb: c => convertJabToRgb(convertJchToJab(c))
21 | },
22 |
23 | fromMode: {
24 | rgb: c => convertJabToJch(convertRgbToJab(c)),
25 | jab: convertJabToJch
26 | },
27 |
28 | channels: ['j', 'c', 'h', 'alpha'],
29 |
30 | ranges: {
31 | j: [0, 0.221],
32 | c: [0, 0.19],
33 | h: [0, 360]
34 | },
35 |
36 | interpolate: {
37 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
38 | c: interpolatorLinear,
39 | j: interpolatorLinear,
40 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
41 | },
42 |
43 | difference: {
44 | h: differenceHueChroma
45 | },
46 |
47 | average: {
48 | h: averageAngle
49 | }
50 | };
51 |
52 | export default definition;
53 |
--------------------------------------------------------------------------------
/src/lab/convertLabToRgb.js:
--------------------------------------------------------------------------------
1 | import convertLabToXyz50 from './convertLabToXyz50.js';
2 | import convertXyz50ToRgb from '../xyz50/convertXyz50ToRgb.js';
3 |
4 | const convertLabToRgb = lab => convertXyz50ToRgb(convertLabToXyz50(lab));
5 |
6 | export default convertLabToRgb;
7 |
--------------------------------------------------------------------------------
/src/lab/convertLabToXyz50.js:
--------------------------------------------------------------------------------
1 | import { k, e } from '../xyz50/constants.js';
2 | import { D50 } from '../constants.js';
3 |
4 | let fn = v => (Math.pow(v, 3) > e ? Math.pow(v, 3) : (116 * v - 16) / k);
5 |
6 | const convertLabToXyz50 = ({ l, a, b, alpha }) => {
7 | if (l === undefined) l = 0;
8 | if (a === undefined) a = 0;
9 | if (b === undefined) b = 0;
10 | let fy = (l + 16) / 116;
11 | let fx = a / 500 + fy;
12 | let fz = fy - b / 200;
13 |
14 | let res = {
15 | mode: 'xyz50',
16 | x: fn(fx) * D50.X,
17 | y: fn(fy) * D50.Y,
18 | z: fn(fz) * D50.Z
19 | };
20 |
21 | if (alpha !== undefined) {
22 | res.alpha = alpha;
23 | }
24 |
25 | return res;
26 | };
27 |
28 | export default convertLabToXyz50;
29 |
--------------------------------------------------------------------------------
/src/lab/convertRgbToLab.js:
--------------------------------------------------------------------------------
1 | import convertRgbToXyz50 from '../xyz50/convertRgbToXyz50.js';
2 | import convertXyz50ToLab from './convertXyz50ToLab.js';
3 |
4 | const convertRgbToLab = rgb => {
5 | let res = convertXyz50ToLab(convertRgbToXyz50(rgb));
6 |
7 | // Fixes achromatic RGB colors having a _slight_ chroma due to floating-point errors
8 | // and approximated computations in sRGB <-> CIELab.
9 | // See: https://github.com/d3/d3-color/pull/46
10 | if (rgb.r === rgb.b && rgb.b === rgb.g) {
11 | res.a = res.b = 0;
12 | }
13 | return res;
14 | };
15 |
16 | export default convertRgbToLab;
17 |
--------------------------------------------------------------------------------
/src/lab/convertXyz50ToLab.js:
--------------------------------------------------------------------------------
1 | import { k, e } from '../xyz50/constants.js';
2 | import { D50 } from '../constants.js';
3 |
4 | const f = value => (value > e ? Math.cbrt(value) : (k * value + 16) / 116);
5 |
6 | const convertXyz50ToLab = ({ x, y, z, alpha }) => {
7 | if (x === undefined) x = 0;
8 | if (y === undefined) y = 0;
9 | if (z === undefined) z = 0;
10 | let f0 = f(x / D50.X);
11 | let f1 = f(y / D50.Y);
12 | let f2 = f(z / D50.Z);
13 |
14 | let res = {
15 | mode: 'lab',
16 | l: 116 * f1 - 16,
17 | a: 500 * (f0 - f1),
18 | b: 200 * (f1 - f2)
19 | };
20 |
21 | if (alpha !== undefined) {
22 | res.alpha = alpha;
23 | }
24 |
25 | return res;
26 | };
27 |
28 | export default convertXyz50ToLab;
29 |
--------------------------------------------------------------------------------
/src/lab/definition.js:
--------------------------------------------------------------------------------
1 | import convertLabToRgb from './convertLabToRgb.js';
2 | import convertLabToXyz50 from './convertLabToXyz50.js';
3 | import convertRgbToLab from './convertRgbToLab.js';
4 | import convertXyz50ToLab from './convertXyz50ToLab.js';
5 | import parseLab from './parseLab.js';
6 | import { interpolatorLinear } from '../interpolate/linear.js';
7 | import { fixupAlpha } from '../fixup/alpha.js';
8 |
9 | const definition = {
10 | mode: 'lab',
11 |
12 | toMode: {
13 | xyz50: convertLabToXyz50,
14 | rgb: convertLabToRgb
15 | },
16 |
17 | fromMode: {
18 | xyz50: convertXyz50ToLab,
19 | rgb: convertRgbToLab
20 | },
21 |
22 | channels: ['l', 'a', 'b', 'alpha'],
23 |
24 | ranges: {
25 | l: [0, 100],
26 | a: [-100, 100],
27 | b: [-100, 100]
28 | },
29 |
30 | parse: [parseLab],
31 | serialize: c =>
32 | `lab(${c.l !== undefined ? c.l : 'none'} ${
33 | c.a !== undefined ? c.a : 'none'
34 | } ${c.b !== undefined ? c.b : 'none'}${
35 | c.alpha < 1 ? ` / ${c.alpha}` : ''
36 | })`,
37 |
38 | interpolate: {
39 | l: interpolatorLinear,
40 | a: interpolatorLinear,
41 | b: interpolatorLinear,
42 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
43 | }
44 | };
45 |
46 | export default definition;
47 |
--------------------------------------------------------------------------------
/src/lab/parseLab.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function parseLab(color, parsed) {
4 | if (!parsed || parsed[0] !== 'lab') {
5 | return undefined;
6 | }
7 | const res = { mode: 'lab' };
8 | const [, l, a, b, alpha] = parsed;
9 | if (l.type === Tok.Hue || a.type === Tok.Hue || b.type === Tok.Hue) {
10 | return undefined;
11 | }
12 | if (l.type !== Tok.None) {
13 | res.l = Math.min(Math.max(0, l.value), 100);
14 | }
15 | if (a.type !== Tok.None) {
16 | res.a = a.type === Tok.Number ? a.value : (a.value * 125) / 100;
17 | }
18 | if (b.type !== Tok.None) {
19 | res.b = b.type === Tok.Number ? b.value : (b.value * 125) / 100;
20 | }
21 | if (alpha.type !== Tok.None) {
22 | res.alpha = Math.min(
23 | 1,
24 | Math.max(
25 | 0,
26 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
27 | )
28 | );
29 | }
30 |
31 | return res;
32 | }
33 |
34 | export default parseLab;
35 |
--------------------------------------------------------------------------------
/src/lab65/convertLab65ToRgb.js:
--------------------------------------------------------------------------------
1 | import convertLab65ToXyz65 from './convertLab65ToXyz65.js';
2 | import convertXyz65ToRgb from '../xyz65/convertXyz65ToRgb.js';
3 |
4 | const convertLab65ToRgb = lab => convertXyz65ToRgb(convertLab65ToXyz65(lab));
5 |
6 | export default convertLab65ToRgb;
7 |
--------------------------------------------------------------------------------
/src/lab65/convertLab65ToXyz65.js:
--------------------------------------------------------------------------------
1 | import { k, e } from '../xyz65/constants.js';
2 | import { D65 } from '../constants.js';
3 |
4 | let fn = v => (Math.pow(v, 3) > e ? Math.pow(v, 3) : (116 * v - 16) / k);
5 |
6 | const convertLab65ToXyz65 = ({ l, a, b, alpha }) => {
7 | if (l === undefined) l = 0;
8 | if (a === undefined) a = 0;
9 | if (b === undefined) b = 0;
10 |
11 | let fy = (l + 16) / 116;
12 | let fx = a / 500 + fy;
13 | let fz = fy - b / 200;
14 |
15 | let res = {
16 | mode: 'xyz65',
17 | x: fn(fx) * D65.X,
18 | y: fn(fy) * D65.Y,
19 | z: fn(fz) * D65.Z
20 | };
21 |
22 | if (alpha !== undefined) {
23 | res.alpha = alpha;
24 | }
25 |
26 | return res;
27 | };
28 |
29 | export default convertLab65ToXyz65;
30 |
--------------------------------------------------------------------------------
/src/lab65/convertRgbToLab65.js:
--------------------------------------------------------------------------------
1 | import convertRgbToXyz65 from '../xyz65/convertRgbToXyz65.js';
2 | import convertXyz65ToLab65 from './convertXyz65ToLab65.js';
3 |
4 | const convertRgbToLab65 = rgb => {
5 | let res = convertXyz65ToLab65(convertRgbToXyz65(rgb));
6 |
7 | // Fixes achromatic RGB colors having a _slight_ chroma due to floating-point errors
8 | // and approximated computations in sRGB <-> CIELab.
9 | // See: https://github.com/d3/d3-color/pull/46
10 | if (rgb.r === rgb.b && rgb.b === rgb.g) {
11 | res.a = res.b = 0;
12 | }
13 | return res;
14 | };
15 |
16 | export default convertRgbToLab65;
17 |
--------------------------------------------------------------------------------
/src/lab65/convertXyz65ToLab65.js:
--------------------------------------------------------------------------------
1 | import { k, e } from '../xyz65/constants.js';
2 | import { D65 } from '../constants.js';
3 |
4 | const f = value => (value > e ? Math.cbrt(value) : (k * value + 16) / 116);
5 |
6 | const convertXyz65ToLab65 = ({ x, y, z, alpha }) => {
7 | if (x === undefined) x = 0;
8 | if (y === undefined) y = 0;
9 | if (z === undefined) z = 0;
10 | let f0 = f(x / D65.X);
11 | let f1 = f(y / D65.Y);
12 | let f2 = f(z / D65.Z);
13 |
14 | let res = {
15 | mode: 'lab65',
16 | l: 116 * f1 - 16,
17 | a: 500 * (f0 - f1),
18 | b: 200 * (f1 - f2)
19 | };
20 |
21 | if (alpha !== undefined) {
22 | res.alpha = alpha;
23 | }
24 |
25 | return res;
26 | };
27 |
28 | export default convertXyz65ToLab65;
29 |
--------------------------------------------------------------------------------
/src/lab65/definition.js:
--------------------------------------------------------------------------------
1 | import convertLab65ToRgb from './convertLab65ToRgb.js';
2 | import convertLab65ToXyz65 from './convertLab65ToXyz65.js';
3 | import convertRgbToLab65 from './convertRgbToLab65.js';
4 | import convertXyz65ToLab65 from './convertXyz65ToLab65.js';
5 | import lab from '../lab/definition.js';
6 |
7 | const definition = {
8 | ...lab,
9 | mode: 'lab65',
10 |
11 | parse: ['--lab-d65'],
12 | serialize: '--lab-d65',
13 |
14 | toMode: {
15 | xyz65: convertLab65ToXyz65,
16 | rgb: convertLab65ToRgb
17 | },
18 |
19 | fromMode: {
20 | xyz65: convertXyz65ToLab65,
21 | rgb: convertRgbToLab65
22 | },
23 |
24 | ranges: {
25 | l: [0, 100],
26 | a: [-86.182, 98.234],
27 | b: [-107.86, 94.477]
28 | }
29 | };
30 |
31 | export default definition;
32 |
--------------------------------------------------------------------------------
/src/lch/convertLabToLch.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 |
3 | /*
4 | References:
5 | * https://drafts.csswg.org/css-color/#lab-to-lch
6 | * https://drafts.csswg.org/css-color/#color-conversion-code
7 | */
8 | const convertLabToLch = ({ l, a, b, alpha }, mode = 'lch') => {
9 | if (a === undefined) a = 0;
10 | if (b === undefined) b = 0;
11 | let c = Math.sqrt(a * a + b * b);
12 | let res = { mode, l, c };
13 | if (c) res.h = normalizeHue((Math.atan2(b, a) * 180) / Math.PI);
14 | if (alpha !== undefined) res.alpha = alpha;
15 | return res;
16 | };
17 |
18 | export default convertLabToLch;
19 |
--------------------------------------------------------------------------------
/src/lch/convertLchToLab.js:
--------------------------------------------------------------------------------
1 | /*
2 | References:
3 | * https://drafts.csswg.org/css-color/#lch-to-lab
4 | * https://drafts.csswg.org/css-color/#color-conversion-code
5 | */
6 | const convertLchToLab = ({ l, c, h, alpha }, mode = 'lab') => {
7 | if (h === undefined) h = 0;
8 | let res = {
9 | mode,
10 | l,
11 | a: c ? c * Math.cos((h / 180) * Math.PI) : 0,
12 | b: c ? c * Math.sin((h / 180) * Math.PI) : 0
13 | };
14 | if (alpha !== undefined) res.alpha = alpha;
15 | return res;
16 | };
17 |
18 | export default convertLchToLab;
19 |
--------------------------------------------------------------------------------
/src/lch/definition.js:
--------------------------------------------------------------------------------
1 | import convertLabToLch from './convertLabToLch.js';
2 | import convertLchToLab from './convertLchToLab.js';
3 | import convertLabToRgb from '../lab/convertLabToRgb.js';
4 | import convertRgbToLab from '../lab/convertRgbToLab.js';
5 | import parseLch from './parseLch.js';
6 | import { fixupHueShorter } from '../fixup/hue.js';
7 | import { fixupAlpha } from '../fixup/alpha.js';
8 | import { interpolatorLinear } from '../interpolate/linear.js';
9 | import { differenceHueChroma } from '../difference.js';
10 | import { averageAngle } from '../average.js';
11 |
12 | const definition = {
13 | mode: 'lch',
14 |
15 | toMode: {
16 | lab: convertLchToLab,
17 | rgb: c => convertLabToRgb(convertLchToLab(c))
18 | },
19 |
20 | fromMode: {
21 | rgb: c => convertLabToLch(convertRgbToLab(c)),
22 | lab: convertLabToLch
23 | },
24 |
25 | channels: ['l', 'c', 'h', 'alpha'],
26 |
27 | ranges: {
28 | l: [0, 100],
29 | c: [0, 150],
30 | h: [0, 360]
31 | },
32 |
33 | parse: [parseLch],
34 | serialize: c =>
35 | `lch(${c.l !== undefined ? c.l : 'none'} ${
36 | c.c !== undefined ? c.c : 'none'
37 | } ${c.h !== undefined ? c.h : 'none'}${
38 | c.alpha < 1 ? ` / ${c.alpha}` : ''
39 | })`,
40 |
41 | interpolate: {
42 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
43 | c: interpolatorLinear,
44 | l: interpolatorLinear,
45 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
46 | },
47 |
48 | difference: {
49 | h: differenceHueChroma
50 | },
51 |
52 | average: {
53 | h: averageAngle
54 | }
55 | };
56 |
57 | export default definition;
58 |
--------------------------------------------------------------------------------
/src/lch/parseLch.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function parseLch(color, parsed) {
4 | if (!parsed || parsed[0] !== 'lch') {
5 | return undefined;
6 | }
7 | const res = { mode: 'lch' };
8 | const [, l, c, h, alpha] = parsed;
9 | if (l.type !== Tok.None) {
10 | if (l.type === Tok.Hue) {
11 | return undefined;
12 | }
13 | res.l = Math.min(Math.max(0, l.value), 100);
14 | }
15 | if (c.type !== Tok.None) {
16 | res.c = Math.max(
17 | 0,
18 | c.type === Tok.Number ? c.value : (c.value * 150) / 100
19 | );
20 | }
21 | if (h.type !== Tok.None) {
22 | if (h.type === Tok.Percentage) {
23 | return undefined;
24 | }
25 | res.h = h.value;
26 | }
27 | if (alpha.type !== Tok.None) {
28 | res.alpha = Math.min(
29 | 1,
30 | Math.max(
31 | 0,
32 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
33 | )
34 | );
35 | }
36 |
37 | return res;
38 | }
39 |
40 | export default parseLch;
41 |
--------------------------------------------------------------------------------
/src/lch65/definition.js:
--------------------------------------------------------------------------------
1 | import convertLabToLch from '../lch/convertLabToLch.js';
2 | import convertLchToLab from '../lch/convertLchToLab.js';
3 | import convertLab65ToRgb from '../lab65/convertLab65ToRgb.js';
4 | import convertRgbToLab65 from '../lab65/convertRgbToLab65.js';
5 | import lch from '../lch/definition.js';
6 |
7 | const definition = {
8 | ...lch,
9 | mode: 'lch65',
10 |
11 | parse: ['--lch-d65'],
12 | serialize: '--lch-d65',
13 |
14 | toMode: {
15 | lab65: c => convertLchToLab(c, 'lab65'),
16 | rgb: c => convertLab65ToRgb(convertLchToLab(c, 'lab65'))
17 | },
18 |
19 | fromMode: {
20 | rgb: c => convertLabToLch(convertRgbToLab65(c), 'lch65'),
21 | lab65: c => convertLabToLch(c, 'lch65')
22 | },
23 |
24 | ranges: {
25 | l: [0, 100],
26 | c: [0, 133.807],
27 | h: [0, 360]
28 | }
29 | };
30 |
31 | export default definition;
32 |
--------------------------------------------------------------------------------
/src/lchuv/convertLchuvToLuv.js:
--------------------------------------------------------------------------------
1 | const convertLchuvToLuv = ({ l, c, h, alpha }) => {
2 | if (h === undefined) h = 0;
3 | let res = {
4 | mode: 'luv',
5 | l: l,
6 | u: c ? c * Math.cos((h / 180) * Math.PI) : 0,
7 | v: c ? c * Math.sin((h / 180) * Math.PI) : 0
8 | };
9 | if (alpha !== undefined) {
10 | res.alpha = alpha;
11 | }
12 | return res;
13 | };
14 |
15 | export default convertLchuvToLuv;
16 |
--------------------------------------------------------------------------------
/src/lchuv/convertLuvToLchuv.js:
--------------------------------------------------------------------------------
1 | import normalizeHue from '../util/normalizeHue.js';
2 |
3 | const convertLuvToLchuv = ({ l, u, v, alpha }) => {
4 | if (u === undefined) u = 0;
5 | if (v === undefined) v = 0;
6 | let c = Math.sqrt(u * u + v * v);
7 | let res = {
8 | mode: 'lchuv',
9 | l: l,
10 | c: c
11 | };
12 | if (c) {
13 | res.h = normalizeHue((Math.atan2(v, u) * 180) / Math.PI);
14 | }
15 | if (alpha !== undefined) {
16 | res.alpha = alpha;
17 | }
18 | return res;
19 | };
20 |
21 | export default convertLuvToLchuv;
22 |
--------------------------------------------------------------------------------
/src/lchuv/definition.js:
--------------------------------------------------------------------------------
1 | /*
2 | CIELChuv color space
3 | --------------------
4 |
5 | Reference:
6 |
7 | https://en.wikipedia.org/wiki/CIELUV
8 | */
9 |
10 | import convertLuvToLchuv from './convertLuvToLchuv.js';
11 | import convertLchuvToLuv from './convertLchuvToLuv.js';
12 | import convertXyz50ToLuv from '../luv/convertXyz50ToLuv.js';
13 | import convertLuvToXyz50 from '../luv/convertLuvToXyz50.js';
14 | import convertXyz50ToRgb from '../xyz50/convertXyz50ToRgb.js';
15 | import convertRgbToXyz50 from '../xyz50/convertRgbToXyz50.js';
16 |
17 | import { fixupHueShorter } from '../fixup/hue.js';
18 | import { fixupAlpha } from '../fixup/alpha.js';
19 | import { interpolatorLinear } from '../interpolate/linear.js';
20 | import { differenceHueChroma } from '../difference.js';
21 | import { averageAngle } from '../average.js';
22 |
23 | const convertRgbToLchuv = rgb =>
24 | convertLuvToLchuv(convertXyz50ToLuv(convertRgbToXyz50(rgb)));
25 | const convertLchuvToRgb = lchuv =>
26 | convertXyz50ToRgb(convertLuvToXyz50(convertLchuvToLuv(lchuv)));
27 |
28 | const definition = {
29 | mode: 'lchuv',
30 |
31 | toMode: {
32 | luv: convertLchuvToLuv,
33 | rgb: convertLchuvToRgb
34 | },
35 |
36 | fromMode: {
37 | rgb: convertRgbToLchuv,
38 | luv: convertLuvToLchuv
39 | },
40 |
41 | channels: ['l', 'c', 'h', 'alpha'],
42 |
43 | parse: ['--lchuv'],
44 | serialize: '--lchuv',
45 |
46 | ranges: {
47 | l: [0, 100],
48 | c: [0, 176.956],
49 | h: [0, 360]
50 | },
51 |
52 | interpolate: {
53 | h: { use: interpolatorLinear, fixup: fixupHueShorter },
54 | c: interpolatorLinear,
55 | l: interpolatorLinear,
56 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
57 | },
58 |
59 | difference: {
60 | h: differenceHueChroma
61 | },
62 |
63 | average: {
64 | h: averageAngle
65 | }
66 | };
67 |
68 | export default definition;
69 |
--------------------------------------------------------------------------------
/src/lrgb/convertLrgbToRgb.js:
--------------------------------------------------------------------------------
1 | const fn = (c = 0) => {
2 | const abs = Math.abs(c);
3 | if (abs > 0.0031308) {
4 | return (Math.sign(c) || 1) * (1.055 * Math.pow(abs, 1 / 2.4) - 0.055);
5 | }
6 | return c * 12.92;
7 | };
8 |
9 | const convertLrgbToRgb = ({ r, g, b, alpha }, mode = 'rgb') => {
10 | let res = {
11 | mode,
12 | r: fn(r),
13 | g: fn(g),
14 | b: fn(b)
15 | };
16 | if (alpha !== undefined) res.alpha = alpha;
17 | return res;
18 | };
19 |
20 | export default convertLrgbToRgb;
21 |
--------------------------------------------------------------------------------
/src/lrgb/convertRgbToLrgb.js:
--------------------------------------------------------------------------------
1 | const fn = (c = 0) => {
2 | const abs = Math.abs(c);
3 | if (abs <= 0.04045) {
4 | return c / 12.92;
5 | }
6 | return (Math.sign(c) || 1) * Math.pow((abs + 0.055) / 1.055, 2.4);
7 | };
8 |
9 | const convertRgbToLrgb = ({ r, g, b, alpha }) => {
10 | let res = {
11 | mode: 'lrgb',
12 | r: fn(r),
13 | g: fn(g),
14 | b: fn(b)
15 | };
16 | if (alpha !== undefined) res.alpha = alpha;
17 | return res;
18 | };
19 |
20 | export default convertRgbToLrgb;
21 |
--------------------------------------------------------------------------------
/src/lrgb/definition.js:
--------------------------------------------------------------------------------
1 | import rgb from '../rgb/definition.js';
2 | import convertRgbToLrgb from './convertRgbToLrgb.js';
3 | import convertLrgbToRgb from './convertLrgbToRgb.js';
4 |
5 | const definition = {
6 | ...rgb,
7 | mode: 'lrgb',
8 |
9 | toMode: {
10 | rgb: convertLrgbToRgb
11 | },
12 |
13 | fromMode: {
14 | rgb: convertRgbToLrgb
15 | },
16 |
17 | parse: ['srgb-linear'],
18 | serialize: 'srgb-linear'
19 | };
20 |
21 | export default definition;
22 |
--------------------------------------------------------------------------------
/src/luv/convertLuvToXyz50.js:
--------------------------------------------------------------------------------
1 | import { k } from '../xyz50/constants.js';
2 | import { D50 } from '../constants.js';
3 |
4 | export const u_fn = (x, y, z) => (4 * x) / (x + 15 * y + 3 * z);
5 | export const v_fn = (x, y, z) => (9 * y) / (x + 15 * y + 3 * z);
6 |
7 | export const un = u_fn(D50.X, D50.Y, D50.Z);
8 | export const vn = v_fn(D50.X, D50.Y, D50.Z);
9 |
10 | const convertLuvToXyz50 = ({ l, u, v, alpha }) => {
11 | if (l === undefined) l = 0;
12 | if (l === 0) {
13 | return { mode: 'xyz50', x: 0, y: 0, z: 0 };
14 | }
15 |
16 | if (u === undefined) u = 0;
17 | if (v === undefined) v = 0;
18 |
19 | let up = u / (13 * l) + un;
20 | let vp = v / (13 * l) + vn;
21 | let y = D50.Y * (l <= 8 ? l / k : Math.pow((l + 16) / 116, 3));
22 | let x = (y * (9 * up)) / (4 * vp);
23 | let z = (y * (12 - 3 * up - 20 * vp)) / (4 * vp);
24 |
25 | let res = { mode: 'xyz50', x, y, z };
26 | if (alpha !== undefined) {
27 | res.alpha = alpha;
28 | }
29 |
30 | return res;
31 | };
32 |
33 | export default convertLuvToXyz50;
34 |
--------------------------------------------------------------------------------
/src/luv/convertXyz50ToLuv.js:
--------------------------------------------------------------------------------
1 | import { k, e } from '../xyz50/constants.js';
2 | import { D50 } from '../constants.js';
3 |
4 | export const u_fn = (x, y, z) => (4 * x) / (x + 15 * y + 3 * z);
5 | export const v_fn = (x, y, z) => (9 * y) / (x + 15 * y + 3 * z);
6 |
7 | export const un = u_fn(D50.X, D50.Y, D50.Z);
8 | export const vn = v_fn(D50.X, D50.Y, D50.Z);
9 |
10 | const l_fn = value => (value <= e ? k * value : 116 * Math.cbrt(value) - 16);
11 |
12 | const convertXyz50ToLuv = ({ x, y, z, alpha }) => {
13 | if (x === undefined) x = 0;
14 | if (y === undefined) y = 0;
15 | if (z === undefined) z = 0;
16 | let l = l_fn(y / D50.Y);
17 | let u = u_fn(x, y, z);
18 | let v = v_fn(x, y, z);
19 |
20 | // guard against NaNs produced by `xyz(0 0 0)` black
21 | if (!isFinite(u) || !isFinite(v)) {
22 | l = u = v = 0;
23 | } else {
24 | u = 13 * l * (u - un);
25 | v = 13 * l * (v - vn);
26 | }
27 |
28 | let res = {
29 | mode: 'luv',
30 | l,
31 | u,
32 | v
33 | };
34 |
35 | if (alpha !== undefined) {
36 | res.alpha = alpha;
37 | }
38 |
39 | return res;
40 | };
41 |
42 | export default convertXyz50ToLuv;
43 |
--------------------------------------------------------------------------------
/src/luv/definition.js:
--------------------------------------------------------------------------------
1 | /*
2 | CIELUV color space
3 | ------------------
4 |
5 | Reference:
6 |
7 | https://en.wikipedia.org/wiki/CIELUV
8 | */
9 |
10 | import convertXyz50ToLuv from './convertXyz50ToLuv.js';
11 | import convertLuvToXyz50 from './convertLuvToXyz50.js';
12 | import convertXyz50ToRgb from '../xyz50/convertXyz50ToRgb.js';
13 | import convertRgbToXyz50 from '../xyz50/convertRgbToXyz50.js';
14 |
15 | import { interpolatorLinear } from '../interpolate/linear.js';
16 | import { fixupAlpha } from '../fixup/alpha.js';
17 |
18 | const definition = {
19 | mode: 'luv',
20 |
21 | toMode: {
22 | xyz50: convertLuvToXyz50,
23 | rgb: luv => convertXyz50ToRgb(convertLuvToXyz50(luv))
24 | },
25 |
26 | fromMode: {
27 | xyz50: convertXyz50ToLuv,
28 | rgb: rgb => convertXyz50ToLuv(convertRgbToXyz50(rgb))
29 | },
30 |
31 | channels: ['l', 'u', 'v', 'alpha'],
32 |
33 | parse: ['--luv'],
34 | serialize: '--luv',
35 |
36 | ranges: {
37 | l: [0, 100],
38 | u: [-84.936, 175.042],
39 | v: [-125.882, 87.243]
40 | },
41 |
42 | interpolate: {
43 | l: interpolatorLinear,
44 | u: interpolatorLinear,
45 | v: interpolatorLinear,
46 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
47 | }
48 | };
49 |
50 | export default definition;
51 |
--------------------------------------------------------------------------------
/src/map.js:
--------------------------------------------------------------------------------
1 | import converter from './converter.js';
2 | import prepare from './_prepare.js';
3 | import { getMode } from './modes.js';
4 |
5 | const mapper = (fn, mode = 'rgb', preserve_mode = false) => {
6 | let channels = mode ? getMode(mode).channels : null;
7 | let conv = mode ? converter(mode) : prepare;
8 | return color => {
9 | let conv_color = conv(color);
10 | if (!conv_color) {
11 | return undefined;
12 | }
13 | let res = (channels || getMode(conv_color.mode).channels).reduce(
14 | (res, ch) => {
15 | let v = fn(conv_color[ch], ch, conv_color, mode);
16 | if (v !== undefined && !isNaN(v)) {
17 | res[ch] = v;
18 | }
19 | return res;
20 | },
21 | { mode: conv_color.mode }
22 | );
23 | if (!preserve_mode) {
24 | return res;
25 | }
26 | let prep = prepare(color);
27 | if (prep && prep.mode !== res.mode) {
28 | return converter(prep.mode)(res);
29 | }
30 | return res;
31 | };
32 | };
33 |
34 | const mapAlphaMultiply = (v, ch, c) => {
35 | if (ch !== 'alpha') {
36 | return (v || 0) * (c.alpha !== undefined ? c.alpha : 1);
37 | }
38 | return v;
39 | };
40 |
41 | const mapAlphaDivide = (v, ch, c) => {
42 | if (ch !== 'alpha' && c.alpha !== 0) {
43 | return (v || 0) / (c.alpha !== undefined ? c.alpha : 1);
44 | }
45 | return v;
46 | };
47 |
48 | const mapTransferLinear =
49 | (slope = 1, intercept = 0) =>
50 | (v, ch) => {
51 | if (ch !== 'alpha') {
52 | return v * slope + intercept;
53 | }
54 | return v;
55 | };
56 |
57 | const mapTransferGamma =
58 | (amplitude = 1, exponent = 1, offset = 0) =>
59 | (v, ch) => {
60 | if (ch !== 'alpha') {
61 | return amplitude * Math.pow(v, exponent) + offset;
62 | }
63 | return v;
64 | };
65 |
66 | export {
67 | mapper,
68 | mapAlphaMultiply,
69 | mapAlphaDivide,
70 | mapTransferLinear,
71 | mapTransferGamma
72 | };
73 |
--------------------------------------------------------------------------------
/src/nearest.js:
--------------------------------------------------------------------------------
1 | import { differenceEuclidean } from './difference.js';
2 |
3 | /*
4 | This works linearly right now, but we might get better performance
5 | with a V-P Tree (Vantage Point Tree).
6 |
7 | Reference:
8 | * http://pnylab.com/papers/vptree/main.html
9 | */
10 |
11 | const nearest = (colors, metric = differenceEuclidean(), accessor = d => d) => {
12 | let arr = colors.map((c, idx) => ({ color: accessor(c), i: idx }));
13 | return (color, n = 1, τ = Infinity) => {
14 | if (isFinite(n)) {
15 | n = Math.max(1, Math.min(n, arr.length - 1));
16 | }
17 |
18 | arr.forEach(c => {
19 | c.d = metric(color, c.color);
20 | });
21 |
22 | return arr
23 | .sort((a, b) => a.d - b.d)
24 | .slice(0, n)
25 | .filter(c => c.d < τ)
26 | .map(c => colors[c.i]);
27 | };
28 | };
29 |
30 | export default nearest;
31 |
--------------------------------------------------------------------------------
/src/okhsl/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Björn Ottosson
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/src/okhsl/modeOkhsl.js:
--------------------------------------------------------------------------------
1 | import convertRgbToOklab from '../oklab/convertRgbToOklab.js';
2 | import convertOklabToRgb from '../oklab/convertOklabToRgb.js';
3 | import convertOklabToOkhsl from './convertOklabToOkhsl.js';
4 | import convertOkhslToOklab from './convertOkhslToOklab.js';
5 |
6 | import modeHsl from '../hsl/definition.js';
7 |
8 | const modeOkhsl = {
9 | ...modeHsl,
10 | mode: 'okhsl',
11 | channels: ['h', 's', 'l', 'alpha'],
12 | parse: ['--okhsl'],
13 | serialize: '--okhsl',
14 | fromMode: {
15 | oklab: convertOklabToOkhsl,
16 | rgb: c => convertOklabToOkhsl(convertRgbToOklab(c))
17 | },
18 | toMode: {
19 | oklab: convertOkhslToOklab,
20 | rgb: c => convertOklabToRgb(convertOkhslToOklab(c))
21 | }
22 | };
23 |
24 | export default modeOkhsl;
25 |
--------------------------------------------------------------------------------
/src/okhsv/modeOkhsv.js:
--------------------------------------------------------------------------------
1 | import convertRgbToOklab from '../oklab/convertRgbToOklab.js';
2 | import convertOklabToRgb from '../oklab/convertOklabToRgb.js';
3 | import convertOklabToOkhsv from './convertOklabToOkhsv.js';
4 | import convertOkhsvToOklab from './convertOkhsvToOklab.js';
5 |
6 | import modeHsv from '../hsv/definition.js';
7 |
8 | const modeOkhsv = {
9 | ...modeHsv,
10 | mode: 'okhsv',
11 | channels: ['h', 's', 'v', 'alpha'],
12 | parse: ['--okhsv'],
13 | serialize: '--okhsv',
14 | fromMode: {
15 | oklab: convertOklabToOkhsv,
16 | rgb: c => convertOklabToOkhsv(convertRgbToOklab(c))
17 | },
18 | toMode: {
19 | oklab: convertOkhsvToOklab,
20 | rgb: c => convertOklabToRgb(convertOkhsvToOklab(c))
21 | }
22 | };
23 |
24 | export default modeOkhsv;
25 |
--------------------------------------------------------------------------------
/src/oklab/convertLrgbToOklab.js:
--------------------------------------------------------------------------------
1 | const convertLrgbToOklab = ({ r, g, b, alpha }) => {
2 | if (r === undefined) r = 0;
3 | if (g === undefined) g = 0;
4 | if (b === undefined) b = 0;
5 | let L = Math.cbrt(
6 | 0.41222147079999993 * r + 0.5363325363 * g + 0.0514459929 * b
7 | );
8 | let M = Math.cbrt(
9 | 0.2119034981999999 * r + 0.6806995450999999 * g + 0.1073969566 * b
10 | );
11 | let S = Math.cbrt(
12 | 0.08830246189999998 * r + 0.2817188376 * g + 0.6299787005000002 * b
13 | );
14 |
15 | let res = {
16 | mode: 'oklab',
17 | l: 0.2104542553 * L + 0.793617785 * M - 0.0040720468 * S,
18 | a: 1.9779984951 * L - 2.428592205 * M + 0.4505937099 * S,
19 | b: 0.0259040371 * L + 0.7827717662 * M - 0.808675766 * S
20 | };
21 |
22 | if (alpha !== undefined) {
23 | res.alpha = alpha;
24 | }
25 |
26 | return res;
27 | };
28 |
29 | export default convertLrgbToOklab;
30 |
--------------------------------------------------------------------------------
/src/oklab/convertOklabToLrgb.js:
--------------------------------------------------------------------------------
1 | const convertOklabToLrgb = ({ l, a, b, alpha }) => {
2 | if (l === undefined) l = 0;
3 | if (a === undefined) a = 0;
4 | if (b === undefined) b = 0;
5 | let L = Math.pow(
6 | l * 0.99999999845051981432 +
7 | 0.39633779217376785678 * a +
8 | 0.21580375806075880339 * b,
9 | 3
10 | );
11 | let M = Math.pow(
12 | l * 1.0000000088817607767 -
13 | 0.1055613423236563494 * a -
14 | 0.063854174771705903402 * b,
15 | 3
16 | );
17 | let S = Math.pow(
18 | l * 1.0000000546724109177 -
19 | 0.089484182094965759684 * a -
20 | 1.2914855378640917399 * b,
21 | 3
22 | );
23 |
24 | let res = {
25 | mode: 'lrgb',
26 | r:
27 | +4.076741661347994 * L -
28 | 3.307711590408193 * M +
29 | 0.230969928729428 * S,
30 | g:
31 | -1.2684380040921763 * L +
32 | 2.6097574006633715 * M -
33 | 0.3413193963102197 * S,
34 | b:
35 | -0.004196086541837188 * L -
36 | 0.7034186144594493 * M +
37 | 1.7076147009309444 * S
38 | };
39 |
40 | if (alpha !== undefined) {
41 | res.alpha = alpha;
42 | }
43 |
44 | return res;
45 | };
46 |
47 | export default convertOklabToLrgb;
48 |
--------------------------------------------------------------------------------
/src/oklab/convertOklabToRgb.js:
--------------------------------------------------------------------------------
1 | import convertLrgbToRgb from '../lrgb/convertLrgbToRgb.js';
2 | import convertOklabToLrgb from './convertOklabToLrgb.js';
3 |
4 | const convertOklabToRgb = c => convertLrgbToRgb(convertOklabToLrgb(c));
5 |
6 | export default convertOklabToRgb;
7 |
--------------------------------------------------------------------------------
/src/oklab/convertRgbToOklab.js:
--------------------------------------------------------------------------------
1 | import convertRgbToLrgb from '../lrgb/convertRgbToLrgb.js';
2 | import convertLrgbToOklab from './convertLrgbToOklab.js';
3 |
4 | const convertRgbToOklab = rgb => {
5 | let res = convertLrgbToOklab(convertRgbToLrgb(rgb));
6 | if (rgb.r === rgb.b && rgb.b === rgb.g) {
7 | res.a = res.b = 0;
8 | }
9 | return res;
10 | };
11 |
12 | export default convertRgbToOklab;
13 |
--------------------------------------------------------------------------------
/src/oklab/definition.js:
--------------------------------------------------------------------------------
1 | import convertOklabToLrgb from './convertOklabToLrgb.js';
2 | import convertLrgbToOklab from './convertLrgbToOklab.js';
3 | import convertRgbToOklab from './convertRgbToOklab.js';
4 | import convertOklabToRgb from './convertOklabToRgb.js';
5 | import parseOklab from './parseOklab.js';
6 |
7 | import lab from '../lab/definition.js';
8 |
9 | /*
10 | Oklab, a perceptual color space for image processing by Björn Ottosson
11 | Reference: https://bottosson.github.io/posts/oklab/
12 | */
13 |
14 | const definition = {
15 | ...lab,
16 | mode: 'oklab',
17 |
18 | toMode: {
19 | lrgb: convertOklabToLrgb,
20 | rgb: convertOklabToRgb
21 | },
22 |
23 | fromMode: {
24 | lrgb: convertLrgbToOklab,
25 | rgb: convertRgbToOklab
26 | },
27 |
28 | ranges: {
29 | l: [0, 1],
30 | a: [-0.4, 0.4],
31 | b: [-0.4, 0.4]
32 | },
33 |
34 | parse: [parseOklab],
35 | serialize: c =>
36 | `oklab(${c.l !== undefined ? c.l : 'none'} ${
37 | c.a !== undefined ? c.a : 'none'
38 | } ${c.b !== undefined ? c.b : 'none'}${
39 | c.alpha < 1 ? ` / ${c.alpha}` : ''
40 | })`
41 | };
42 |
43 | export default definition;
44 |
--------------------------------------------------------------------------------
/src/oklab/parseOklab.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function parseOklab(color, parsed) {
4 | if (!parsed || parsed[0] !== 'oklab') {
5 | return undefined;
6 | }
7 | const res = { mode: 'oklab' };
8 | const [, l, a, b, alpha] = parsed;
9 | if (l.type === Tok.Hue || a.type === Tok.Hue || b.type === Tok.Hue) {
10 | return undefined;
11 | }
12 | if (l.type !== Tok.None) {
13 | res.l = Math.min(
14 | Math.max(0, l.type === Tok.Number ? l.value : l.value / 100),
15 | 1
16 | );
17 | }
18 | if (a.type !== Tok.None) {
19 | res.a = a.type === Tok.Number ? a.value : (a.value * 0.4) / 100;
20 | }
21 | if (b.type !== Tok.None) {
22 | res.b = b.type === Tok.Number ? b.value : (b.value * 0.4) / 100;
23 | }
24 | if (alpha.type !== Tok.None) {
25 | res.alpha = Math.min(
26 | 1,
27 | Math.max(
28 | 0,
29 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
30 | )
31 | );
32 | }
33 |
34 | return res;
35 | }
36 |
37 | export default parseOklab;
38 |
--------------------------------------------------------------------------------
/src/oklch/definition.js:
--------------------------------------------------------------------------------
1 | import lch from '../lch/definition.js';
2 | import convertLabToLch from '../lch/convertLabToLch.js';
3 | import convertLchToLab from '../lch/convertLchToLab.js';
4 | import convertOklabToRgb from '../oklab/convertOklabToRgb.js';
5 | import convertRgbToOklab from '../oklab/convertRgbToOklab.js';
6 | import parseOklch from './parseOklch.js';
7 |
8 | const definition = {
9 | ...lch,
10 | mode: 'oklch',
11 |
12 | toMode: {
13 | oklab: c => convertLchToLab(c, 'oklab'),
14 | rgb: c => convertOklabToRgb(convertLchToLab(c, 'oklab'))
15 | },
16 |
17 | fromMode: {
18 | rgb: c => convertLabToLch(convertRgbToOklab(c), 'oklch'),
19 | oklab: c => convertLabToLch(c, 'oklch')
20 | },
21 |
22 | parse: [parseOklch],
23 | serialize: c =>
24 | `oklch(${c.l !== undefined ? c.l : 'none'} ${
25 | c.c !== undefined ? c.c : 'none'
26 | } ${c.h !== undefined ? c.h : 'none'}${
27 | c.alpha < 1 ? ` / ${c.alpha}` : ''
28 | })`,
29 |
30 | ranges: {
31 | l: [0, 1],
32 | c: [0, 0.4],
33 | h: [0, 360]
34 | }
35 | };
36 |
37 | export default definition;
38 |
--------------------------------------------------------------------------------
/src/oklch/parseOklch.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function parseOklch(color, parsed) {
4 | if (!parsed || parsed[0] !== 'oklch') {
5 | return undefined;
6 | }
7 | const res = { mode: 'oklch' };
8 | const [, l, c, h, alpha] = parsed;
9 | if (l.type !== Tok.None) {
10 | if (l.type === Tok.Hue) {
11 | return undefined;
12 | }
13 | res.l = Math.min(
14 | Math.max(0, l.type === Tok.Number ? l.value : l.value / 100),
15 | 1
16 | );
17 | }
18 | if (c.type !== Tok.None) {
19 | res.c = Math.max(
20 | 0,
21 | c.type === Tok.Number ? c.value : (c.value * 0.4) / 100
22 | );
23 | }
24 | if (h.type !== Tok.None) {
25 | if (h.type === Tok.Percentage) {
26 | return undefined;
27 | }
28 | res.h = h.value;
29 | }
30 | if (alpha.type !== Tok.None) {
31 | res.alpha = Math.min(
32 | 1,
33 | Math.max(
34 | 0,
35 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
36 | )
37 | );
38 | }
39 |
40 | return res;
41 | }
42 |
43 | export default parseOklch;
44 |
--------------------------------------------------------------------------------
/src/p3/convertP3ToXyz65.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert Display P3 values to CIE XYZ D65
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | */
8 |
9 | import convertRgbToLrgb from '../lrgb/convertRgbToLrgb.js';
10 |
11 | const convertP3ToXyz65 = rgb => {
12 | let { r, g, b, alpha } = convertRgbToLrgb(rgb);
13 | let res = {
14 | mode: 'xyz65',
15 | x:
16 | 0.486570948648216 * r +
17 | 0.265667693169093 * g +
18 | 0.1982172852343625 * b,
19 | y:
20 | 0.2289745640697487 * r +
21 | 0.6917385218365062 * g +
22 | 0.079286914093745 * b,
23 | z: 0.0 * r + 0.0451133818589026 * g + 1.043944368900976 * b
24 | };
25 | if (alpha !== undefined) {
26 | res.alpha = alpha;
27 | }
28 | return res;
29 | };
30 |
31 | export default convertP3ToXyz65;
32 |
--------------------------------------------------------------------------------
/src/p3/convertXyz65ToP3.js:
--------------------------------------------------------------------------------
1 | /*
2 | CIE XYZ D65 values to Display P3.
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | */
8 |
9 | import convertLrgbToRgb from '../lrgb/convertLrgbToRgb.js';
10 |
11 | const convertXyz65ToP3 = ({ x, y, z, alpha }) => {
12 | if (x === undefined) x = 0;
13 | if (y === undefined) y = 0;
14 | if (z === undefined) z = 0;
15 | let res = convertLrgbToRgb(
16 | {
17 | r:
18 | x * 2.4934969119414263 -
19 | y * 0.9313836179191242 -
20 | 0.402710784450717 * z,
21 | g:
22 | x * -0.8294889695615749 +
23 | y * 1.7626640603183465 +
24 | 0.0236246858419436 * z,
25 | b:
26 | x * 0.0358458302437845 -
27 | y * 0.0761723892680418 +
28 | 0.9568845240076871 * z
29 | },
30 | 'p3'
31 | );
32 | if (alpha !== undefined) {
33 | res.alpha = alpha;
34 | }
35 | return res;
36 | };
37 |
38 | export default convertXyz65ToP3;
39 |
--------------------------------------------------------------------------------
/src/p3/definition.js:
--------------------------------------------------------------------------------
1 | import rgb from '../rgb/definition.js';
2 | import convertP3ToXyz65 from './convertP3ToXyz65.js';
3 | import convertXyz65ToP3 from './convertXyz65ToP3.js';
4 | import convertRgbToXyz65 from '../xyz65/convertRgbToXyz65.js';
5 | import convertXyz65ToRgb from '../xyz65/convertXyz65ToRgb.js';
6 |
7 | const definition = {
8 | ...rgb,
9 | mode: 'p3',
10 | parse: ['display-p3'],
11 | serialize: 'display-p3',
12 |
13 | fromMode: {
14 | rgb: color => convertXyz65ToP3(convertRgbToXyz65(color)),
15 | xyz65: convertXyz65ToP3
16 | },
17 |
18 | toMode: {
19 | rgb: color => convertXyz65ToRgb(convertP3ToXyz65(color)),
20 | xyz65: convertP3ToXyz65
21 | }
22 | };
23 |
24 | export default definition;
25 |
--------------------------------------------------------------------------------
/src/prophoto/convertProphotoToXyz50.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert ProPhoto RGB values to CIE XYZ D50
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | */
8 |
9 | const linearize = (v = 0) => {
10 | let abs = Math.abs(v);
11 | if (abs >= 16 / 512) {
12 | return Math.sign(v) * Math.pow(abs, 1.8);
13 | }
14 | return v / 16;
15 | };
16 |
17 | const convertProphotoToXyz50 = prophoto => {
18 | let r = linearize(prophoto.r);
19 | let g = linearize(prophoto.g);
20 | let b = linearize(prophoto.b);
21 | let res = {
22 | mode: 'xyz50',
23 | x:
24 | 0.7977666449006423 * r +
25 | 0.1351812974005331 * g +
26 | 0.0313477341283922 * b,
27 | y:
28 | 0.2880748288194013 * r +
29 | 0.7118352342418731 * g +
30 | 0.0000899369387256 * b,
31 | z: 0 * r + 0 * g + 0.8251046025104602 * b
32 | };
33 | if (prophoto.alpha !== undefined) {
34 | res.alpha = prophoto.alpha;
35 | }
36 | return res;
37 | };
38 |
39 | export default convertProphotoToXyz50;
40 |
--------------------------------------------------------------------------------
/src/prophoto/convertXyz50ToProphoto.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert CIE XYZ D50 values to ProPhoto RGB
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | */
8 |
9 | const gamma = v => {
10 | let abs = Math.abs(v);
11 | if (abs >= 1 / 512) {
12 | return Math.sign(v) * Math.pow(abs, 1 / 1.8);
13 | }
14 | return 16 * v;
15 | };
16 |
17 | const convertXyz50ToProphoto = ({ x, y, z, alpha }) => {
18 | if (x === undefined) x = 0;
19 | if (y === undefined) y = 0;
20 | if (z === undefined) z = 0;
21 | let res = {
22 | mode: 'prophoto',
23 | r: gamma(
24 | x * 1.3457868816471585 -
25 | y * 0.2555720873797946 -
26 | 0.0511018649755453 * z
27 | ),
28 | g: gamma(
29 | x * -0.5446307051249019 +
30 | y * 1.5082477428451466 +
31 | 0.0205274474364214 * z
32 | ),
33 | b: gamma(x * 0.0 + y * 0.0 + 1.2119675456389452 * z)
34 | };
35 | if (alpha !== undefined) {
36 | res.alpha = alpha;
37 | }
38 | return res;
39 | };
40 |
41 | export default convertXyz50ToProphoto;
42 |
--------------------------------------------------------------------------------
/src/prophoto/definition.js:
--------------------------------------------------------------------------------
1 | import rgb from '../rgb/definition.js';
2 |
3 | import convertXyz50ToProphoto from './convertXyz50ToProphoto.js';
4 | import convertProphotoToXyz50 from './convertProphotoToXyz50.js';
5 |
6 | import convertXyz50ToRgb from '../xyz50/convertXyz50ToRgb.js';
7 | import convertRgbToXyz50 from '../xyz50/convertRgbToXyz50.js';
8 |
9 | /*
10 | ProPhoto RGB Color space
11 |
12 | References:
13 | * https://en.wikipedia.org/wiki/ProPhoto_RGB_color_space
14 | */
15 |
16 | const definition = {
17 | ...rgb,
18 | mode: 'prophoto',
19 | parse: ['prophoto-rgb'],
20 | serialize: 'prophoto-rgb',
21 |
22 | fromMode: {
23 | xyz50: convertXyz50ToProphoto,
24 | rgb: color => convertXyz50ToProphoto(convertRgbToXyz50(color))
25 | },
26 |
27 | toMode: {
28 | xyz50: convertProphotoToXyz50,
29 | rgb: color => convertXyz50ToRgb(convertProphotoToXyz50(color))
30 | }
31 | };
32 |
33 | export default definition;
34 |
--------------------------------------------------------------------------------
/src/random.js:
--------------------------------------------------------------------------------
1 | import { getMode } from './modes.js';
2 |
3 | /*
4 | Generate a random number between `min` and `max`
5 | */
6 | const rand = ([min, max]) => min + Math.random() * (max - min);
7 |
8 | /*
9 | Convert a constraints object to intervals.
10 | */
11 | const to_intervals = constraints =>
12 | Object.keys(constraints).reduce((o, k) => {
13 | let v = constraints[k];
14 | o[k] = Array.isArray(v) ? v : [v, v];
15 | return o;
16 | }, {});
17 |
18 | /*
19 | Generate a random color.
20 | */
21 | const random = (mode = 'rgb', constraints = {}) => {
22 | let def = getMode(mode);
23 | let limits = to_intervals(constraints);
24 | return def.channels.reduce(
25 | (res, ch) => {
26 | // ignore alpha if not present in constraints
27 | if (limits.alpha || ch !== 'alpha') {
28 | res[ch] = rand(limits[ch] || def.ranges[ch]);
29 | }
30 | return res;
31 | },
32 | { mode }
33 | );
34 | };
35 |
36 | export default random;
37 |
--------------------------------------------------------------------------------
/src/rec2020/convertRec2020ToXyz65.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert Rec. 2020 values to CIE XYZ D65
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | * https://www.itu.int/rec/R-REC-BT.2020/en
8 | */
9 |
10 | const α = 1.09929682680944;
11 | const β = 0.018053968510807;
12 |
13 | const linearize = (v = 0) => {
14 | let abs = Math.abs(v);
15 | if (abs < β * 4.5) {
16 | return v / 4.5;
17 | }
18 | return (Math.sign(v) || 1) * Math.pow((abs + α - 1) / α, 1 / 0.45);
19 | };
20 |
21 | const convertRec2020ToXyz65 = rec2020 => {
22 | let r = linearize(rec2020.r);
23 | let g = linearize(rec2020.g);
24 | let b = linearize(rec2020.b);
25 | let res = {
26 | mode: 'xyz65',
27 | x:
28 | 0.6369580483012911 * r +
29 | 0.1446169035862083 * g +
30 | 0.1688809751641721 * b,
31 | y:
32 | 0.262700212011267 * r +
33 | 0.6779980715188708 * g +
34 | 0.059301716469862 * b,
35 | z: 0 * r + 0.0280726930490874 * g + 1.0609850577107909 * b
36 | };
37 | if (rec2020.alpha !== undefined) {
38 | res.alpha = rec2020.alpha;
39 | }
40 | return res;
41 | };
42 |
43 | export default convertRec2020ToXyz65;
44 |
--------------------------------------------------------------------------------
/src/rec2020/convertXyz65ToRec2020.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert CIE XYZ D65 values to Rec. 2020
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | * https://www.itu.int/rec/R-REC-BT.2020/en
8 | */
9 |
10 | const α = 1.09929682680944;
11 | const β = 0.018053968510807;
12 | const gamma = v => {
13 | const abs = Math.abs(v);
14 | if (abs > β) {
15 | return (Math.sign(v) || 1) * (α * Math.pow(abs, 0.45) - (α - 1));
16 | }
17 | return 4.5 * v;
18 | };
19 |
20 | const convertXyz65ToRec2020 = ({ x, y, z, alpha }) => {
21 | if (x === undefined) x = 0;
22 | if (y === undefined) y = 0;
23 | if (z === undefined) z = 0;
24 | let res = {
25 | mode: 'rec2020',
26 | r: gamma(
27 | x * 1.7166511879712683 -
28 | y * 0.3556707837763925 -
29 | 0.2533662813736599 * z
30 | ),
31 | g: gamma(
32 | x * -0.6666843518324893 +
33 | y * 1.6164812366349395 +
34 | 0.0157685458139111 * z
35 | ),
36 | b: gamma(
37 | x * 0.0176398574453108 -
38 | y * 0.0427706132578085 +
39 | 0.9421031212354739 * z
40 | )
41 | };
42 | if (alpha !== undefined) {
43 | res.alpha = alpha;
44 | }
45 | return res;
46 | };
47 |
48 | export default convertXyz65ToRec2020;
49 |
--------------------------------------------------------------------------------
/src/rec2020/definition.js:
--------------------------------------------------------------------------------
1 | import rgb from '../rgb/definition.js';
2 |
3 | import convertXyz65ToRec2020 from './convertXyz65ToRec2020.js';
4 | import convertRec2020ToXyz65 from './convertRec2020ToXyz65.js';
5 |
6 | import convertRgbToXyz65 from '../xyz65/convertRgbToXyz65.js';
7 | import convertXyz65ToRgb from '../xyz65/convertXyz65ToRgb.js';
8 |
9 | const definition = {
10 | ...rgb,
11 | mode: 'rec2020',
12 |
13 | fromMode: {
14 | xyz65: convertXyz65ToRec2020,
15 | rgb: color => convertXyz65ToRec2020(convertRgbToXyz65(color))
16 | },
17 |
18 | toMode: {
19 | xyz65: convertRec2020ToXyz65,
20 | rgb: color => convertXyz65ToRgb(convertRec2020ToXyz65(color))
21 | },
22 |
23 | parse: ['rec2020'],
24 | serialize: 'rec2020'
25 | };
26 |
27 | export default definition;
28 |
--------------------------------------------------------------------------------
/src/rgb/definition.js:
--------------------------------------------------------------------------------
1 | import parseNamed from './parseNamed.js';
2 | import parseHex from './parseHex.js';
3 | import parseRgbLegacy from './parseRgbLegacy.js';
4 | import parseRgb from './parseRgb.js';
5 | import parseTransparent from './parseTransparent.js';
6 | import { interpolatorLinear } from '../interpolate/linear.js';
7 | import { fixupAlpha } from '../fixup/alpha.js';
8 |
9 | /*
10 | sRGB color space
11 | */
12 |
13 | const definition = {
14 | mode: 'rgb',
15 | channels: ['r', 'g', 'b', 'alpha'],
16 | parse: [
17 | parseRgb,
18 | parseHex,
19 | parseRgbLegacy,
20 | parseNamed,
21 | parseTransparent,
22 | 'srgb'
23 | ],
24 | serialize: 'srgb',
25 | interpolate: {
26 | r: interpolatorLinear,
27 | g: interpolatorLinear,
28 | b: interpolatorLinear,
29 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
30 | },
31 | gamut: true,
32 | white: { r: 1, g: 1, b: 1 },
33 | black: { r: 0, g: 0, b: 0 }
34 | };
35 |
36 | export default definition;
37 |
--------------------------------------------------------------------------------
/src/rgb/parseHex.js:
--------------------------------------------------------------------------------
1 | import parseNumber from './parseNumber.js';
2 |
3 | const hex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i;
4 |
5 | const parseHex = color => {
6 | let match;
7 | // eslint-disable-next-line no-cond-assign
8 | return (match = color.match(hex))
9 | ? parseNumber(parseInt(match[1], 16), match[1].length)
10 | : undefined;
11 | };
12 |
13 | export default parseHex;
14 |
--------------------------------------------------------------------------------
/src/rgb/parseNamed.js:
--------------------------------------------------------------------------------
1 | import parseNumber from './parseNumber.js';
2 | import named from '../colors/named.js';
3 |
4 | // Also supports the `transparent` color as defined in:
5 | // https://drafts.csswg.org/css-color/#transparent-black
6 | const parseNamed = color => {
7 | return parseNumber(named[color.toLowerCase()], 6);
8 | };
9 |
10 | export default parseNamed;
11 |
--------------------------------------------------------------------------------
/src/rgb/parseNumber.js:
--------------------------------------------------------------------------------
1 | const parseNumber = (color, len) => {
2 | if (typeof color !== 'number') return;
3 |
4 | // hex3: #c93 -> #cc9933
5 | if (len === 3) {
6 | return {
7 | mode: 'rgb',
8 | r: (((color >> 8) & 0xf) | ((color >> 4) & 0xf0)) / 255,
9 | g: (((color >> 4) & 0xf) | (color & 0xf0)) / 255,
10 | b: ((color & 0xf) | ((color << 4) & 0xf0)) / 255
11 | };
12 | }
13 |
14 | // hex4: #c931 -> #cc993311
15 | if (len === 4) {
16 | return {
17 | mode: 'rgb',
18 | r: (((color >> 12) & 0xf) | ((color >> 8) & 0xf0)) / 255,
19 | g: (((color >> 8) & 0xf) | ((color >> 4) & 0xf0)) / 255,
20 | b: (((color >> 4) & 0xf) | (color & 0xf0)) / 255,
21 | alpha: ((color & 0xf) | ((color << 4) & 0xf0)) / 255
22 | };
23 | }
24 |
25 | // hex6: #f0f1f2
26 | if (len === 6) {
27 | return {
28 | mode: 'rgb',
29 | r: ((color >> 16) & 0xff) / 255,
30 | g: ((color >> 8) & 0xff) / 255,
31 | b: (color & 0xff) / 255
32 | };
33 | }
34 |
35 | // hex8: #f0f1f2ff
36 | if (len === 8) {
37 | return {
38 | mode: 'rgb',
39 | r: ((color >> 24) & 0xff) / 255,
40 | g: ((color >> 16) & 0xff) / 255,
41 | b: ((color >> 8) & 0xff) / 255,
42 | alpha: (color & 0xff) / 255
43 | };
44 | }
45 | };
46 |
47 | export default parseNumber;
48 |
--------------------------------------------------------------------------------
/src/rgb/parseRgb.js:
--------------------------------------------------------------------------------
1 | import { Tok } from '../parse.js';
2 |
3 | function parseRgb(color, parsed) {
4 | if (!parsed || (parsed[0] !== 'rgb' && parsed[0] !== 'rgba')) {
5 | return undefined;
6 | }
7 | const res = { mode: 'rgb' };
8 | const [, r, g, b, alpha] = parsed;
9 | if (r.type === Tok.Hue || g.type === Tok.Hue || b.type === Tok.Hue) {
10 | return undefined;
11 | }
12 | if (r.type !== Tok.None) {
13 | res.r = r.type === Tok.Number ? r.value / 255 : r.value / 100;
14 | }
15 | if (g.type !== Tok.None) {
16 | res.g = g.type === Tok.Number ? g.value / 255 : g.value / 100;
17 | }
18 | if (b.type !== Tok.None) {
19 | res.b = b.type === Tok.Number ? b.value / 255 : b.value / 100;
20 | }
21 | if (alpha.type !== Tok.None) {
22 | res.alpha = Math.min(
23 | 1,
24 | Math.max(
25 | 0,
26 | alpha.type === Tok.Number ? alpha.value : alpha.value / 100
27 | )
28 | );
29 | }
30 |
31 | return res;
32 | }
33 |
34 | export default parseRgb;
35 |
--------------------------------------------------------------------------------
/src/rgb/parseRgbLegacy.js:
--------------------------------------------------------------------------------
1 | import { num, per, num_per, c } from '../util/regex.js';
2 |
3 | /*
4 | rgb() regular expressions for legacy format
5 | Reference: https://drafts.csswg.org/css-color/#rgb-functions
6 | */
7 | const rgb_num_old = new RegExp(
8 | `^rgba?\\(\\s*${num}${c}${num}${c}${num}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
9 | );
10 |
11 | const rgb_per_old = new RegExp(
12 | `^rgba?\\(\\s*${per}${c}${per}${c}${per}\\s*(?:,\\s*${num_per}\\s*)?\\)$`
13 | );
14 |
15 | const parseRgbLegacy = color => {
16 | let res = { mode: 'rgb' };
17 | let match;
18 | if ((match = color.match(rgb_num_old))) {
19 | if (match[1] !== undefined) {
20 | res.r = match[1] / 255;
21 | }
22 | if (match[2] !== undefined) {
23 | res.g = match[2] / 255;
24 | }
25 | if (match[3] !== undefined) {
26 | res.b = match[3] / 255;
27 | }
28 | } else if ((match = color.match(rgb_per_old))) {
29 | if (match[1] !== undefined) {
30 | res.r = match[1] / 100;
31 | }
32 | if (match[2] !== undefined) {
33 | res.g = match[2] / 100;
34 | }
35 | if (match[3] !== undefined) {
36 | res.b = match[3] / 100;
37 | }
38 | } else {
39 | return undefined;
40 | }
41 |
42 | if (match[4] !== undefined) {
43 | res.alpha = Math.max(0, Math.min(1, match[4] / 100));
44 | } else if (match[5] !== undefined) {
45 | res.alpha = Math.max(0, Math.min(1, +match[5]));
46 | }
47 |
48 | return res;
49 | };
50 |
51 | export default parseRgbLegacy;
52 |
--------------------------------------------------------------------------------
/src/rgb/parseTransparent.js:
--------------------------------------------------------------------------------
1 | const parseTransparent = c =>
2 | c === 'transparent'
3 | ? { mode: 'rgb', r: 0, g: 0, b: 0, alpha: 0 }
4 | : undefined;
5 |
6 | export default parseTransparent;
7 |
--------------------------------------------------------------------------------
/src/round.js:
--------------------------------------------------------------------------------
1 | // From: https://github.com/d3/d3-format/issues/32
2 |
3 | const r = (value, precision) =>
4 | Math.round(value * (precision = Math.pow(10, precision))) / precision;
5 |
6 | const round =
7 | (precision = 4) =>
8 | value =>
9 | typeof value === 'number' ? r(value, precision) : value;
10 |
11 | export default round;
12 |
--------------------------------------------------------------------------------
/src/samples.js:
--------------------------------------------------------------------------------
1 | import gamma from './easing/gamma.js';
2 |
3 | const samples = (n = 2, γ = 1) => {
4 | let ease = gamma(γ);
5 | if (n < 2) {
6 | return n < 1 ? [] : [ease(0.5)];
7 | }
8 | let res = [];
9 | for (let i = 0; i < n; i++) {
10 | res.push(ease(i / (n - 1)));
11 | }
12 | return res;
13 | };
14 |
15 | export default samples;
16 |
--------------------------------------------------------------------------------
/src/util/hue.js:
--------------------------------------------------------------------------------
1 | const hueToDeg = (val, unit) => {
2 | switch (unit) {
3 | case 'deg':
4 | return +val;
5 | case 'rad':
6 | return (val / Math.PI) * 180;
7 | case 'grad':
8 | return (val / 10) * 9;
9 | case 'turn':
10 | return val * 360;
11 | }
12 | };
13 |
14 | export default hueToDeg;
15 |
--------------------------------------------------------------------------------
/src/util/normalizeHue.js:
--------------------------------------------------------------------------------
1 | const normalizeHue = hue => ((hue = hue % 360) < 0 ? hue + 360 : hue);
2 |
3 | export default normalizeHue;
4 |
--------------------------------------------------------------------------------
/src/util/normalizePositions.js:
--------------------------------------------------------------------------------
1 | /*
2 | Normalize an array of color stop positions for a gradient
3 | based on the rules defined in the CSS Images Module 4 spec:
4 |
5 | 1. make the first position 0 and the last position 1 if missing
6 | 2. sequences of unpositioned color stops should be spread out evenly
7 | 3. no position can be smaller than any of the ones preceding it
8 |
9 | Reference: https://drafts.csswg.org/css-images-4/#color-stop-fixup
10 |
11 | Note: this method does not make a defensive copy of the array
12 | it receives as argument. Instead, it adjusts the values in-place.
13 | */
14 | const normalizePositions = arr => {
15 | // 1. fix up first/last position if missing
16 | if (arr[0] === undefined) {
17 | arr[0] = 0;
18 | }
19 | if (arr[arr.length - 1] === undefined) {
20 | arr[arr.length - 1] = 1;
21 | }
22 |
23 | let i = 1;
24 | let j;
25 | let from_idx;
26 | let from_pos;
27 | let inc;
28 | while (i < arr.length) {
29 | // 2. fill up undefined positions
30 | if (arr[i] === undefined) {
31 | from_idx = i;
32 | from_pos = arr[i - 1];
33 | j = i;
34 |
35 | // find end of `undefined` sequence...
36 | while (arr[j] === undefined) j++;
37 |
38 | // ...and add evenly-spread positions
39 | inc = (arr[j] - from_pos) / (j - i + 1);
40 | while (i < j) {
41 | arr[i] = from_pos + (i + 1 - from_idx) * inc;
42 | i++;
43 | }
44 | } else if (arr[i] < arr[i - 1]) {
45 | // 3. make positions increase
46 | arr[i] = arr[i - 1];
47 | }
48 | i++;
49 | }
50 | return arr;
51 | };
52 |
53 | export default normalizePositions;
54 |
--------------------------------------------------------------------------------
/src/util/regex.js:
--------------------------------------------------------------------------------
1 | /*
2 | Basic building blocks for color regexes
3 | ---------------------------------------
4 |
5 | These regexes are expressed as strings
6 | to be interpolated in the color regexes.
7 | */
8 |
9 | //
10 | export const num = '([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)';
11 |
12 | // or 'none'
13 | export const num_none = `(?:${num}|none)`;
14 |
15 | //
16 | export const per = `${num}%`;
17 |
18 | // or 'none'
19 | export const per_none = `(?:${num}%|none)`;
20 |
21 | // ()
22 | export const num_per = `(?:${num}%|${num})`;
23 |
24 | // () or 'none'
25 | export const num_per_none = `(?:${num}%|${num}|none)`;
26 |
27 | //
28 | export const hue = `(?:${num}(deg|grad|rad|turn)|${num})`;
29 |
30 | // or 'none'
31 | export const hue_none = `(?:${num}(deg|grad|rad|turn)|${num}|none)`;
32 |
33 | export const c = `\\s*,\\s*`; // comma
34 | export const so = '\\s*'; // space, optional
35 | export const s = `\\s+`; // space
36 |
37 | export const rx_num_per_none = new RegExp('^' + num_per_none + '$');
38 |
--------------------------------------------------------------------------------
/src/wcag.js:
--------------------------------------------------------------------------------
1 | import converter from './converter.js';
2 |
3 | /*
4 | WCAG luminance
5 | References:
6 |
7 | https://en.wikipedia.org/wiki/Relative_luminance
8 | https://github.com/w3c/wcag/issues/236#issuecomment-379526596
9 | */
10 | export function luminance(color) {
11 | let c = converter('lrgb')(color);
12 | return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
13 | }
14 |
15 | /*
16 | WCAG contrast
17 | */
18 | export function contrast(a, b) {
19 | let L1 = luminance(a);
20 | let L2 = luminance(b);
21 | return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
22 | }
23 |
--------------------------------------------------------------------------------
/src/xyb/constants.js:
--------------------------------------------------------------------------------
1 | export const bias = 0.00379307325527544933;
2 | export const bias_cbrt = Math.cbrt(bias);
3 |
--------------------------------------------------------------------------------
/src/xyb/convertRgbToXyb.js:
--------------------------------------------------------------------------------
1 | import convertRgbToLrgb from '../lrgb/convertRgbToLrgb.js';
2 | import { bias, bias_cbrt } from './constants.js';
3 |
4 | const transfer = v => Math.cbrt(v) - bias_cbrt;
5 |
6 | const convertRgbToXyb = color => {
7 | const { r, g, b, alpha } = convertRgbToLrgb(color);
8 | const l = transfer(0.3 * r + 0.622 * g + 0.078 * b + bias);
9 | const m = transfer(0.23 * r + 0.692 * g + 0.078 * b + bias);
10 | const s = transfer(
11 | 0.24342268924547819 * r +
12 | 0.20476744424496821 * g +
13 | 0.5518098665095536 * b +
14 | bias
15 | );
16 | const res = {
17 | mode: 'xyb',
18 | x: (l - m) / 2,
19 | y: (l + m) / 2,
20 | /* Apply default chroma from luma (subtract Y from B) */
21 | b: s - (l + m) / 2
22 | };
23 | if (alpha !== undefined) res.alpha = alpha;
24 | return res;
25 | };
26 |
27 | export default convertRgbToXyb;
28 |
--------------------------------------------------------------------------------
/src/xyb/convertXybToRgb.js:
--------------------------------------------------------------------------------
1 | import convertLrgbToRgb from '../lrgb/convertLrgbToRgb.js';
2 | import { bias, bias_cbrt } from './constants.js';
3 |
4 | const transfer = v => Math.pow(v + bias_cbrt, 3);
5 |
6 | const convertXybToRgb = ({ x, y, b, alpha }) => {
7 | if (x === undefined) x = 0;
8 | if (y === undefined) y = 0;
9 | if (b === undefined) b = 0;
10 | const l = transfer(x + y) - bias;
11 | const m = transfer(y - x) - bias;
12 | /* Account for chroma from luma: add Y back to B */
13 | const s = transfer(b + y) - bias;
14 |
15 | const res = convertLrgbToRgb({
16 | r:
17 | 11.031566904639861 * l -
18 | 9.866943908131562 * m -
19 | 0.16462299650829934 * s,
20 | g:
21 | -3.2541473810744237 * l +
22 | 4.418770377582723 * m -
23 | 0.16462299650829934 * s,
24 | b:
25 | -3.6588512867136815 * l +
26 | 2.7129230459360922 * m +
27 | 1.9459282407775895 * s
28 | });
29 | if (alpha !== undefined) res.alpha = alpha;
30 | return res;
31 | };
32 |
33 | export default convertXybToRgb;
34 |
--------------------------------------------------------------------------------
/src/xyb/definition.js:
--------------------------------------------------------------------------------
1 | import { interpolatorLinear } from '../interpolate/linear.js';
2 | import { fixupAlpha } from '../fixup/alpha.js';
3 | import convertRgbToXyb from './convertRgbToXyb.js';
4 | import convertXybToRgb from './convertXybToRgb.js';
5 |
6 | /*
7 | The XYB color space, used in JPEG XL.
8 | Reference: https://ds.jpeg.org/whitepapers/jpeg-xl-whitepaper.pdf
9 | */
10 |
11 | const definition = {
12 | mode: 'xyb',
13 | channels: ['x', 'y', 'b', 'alpha'],
14 | parse: ['--xyb'],
15 | serialize: '--xyb',
16 |
17 | toMode: {
18 | rgb: convertXybToRgb
19 | },
20 |
21 | fromMode: {
22 | rgb: convertRgbToXyb
23 | },
24 |
25 | ranges: {
26 | x: [-0.0154, 0.0281],
27 | y: [0, 0.8453],
28 | b: [-0.2778, 0.388]
29 | },
30 |
31 | interpolate: {
32 | x: interpolatorLinear,
33 | y: interpolatorLinear,
34 | b: interpolatorLinear,
35 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
36 | }
37 | };
38 |
39 | export default definition;
40 |
--------------------------------------------------------------------------------
/src/xyz50/constants.js:
--------------------------------------------------------------------------------
1 | export const k = Math.pow(29, 3) / Math.pow(3, 3);
2 | export const e = Math.pow(6, 3) / Math.pow(29, 3);
3 |
--------------------------------------------------------------------------------
/src/xyz50/convertRgbToXyz50.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert sRGB values to CIE XYZ D50
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 |
8 | */
9 |
10 | import convertRgbToLrgb from '../lrgb/convertRgbToLrgb.js';
11 |
12 | const convertRgbToXyz50 = rgb => {
13 | let { r, g, b, alpha } = convertRgbToLrgb(rgb);
14 | let res = {
15 | mode: 'xyz50',
16 | x:
17 | 0.436065742824811 * r +
18 | 0.3851514688337912 * g +
19 | 0.14307845442264197 * b,
20 | y:
21 | 0.22249319175623702 * r +
22 | 0.7168870538238823 * g +
23 | 0.06061979053616537 * b,
24 | z:
25 | 0.013923904500943465 * r +
26 | 0.09708128566574634 * g +
27 | 0.7140993584005155 * b
28 | };
29 | if (alpha !== undefined) {
30 | res.alpha = alpha;
31 | }
32 | return res;
33 | };
34 |
35 | export default convertRgbToXyz50;
36 |
--------------------------------------------------------------------------------
/src/xyz50/convertXyz50ToRgb.js:
--------------------------------------------------------------------------------
1 | /*
2 | CIE XYZ D50 values to sRGB.
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | */
8 |
9 | import convertLrgbToRgb from '../lrgb/convertLrgbToRgb.js';
10 |
11 | const convertXyz50ToRgb = ({ x, y, z, alpha }) => {
12 | if (x === undefined) x = 0;
13 | if (y === undefined) y = 0;
14 | if (z === undefined) z = 0;
15 | let res = convertLrgbToRgb({
16 | r:
17 | x * 3.1341359569958707 -
18 | y * 1.6173863321612538 -
19 | 0.4906619460083532 * z,
20 | g:
21 | x * -0.978795502912089 +
22 | y * 1.916254567259524 +
23 | 0.03344273116131949 * z,
24 | b:
25 | x * 0.07195537988411677 -
26 | y * 0.2289768264158322 +
27 | 1.405386058324125 * z
28 | });
29 | if (alpha !== undefined) {
30 | res.alpha = alpha;
31 | }
32 | return res;
33 | };
34 |
35 | export default convertXyz50ToRgb;
36 |
--------------------------------------------------------------------------------
/src/xyz50/definition.js:
--------------------------------------------------------------------------------
1 | /*
2 | The XYZ D50 color space
3 | -----------------------
4 | */
5 |
6 | import convertXyz50ToRgb from './convertXyz50ToRgb.js';
7 | import convertXyz50ToLab from '../lab/convertXyz50ToLab.js';
8 | import convertRgbToXyz50 from './convertRgbToXyz50.js';
9 | import convertLabToXyz50 from '../lab/convertLabToXyz50.js';
10 | import { interpolatorLinear } from '../interpolate/linear.js';
11 | import { fixupAlpha } from '../fixup/alpha.js';
12 |
13 | const definition = {
14 | mode: 'xyz50',
15 | parse: ['xyz-d50'],
16 | serialize: 'xyz-d50',
17 |
18 | toMode: {
19 | rgb: convertXyz50ToRgb,
20 | lab: convertXyz50ToLab
21 | },
22 |
23 | fromMode: {
24 | rgb: convertRgbToXyz50,
25 | lab: convertLabToXyz50
26 | },
27 |
28 | channels: ['x', 'y', 'z', 'alpha'],
29 |
30 | ranges: {
31 | x: [0, 0.964],
32 | y: [0, 0.999],
33 | z: [0, 0.825]
34 | },
35 |
36 | interpolate: {
37 | x: interpolatorLinear,
38 | y: interpolatorLinear,
39 | z: interpolatorLinear,
40 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
41 | }
42 | };
43 |
44 | export default definition;
45 |
--------------------------------------------------------------------------------
/src/xyz65/constants.js:
--------------------------------------------------------------------------------
1 | export const k = Math.pow(29, 3) / Math.pow(3, 3);
2 | export const e = Math.pow(6, 3) / Math.pow(29, 3);
3 |
--------------------------------------------------------------------------------
/src/xyz65/convertRgbToXyz65.js:
--------------------------------------------------------------------------------
1 | /*
2 | Convert sRGB values to CIE XYZ D65
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | * https://observablehq.com/@danburzo/color-matrix-calculator
8 | */
9 |
10 | import convertRgbToLrgb from '../lrgb/convertRgbToLrgb.js';
11 |
12 | const convertRgbToXyz65 = rgb => {
13 | let { r, g, b, alpha } = convertRgbToLrgb(rgb);
14 | let res = {
15 | mode: 'xyz65',
16 | x:
17 | 0.4123907992659593 * r +
18 | 0.357584339383878 * g +
19 | 0.1804807884018343 * b,
20 | y:
21 | 0.2126390058715102 * r +
22 | 0.715168678767756 * g +
23 | 0.0721923153607337 * b,
24 | z:
25 | 0.0193308187155918 * r +
26 | 0.119194779794626 * g +
27 | 0.9505321522496607 * b
28 | };
29 | if (alpha !== undefined) {
30 | res.alpha = alpha;
31 | }
32 | return res;
33 | };
34 |
35 | export default convertRgbToXyz65;
36 |
--------------------------------------------------------------------------------
/src/xyz65/convertXyz50ToXyz65.js:
--------------------------------------------------------------------------------
1 | /*
2 | Chromatic adaptation of CIE XYZ from D50 to D65 white point
3 | using the Bradford method.
4 |
5 | References:
6 | * https://drafts.csswg.org/css-color/#color-conversion-code
7 | * http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
8 | */
9 |
10 | const convertXyz50ToXyz65 = xyz50 => {
11 | let { x, y, z, alpha } = xyz50;
12 | if (x === undefined) x = 0;
13 | if (y === undefined) y = 0;
14 | if (z === undefined) z = 0;
15 | let res = {
16 | mode: 'xyz65',
17 | x:
18 | 0.9554734527042182 * x -
19 | 0.0230985368742614 * y +
20 | 0.0632593086610217 * z,
21 | y:
22 | -0.0283697069632081 * x +
23 | 1.0099954580058226 * y +
24 | 0.021041398966943 * z,
25 | z:
26 | 0.0123140016883199 * x -
27 | 0.0205076964334779 * y +
28 | 1.3303659366080753 * z
29 | };
30 | if (alpha !== undefined) {
31 | res.alpha = alpha;
32 | }
33 | return res;
34 | };
35 |
36 | export default convertXyz50ToXyz65;
37 |
--------------------------------------------------------------------------------
/src/xyz65/convertXyz65ToRgb.js:
--------------------------------------------------------------------------------
1 | /*
2 | CIE XYZ D65 values to sRGB.
3 |
4 | References:
5 | * https://drafts.csswg.org/css-color/#color-conversion-code
6 | * http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
7 | * https://observablehq.com/@danburzo/color-matrix-calculator
8 | */
9 |
10 | import convertLrgbToRgb from '../lrgb/convertLrgbToRgb.js';
11 |
12 | const convertXyz65ToRgb = ({ x, y, z, alpha }) => {
13 | if (x === undefined) x = 0;
14 | if (y === undefined) y = 0;
15 | if (z === undefined) z = 0;
16 | let res = convertLrgbToRgb({
17 | r:
18 | x * 3.2409699419045226 -
19 | y * 1.5373831775700939 -
20 | 0.4986107602930034 * z,
21 | g:
22 | x * -0.9692436362808796 +
23 | y * 1.8759675015077204 +
24 | 0.0415550574071756 * z,
25 | b:
26 | x * 0.0556300796969936 -
27 | y * 0.2039769588889765 +
28 | 1.0569715142428784 * z
29 | });
30 | if (alpha !== undefined) {
31 | res.alpha = alpha;
32 | }
33 | return res;
34 | };
35 |
36 | export default convertXyz65ToRgb;
37 |
--------------------------------------------------------------------------------
/src/xyz65/convertXyz65ToXyz50.js:
--------------------------------------------------------------------------------
1 | /*
2 | Chromatic adaptation of CIE XYZ from D65 to D50 white point
3 | using the Bradford method.
4 |
5 | References:
6 | * https://drafts.csswg.org/css-color/#color-conversion-code
7 | * http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
8 | */
9 |
10 | const convertXyz65ToXyz50 = xyz65 => {
11 | let { x, y, z, alpha } = xyz65;
12 | if (x === undefined) x = 0;
13 | if (y === undefined) y = 0;
14 | if (z === undefined) z = 0;
15 | let res = {
16 | mode: 'xyz50',
17 | x:
18 | 1.0479298208405488 * x +
19 | 0.0229467933410191 * y -
20 | 0.0501922295431356 * z,
21 | y:
22 | 0.0296278156881593 * x +
23 | 0.990434484573249 * y -
24 | 0.0170738250293851 * z,
25 | z:
26 | -0.0092430581525912 * x +
27 | 0.0150551448965779 * y +
28 | 0.7518742899580008 * z
29 | };
30 | if (alpha !== undefined) {
31 | res.alpha = alpha;
32 | }
33 | return res;
34 | };
35 |
36 | export default convertXyz65ToXyz50;
37 |
--------------------------------------------------------------------------------
/src/xyz65/definition.js:
--------------------------------------------------------------------------------
1 | /*
2 | The XYZ D65 color space
3 | -----------------------
4 | */
5 |
6 | import convertXyz65ToRgb from './convertXyz65ToRgb.js';
7 | import convertRgbToXyz65 from './convertRgbToXyz65.js';
8 |
9 | import convertXyz65ToXyz50 from './convertXyz65ToXyz50.js';
10 | import convertXyz50ToXyz65 from './convertXyz50ToXyz65.js';
11 |
12 | import { interpolatorLinear } from '../interpolate/linear.js';
13 | import { fixupAlpha } from '../fixup/alpha.js';
14 |
15 | const definition = {
16 | mode: 'xyz65',
17 |
18 | toMode: {
19 | rgb: convertXyz65ToRgb,
20 | xyz50: convertXyz65ToXyz50
21 | },
22 |
23 | fromMode: {
24 | rgb: convertRgbToXyz65,
25 | xyz50: convertXyz50ToXyz65
26 | },
27 |
28 | ranges: {
29 | x: [0, 0.95],
30 | y: [0, 1],
31 | z: [0, 1.088]
32 | },
33 |
34 | channels: ['x', 'y', 'z', 'alpha'],
35 |
36 | parse: ['xyz', 'xyz-d65'],
37 | serialize: 'xyz-d65',
38 |
39 | interpolate: {
40 | x: interpolatorLinear,
41 | y: interpolatorLinear,
42 | z: interpolatorLinear,
43 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
44 | }
45 | };
46 |
47 | export default definition;
48 |
--------------------------------------------------------------------------------
/src/yiq/convertRgbToYiq.js:
--------------------------------------------------------------------------------
1 | const convertRgbToYiq = ({ r, g, b, alpha }) => {
2 | if (r === undefined) r = 0;
3 | if (g === undefined) g = 0;
4 | if (b === undefined) b = 0;
5 | const res = {
6 | mode: 'yiq',
7 | y: 0.29889531 * r + 0.58662247 * g + 0.11448223 * b,
8 | i: 0.59597799 * r - 0.2741761 * g - 0.32180189 * b,
9 | q: 0.21147017 * r - 0.52261711 * g + 0.31114694 * b
10 | };
11 | if (alpha !== undefined) res.alpha = alpha;
12 | return res;
13 | };
14 |
15 | export default convertRgbToYiq;
16 |
--------------------------------------------------------------------------------
/src/yiq/convertYiqToRgb.js:
--------------------------------------------------------------------------------
1 | const convertYiqToRgb = ({ y, i, q, alpha }) => {
2 | if (y === undefined) y = 0;
3 | if (i === undefined) i = 0;
4 | if (q === undefined) q = 0;
5 | const res = {
6 | mode: 'rgb',
7 | r: y + 0.95608445 * i + 0.6208885 * q,
8 | g: y - 0.27137664 * i - 0.6486059 * q,
9 | b: y - 1.10561724 * i + 1.70250126 * q
10 | };
11 | if (alpha !== undefined) res.alpha = alpha;
12 | return res;
13 | };
14 |
15 | export default convertYiqToRgb;
16 |
--------------------------------------------------------------------------------
/src/yiq/definition.js:
--------------------------------------------------------------------------------
1 | import convertRgbToYiq from './convertRgbToYiq.js';
2 | import convertYiqToRgb from './convertYiqToRgb.js';
3 | import { interpolatorLinear } from '../interpolate/linear.js';
4 | import { fixupAlpha } from '../fixup/alpha.js';
5 |
6 | /*
7 | YIQ Color Space
8 |
9 | References
10 | ----------
11 |
12 | Wikipedia:
13 | https://en.wikipedia.org/wiki/YIQ
14 |
15 | "Measuring perceived color difference using YIQ NTSC
16 | transmission color space in mobile applications"
17 |
18 | by Yuriy Kotsarenko, Fernando Ramos in:
19 | Programación Matemática y Software (2010)
20 |
21 | Available at:
22 |
23 | http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
24 | */
25 |
26 | const definition = {
27 | mode: 'yiq',
28 |
29 | toMode: {
30 | rgb: convertYiqToRgb
31 | },
32 |
33 | fromMode: {
34 | rgb: convertRgbToYiq
35 | },
36 |
37 | channels: ['y', 'i', 'q', 'alpha'],
38 |
39 | parse: ['--yiq'],
40 | serialize: '--yiq',
41 |
42 | ranges: {
43 | i: [-0.595, 0.595],
44 | q: [-0.522, 0.522]
45 | },
46 |
47 | interpolate: {
48 | y: interpolatorLinear,
49 | i: interpolatorLinear,
50 | q: interpolatorLinear,
51 | alpha: { use: interpolatorLinear, fixup: fixupAlpha }
52 | }
53 | };
54 |
55 | export default definition;
56 |
--------------------------------------------------------------------------------
/test/a98.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { a98, rgb, formatCss } from '../src/index.js';
4 |
5 | test('a98', t => {
6 | assert.deepEqual(
7 | a98('white'),
8 | { mode: 'a98', r: 0.9999999999999999, g: 1, b: 1.0000000000000002 },
9 | 'white'
10 | );
11 | assert.deepEqual(a98('black'), { mode: 'a98', r: 0, g: 0, b: 0 }, 'black');
12 | assert.deepEqual(
13 | a98('red'),
14 | {
15 | mode: 'a98',
16 | r: 0.8585916022954421,
17 | g: -5.533624232798453e-8,
18 | b: 2.1612063668957067e-8
19 | },
20 | 'red'
21 | );
22 | });
23 |
24 | test('color(a98-rgb)', t => {
25 | assert.deepEqual(a98('color(a98-rgb 1 0 0 / 0.25)'), {
26 | r: 1,
27 | g: 0,
28 | b: 0,
29 | alpha: 0.25,
30 | mode: 'a98'
31 | });
32 | assert.deepEqual(a98('color(a98-rgb 0% 50% 0.5 / 25%)'), {
33 | r: 0,
34 | g: 0.5,
35 | b: 0.5,
36 | alpha: 0.25,
37 | mode: 'a98'
38 | });
39 | });
40 |
41 | test('formatCss', t => {
42 | assert.equal(
43 | formatCss('color(a98-rgb 0% 50% 0.5 / 25%)'),
44 | 'color(a98-rgb 0 0.5 0.5 / 0.25)'
45 | );
46 | });
47 |
48 | test('missing components', t => {
49 | assert.ok(rgb('color(a98-rgb none 0.5 none)'), 'a98 to rgb is ok');
50 | assert.deepEqual(
51 | rgb('color(a98-rgb none 0.5 none)'),
52 | rgb('color(a98-rgb 0 0.5 0')
53 | );
54 | assert.ok(a98('rgb(none 100 20)'), 'rgb to a98 is ok');
55 | assert.deepEqual(a98('rgb(none 100 20)'), a98('rgb(0 100 20)'));
56 | });
57 |
--------------------------------------------------------------------------------
/test/average.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { average, averageAngle, formatHex } from '../src/index.js';
4 |
5 | test('average', t => {
6 | assert.equal(formatHex(average(['#ff0000', '#0000ff'])), '#800080');
7 | assert.equal(formatHex(average(['#ff0000', '#0000ff'], 'lch')), '#f50086');
8 | });
9 |
10 | test('averageAngle', t => {
11 | assert.equal(averageAngle([270, 0]), 315);
12 | });
13 |
--------------------------------------------------------------------------------
/test/blend.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { blend } from '../src/index.js';
4 |
5 | test('blendNormal', t => {
6 | assert.deepEqual(blend(['white', 'rgba(0, 0, 0, 0.5)']), {
7 | mode: 'rgb',
8 | r: 0.5,
9 | g: 0.5,
10 | b: 0.5,
11 | alpha: 1
12 | });
13 |
14 | assert.deepEqual(
15 | blend([
16 | { mode: 'rgb', r: 1, g: 0, b: 0, alpha: 0.5 },
17 | { mode: 'rgb', r: 0, g: 0, b: 1, alpha: 0.5 }
18 | ]),
19 | {
20 | mode: 'rgb',
21 | r: 0.3333333333333333,
22 | g: 0,
23 | b: 0.6666666666666666,
24 | alpha: 0.75
25 | }
26 | );
27 |
28 | // blend with transparent source
29 | assert.deepEqual(
30 | blend([{ mode: 'rgb', r: 1, g: 0, b: 0, alpha: 0.5 }, 'transparent']),
31 | { mode: 'rgb', r: 1, g: 0, b: 0, alpha: 0.5 }
32 | );
33 |
34 | // blend with transparent backdrop
35 | assert.deepEqual(
36 | blend(['transparent', { mode: 'rgb', r: 1, g: 0, b: 0, alpha: 0.5 }]),
37 | { mode: 'rgb', r: 1, g: 0, b: 0, alpha: 0.5 }
38 | );
39 | });
40 |
--------------------------------------------------------------------------------
/test/cat.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { xyz65, xyz50, rgb } from '../src/index.js';
4 |
5 | let colors = ['red', 'green', 'blue', 'white', 'black', 'magenta', 'tomato'];
6 | let e = 1e-14;
7 |
8 | let sameXYZ = (a, b) => {
9 | return (
10 | a.mode === b.mode &&
11 | Math.abs(a.x - b.x) < e &&
12 | Math.abs(a.y - b.y) < e &&
13 | Math.abs(a.z - b.z) < e
14 | );
15 | };
16 |
17 | let sameRGB = (a, b) => {
18 | return (
19 | a.mode === b.mode &&
20 | Math.abs(a.r - b.r) < e &&
21 | Math.abs(a.g - b.g) < e &&
22 | Math.abs(a.b - b.b) < e
23 | );
24 | };
25 |
26 | test('rgb -> xyz50 = rgb -> xyz65 -> xyz50', t => {
27 | colors.forEach(c => {
28 | assert.ok(sameXYZ(xyz50(rgb(c)), xyz50(xyz65(rgb(c)))), c);
29 | });
30 | });
31 |
32 | test('rgb -> xyz50 -> rgb = rgb -> xyz50 -> xyz65 -> rgb', t => {
33 | colors.forEach(c => {
34 | assert.ok(sameRGB(rgb(xyz50(rgb(c))), rgb(xyz65(xyz50(rgb(c))))), c);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/color-syntax.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { parse } from '../src/index.js';
4 |
5 | test('fewer values than channels', t => {
6 | const tests = [
7 | 'color(srgb)',
8 | 'color(srgb )',
9 | 'color(srgb/)',
10 | 'color(srgb /0.5)',
11 | 'color(srgb 0.25)',
12 | 'color(srgb 0.25 50%)',
13 | 'color( srgb 25% .5 / 0.2)'
14 | ];
15 | tests.forEach(test => {
16 | assert.equal(parse(test), undefined, test);
17 | });
18 | });
19 |
20 | test('more values than channels', t => {
21 | const tests = [
22 | 'color(srgb 25% .5 75% 0.33 0.66)',
23 | 'color(srgb 25% .5 75% 0.33 0.66 / 70% )',
24 | 'color(srgb 25% .5 75% 0.33 / 0.7)'
25 | ];
26 | tests.forEach(test => {
27 | assert.equal(parse(test), undefined, test);
28 | });
29 | });
30 |
31 | test('hue components', t => {
32 | const tests = ['color(srgb 0.5 0.5 0deg)'];
33 | tests.forEach(test => {
34 | assert.equal(parse(test), undefined, test);
35 | });
36 | });
37 |
38 | test('clamp alpha', t => {
39 | assert.deepEqual(
40 | parse('color(srgb 1.5 -0.4 0.2 / -0.5)'),
41 | { mode: 'rgb', r: 1.5, g: -0.4, b: 0.2, alpha: 0 },
42 | 'clamp alpha < 0'
43 | );
44 | assert.deepEqual(
45 | parse('color(srgb 1.5 -0.4 0.2 / 1.5)'),
46 | { mode: 'rgb', r: 1.5, g: -0.4, b: 0.2, alpha: 1 },
47 | 'clamp alpha > 1'
48 | );
49 | });
50 |
--------------------------------------------------------------------------------
/test/css.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { formatRgb, formatHex, rgb } from '../src/index.js';
4 |
5 | test('formatRgb', t => {
6 | assert.deepEqual(
7 | formatRgb(rgb('rgb(200, 300, 100)')),
8 | 'rgb(200, 255, 100)',
9 | 'rgb'
10 | );
11 |
12 | assert.deepEqual(
13 | formatRgb(rgb('rgba(200, 300, 100, 0.1)')),
14 | 'rgba(200, 255, 100, 0.1)',
15 | 'rgb'
16 | );
17 | });
18 |
19 | test('formatHex', t => {
20 | assert.deepEqual(formatHex(rgb('#c0c2ff')), '#c0c2ff', '#c0c2ff');
21 |
22 | assert.deepEqual(formatHex(rgb('#00c2ff')), '#00c2ff', '#00c2ff');
23 | });
24 |
--------------------------------------------------------------------------------
/test/cubehelix.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { cubehelix, formatCss, rgb } from '../src/index.js';
4 |
5 | test('color(--cubehelix)', t => {
6 | assert.deepEqual(cubehelix('color(--cubehelix 30 0.5 1 / 0.25)'), {
7 | h: 30,
8 | s: 0.5,
9 | l: 1,
10 | alpha: 0.25,
11 | mode: 'cubehelix'
12 | });
13 | assert.deepEqual(cubehelix('color(--cubehelix 0 50% 0.5 / 25%)'), {
14 | h: 0,
15 | s: 0.5,
16 | l: 0.5,
17 | alpha: 0.25,
18 | mode: 'cubehelix'
19 | });
20 | });
21 |
22 | test('formatCss', t => {
23 | assert.equal(
24 | formatCss('color(--cubehelix 0 50% 0.5 / 25%)'),
25 | 'color(--cubehelix 0 0.5 0.5 / 0.25)'
26 | );
27 | });
28 |
29 | test('missing components', t => {
30 | assert.ok(rgb('color(--cubehelix none 0.5 none)'));
31 | assert.deepEqual(
32 | rgb('color(--cubehelix none 0.5 none)'),
33 | rgb('color(--cubehelix 0 0.5 0)')
34 | );
35 | assert.ok(cubehelix('rgb(none 100 20)'));
36 | assert.deepEqual(cubehelix('rgb(none 100 20)'), cubehelix('rgb(0 100 20)'));
37 | });
38 |
--------------------------------------------------------------------------------
/test/deficiency.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import {
4 | filterDeficiencyProt,
5 | filterDeficiencyDeuter,
6 | filterDeficiencyTrit,
7 | formatHex
8 | } from '../src/index.js';
9 |
10 | test('0 severity', t => {
11 | assert.equal(formatHex(filterDeficiencyProt(0)('red')), '#ff0000');
12 | assert.equal(formatHex(filterDeficiencyDeuter(0)('red')), '#ff0000');
13 | assert.equal(formatHex(filterDeficiencyTrit(0)('red')), '#ff0000');
14 | });
15 |
16 | test('0.55 severity', t => {
17 | assert.equal(formatHex(filterDeficiencyProt(0.55)('blue')), '#0012ff');
18 | assert.equal(formatHex(filterDeficiencyDeuter(0.55)('blue')), '#000afa');
19 | assert.equal(formatHex(filterDeficiencyTrit(0.55)('blue')), '#000fae');
20 | });
21 |
22 | test('1 severity', t => {
23 | assert.equal(formatHex(filterDeficiencyProt(1)('green')), '#876500');
24 | assert.equal(formatHex(filterDeficiencyDeuter(1)('green')), '#6e5605');
25 | assert.equal(formatHex(filterDeficiencyTrit(1)('green')), '#007758');
26 | });
27 |
--------------------------------------------------------------------------------
/test/dlab.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { dlab, rgb, formatCss } from '../src/index.js';
4 |
5 | test('dlab', t => {
6 | assert.deepEqual(
7 | dlab('white'),
8 | { mode: 'dlab', l: 100, a: 0, b: 0 },
9 | 'white'
10 | );
11 |
12 | // Tests that achromatic RGB colors get a = b = 0
13 | assert.deepEqual(
14 | dlab('#111'),
15 | { mode: 'dlab', l: 5.938147621379976, a: 0, b: 0 },
16 | '#111'
17 | );
18 |
19 | assert.deepEqual(
20 | dlab('black'),
21 | { mode: 'dlab', l: 0, a: 0, b: 0 },
22 | 'black'
23 | );
24 | assert.deepEqual(
25 | dlab('red'),
26 | {
27 | mode: 'dlab',
28 | l: 57.28917941426675,
29 | a: 39.49797800074304,
30 | b: 30.518440059252875
31 | },
32 | 'red'
33 | );
34 | });
35 |
36 | test('color(--din99o-lab)', t => {
37 | assert.deepEqual(dlab('color(--din99o-lab 30 0.5 1 / 0.25)'), {
38 | l: 30,
39 | a: 0.5,
40 | b: 1,
41 | alpha: 0.25,
42 | mode: 'dlab'
43 | });
44 | assert.deepEqual(dlab('color(--din99o-lab 0 50% 0.5 / 25%)'), {
45 | l: 0,
46 | a: 0.5,
47 | b: 0.5,
48 | alpha: 0.25,
49 | mode: 'dlab'
50 | });
51 | });
52 |
53 | test('formatCss', t => {
54 | assert.equal(
55 | formatCss('color(--din99o-lab 0 50% 0.5 / 25%)'),
56 | 'color(--din99o-lab 0 0.5 0.5 / 0.25)'
57 | );
58 | });
59 |
60 | test('missing components', t => {
61 | assert.ok(rgb('color(--din99o-lab none 0.5 none)'), 'dlab to rgb is ok');
62 | assert.deepEqual(
63 | rgb('color(--din99o-lab none 0.5 none)'),
64 | rgb('color(--din99o-lab 0 0.5 0)')
65 | );
66 | assert.ok(dlab('rgb(none 100 20)'), 'rgb to dlab is ok');
67 | assert.deepEqual(dlab('rgb(none 100 20)'), dlab('rgb(0 100 20)'));
68 | });
69 |
--------------------------------------------------------------------------------
/test/dlch.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { dlch, rgb, formatCss } from '../src/index.js';
4 |
5 | test('dlch', t => {
6 | assert.deepEqual(dlch('white'), { mode: 'dlch', l: 100, c: 0 }, 'white');
7 | assert.deepEqual(
8 | dlch('#111'),
9 | { mode: 'dlch', l: 5.938147621379976, c: 0 },
10 | '#111'
11 | );
12 | assert.deepEqual(dlch('black'), { mode: 'dlch', l: 0, c: 0 }, 'black');
13 | assert.deepEqual(
14 | dlch('red'),
15 | {
16 | mode: 'dlch',
17 | l: 57.28917941426675,
18 | c: 49.914581534832,
19 | h: 37.691765574369924
20 | },
21 | 'red'
22 | );
23 | });
24 |
25 | test('color(--din99o-lch)', t => {
26 | assert.deepEqual(dlch('color(--din99o-lch 30 0.5 1 / 0.25)'), {
27 | l: 30,
28 | c: 0.5,
29 | h: 1,
30 | alpha: 0.25,
31 | mode: 'dlch'
32 | });
33 | assert.deepEqual(dlch('color(--din99o-lch 0 50% 0.5 / 25%)'), {
34 | l: 0,
35 | c: 0.5,
36 | h: 0.5,
37 | alpha: 0.25,
38 | mode: 'dlch'
39 | });
40 | });
41 |
42 | test('formatCss', t => {
43 | assert.equal(
44 | formatCss('color(--din99o-lch 0 50% 0.5 / 25%)'),
45 | 'color(--din99o-lch 0 0.5 0.5 / 0.25)'
46 | );
47 | });
48 |
49 | test('missing components', t => {
50 | assert.ok(rgb('color(--din99o-lch none 0.5 none)'), 'dlch to rgb is ok');
51 | assert.deepEqual(
52 | rgb('color(--din99o-lch none 0.5 none)'),
53 | rgb('color(--din99o-lch 0% 0.5 0)')
54 | );
55 | assert.ok(dlch('rgb(none 100 20)'), 'rgb to dlch is ok');
56 | assert.deepEqual(dlch('rgb(none 100 20)'), dlch('rgb(0 100 20)'));
57 | });
58 |
--------------------------------------------------------------------------------
/test/easing.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 |
4 | import midpoint from '../src/easing/midpoint.js';
5 | import {
6 | easingSmoothstep,
7 | easingSmoothstepInverse
8 | } from '../src/easing/smoothstep.js';
9 | import smootherstep from '../src/easing/smootherstep.js';
10 | import samples from '../src/samples.js';
11 |
12 | test('easingMidpoint', t => {
13 | let noop = midpoint(0.5);
14 | let curve = midpoint(0.2);
15 |
16 | assert.equal(noop(0.1), 0.1);
17 | assert.equal(noop(0.4), 0.4);
18 | assert.equal(noop(0.5), 0.5);
19 | assert.equal(noop(0.7), 0.7);
20 | assert.equal(noop(1), 1);
21 |
22 | assert.equal(curve(0), 0);
23 | assert.equal(curve(1), 1);
24 | });
25 |
26 | test('easingSmoothstep', t => {
27 | assert.equal(easingSmoothstep(0), 0);
28 | assert.equal(easingSmoothstep(1), 1);
29 | assert.equal(easingSmoothstep(0.5), 0.5);
30 | });
31 |
32 | test('easingSmoothstepInverse', t => {
33 | samples(5).forEach(x => {
34 | assert.ok(
35 | Math.abs(easingSmoothstepInverse(easingSmoothstep(x)) - x) < 1e-15,
36 | `roundtrip: ${x}`
37 | );
38 | });
39 | });
40 |
41 | test('easingSmootherstep', t => {
42 | assert.equal(smootherstep(0), 0);
43 | assert.equal(smootherstep(1), 1);
44 | assert.equal(smootherstep(0.5), 0.5);
45 | });
46 |
--------------------------------------------------------------------------------
/test/filter.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import {
4 | filterBrightness,
5 | filterContrast,
6 | filterSepia,
7 | formatHex,
8 | filterSaturate,
9 | filterGrayscale,
10 | filterInvert,
11 | filterHueRotate
12 | } from '../src/index.js';
13 |
14 | test('filterBrightness', t => {
15 | assert.deepEqual(
16 | filterBrightness(1)('hsl(60 100% 50% / 25%)'),
17 | { mode: 'hsl', h: 60, s: 1, l: 0.5, alpha: 0.25 },
18 | 'unchanged in the original color space'
19 | );
20 | assert.equal(
21 | formatHex(filterBrightness(2)('#003366')),
22 | '#0066cc',
23 | 'brightens the color'
24 | );
25 | });
26 |
27 | test('filterContrast', t => {
28 | assert.equal(
29 | formatHex(filterContrast(2)('#003366')),
30 | '#00004d',
31 | 'Increases the contrast'
32 | );
33 | });
34 |
35 | test('filterSepia', t => {
36 | assert.equal(formatHex(filterSepia(0)('red')), '#ff0000', 'unchanged');
37 | assert.equal(formatHex(filterSepia(1)('red')), '#645945', 'fully sepia');
38 | });
39 |
40 | test('filterSaturate', t => {
41 | assert.equal(
42 | formatHex(filterSaturate(0)('#cc0033')),
43 | '#2f2f2f',
44 | 'fully desaturated'
45 | );
46 | assert.equal(
47 | formatHex(filterSaturate(1)('#cc0033')),
48 | '#cc0033',
49 | 'unchanged'
50 | );
51 | assert.equal(
52 | formatHex(filterSaturate(2)('#cc0033')),
53 | '#ff0037',
54 | 'oversaturated'
55 | );
56 | });
57 |
58 | test('filterGrayscale', t => {
59 | assert.equal(formatHex(filterGrayscale(0)('red')), '#ff0000', 'unchanged');
60 | assert.equal(
61 | formatHex(filterGrayscale(1)('red')),
62 | '#363636',
63 | 'fully grayscale'
64 | );
65 | });
66 |
67 | test('filterInvert', t => {
68 | assert.equal(formatHex(filterInvert(0)('red')), '#ff0000', 'unchanged');
69 | assert.equal(formatHex(filterInvert(0.5)('red')), '#808080', 'gray');
70 | assert.equal(
71 | formatHex(filterInvert(1)('red')),
72 | '#00ffff',
73 | 'fully inverted'
74 | );
75 | });
76 |
77 | test('filterHueRotate', t => {
78 | assert.equal(formatHex(filterHueRotate(60)('red')), '#6c3b00');
79 | });
80 |
--------------------------------------------------------------------------------
/test/fixupAlpha.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { fixupAlpha } from '../src/index.js';
4 |
5 | test('fixupAlpha: some defined', t => {
6 | assert.deepEqual(fixupAlpha([undefined, 0, undefined]), [1, 0, 1]);
7 | });
8 |
9 | test('fixupAlpha: all undefined', t => {
10 | assert.deepEqual(fixupAlpha([undefined, undefined, undefined]), [
11 | undefined,
12 | undefined,
13 | undefined
14 | ]);
15 | });
16 |
--------------------------------------------------------------------------------
/test/fixupHue.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import {
4 | fixupHueShorter,
5 | fixupHueLonger,
6 | fixupHueIncreasing,
7 | fixupHueDecreasing
8 | } from '../src/index.js';
9 |
10 | test('fixupHueShorter', t => {
11 | assert.deepEqual(
12 | fixupHueShorter([0, 340, 30, 0, 170]),
13 | [0, -20, 30, 0, 170]
14 | );
15 | assert.deepEqual(fixupHueShorter([-250, -8]), [110, -8]);
16 | });
17 |
18 | test('fixupHueLonger', t => {
19 | assert.deepEqual(
20 | fixupHueLonger([0, 340, 30, 0, 170]),
21 | [0, 340, 30, 360, 170]
22 | );
23 |
24 | assert.deepEqual(
25 | fixupHueLonger([0, 179, 179, 360]),
26 | [0, -181, -181, 0],
27 | 'equal consecutive values'
28 | );
29 | });
30 |
31 | test('fixupHueIncreasing', t => {
32 | let hues = [0, 340, 30, 0, 170];
33 | assert.deepEqual(fixupHueIncreasing(hues), [0, 340, 390, 720, 890]);
34 | });
35 |
36 | test('fixupHueDecreasing', t => {
37 | let hues = [0, 340, 30, 0, 170];
38 | assert.deepEqual(fixupHueDecreasing(hues), [0, -20, -330, -360, -550]);
39 | });
40 |
--------------------------------------------------------------------------------
/test/formatter.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import {
4 | formatHex,
5 | formatHex8,
6 | formatRgb,
7 | formatHsl,
8 | rgb
9 | } from '../src/index.js';
10 |
11 | test('formatHex', t => {
12 | assert.equal(formatHex('tomato'), '#ff6347');
13 | });
14 |
15 | test('formatHex8', t => {
16 | assert.equal(
17 | formatHex8({ mode: 'rgb', r: 1, g: 1, b: 1, alpha: 0 }),
18 | '#ffffff00'
19 | );
20 | });
21 |
22 | test('formatRgb', t => {
23 | assert.equal(formatRgb(rgb('#f0f0f0f0')), 'rgba(240, 240, 240, 0.94)');
24 | assert.equal(formatRgb('#f0f0f0f0'), 'rgba(240, 240, 240, 0.94)');
25 | });
26 |
27 | test('formatHsl', t => {
28 | assert.equal(formatHsl('red'), 'hsl(0, 100%, 50%)');
29 | assert.equal(
30 | formatHsl({
31 | mode: 'hsl',
32 | h: 30.21,
33 | s: 0.2361,
34 | l: 0.48321,
35 | alpha: -0.2
36 | }),
37 | 'hsla(30.21, 23.61%, 48.32%, 0)'
38 | );
39 | assert.equal(
40 | formatHsl({ mode: 'hsl', h: 405, s: 1.2, l: -1 }),
41 | 'hsl(405, 100%, 0%)'
42 | );
43 | });
44 |
--------------------------------------------------------------------------------
/test/hwb.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { hwb, rgb, formatCss } from '../src/index.js';
4 |
5 | test('hwb() parses hwb CSS strings', t => {
6 | assert.deepEqual(
7 | hwb('hwb(100 0% 0%)'),
8 | { h: 100, w: 0, b: 0, mode: 'hwb' },
9 | 'black'
10 | );
11 |
12 | assert.deepEqual(
13 | hwb('hwb(200 150% 150%)'),
14 | { h: 200, w: 1.5, b: 1.5, mode: 'hwb' },
15 | 'grey'
16 | );
17 |
18 | assert.deepEqual(
19 | hwb('hwb(200 150% 50% / 50%)'),
20 | { h: 200, w: 1.5, b: 0.5, mode: 'hwb', alpha: 0.5 },
21 | 'grey (alpha [0-1])'
22 | );
23 |
24 | assert.deepEqual(
25 | hwb('hwb(200 150% 50% / .5)'),
26 | { h: 200, w: 1.5, b: 0.5, mode: 'hwb', alpha: 0.5 },
27 | 'grey (alpha percentage)'
28 | );
29 | });
30 |
31 | test('formatCss', t => {
32 | assert.equal(
33 | formatCss('hwb(200 150% 50% / .5)'),
34 | 'hwb(200 150% 50% / 0.5)'
35 | );
36 | assert.equal(formatCss('hwb(200 150% 50% / 100%)'), 'hwb(200 150% 50%)');
37 | assert.equal(formatCss('hwb(200 150% 50%)'), 'hwb(200 150% 50%)');
38 | assert.equal(formatCss(hwb('#ffffff00')), 'hwb(none 100% 0% / 0)');
39 | });
40 |
41 | test('missing components', t => {
42 | assert.ok(rgb('hwb(none 50% none)'));
43 | assert.deepEqual(rgb('hwb(none 50% none)'), rgb('hwb(0deg 50% 0%'));
44 | assert.ok(hwb('rgb(none 100 20)'));
45 | assert.deepEqual(hwb('rgb(none 100 20)'), hwb('rgb(0 100 20)'));
46 | });
47 |
48 | test('powerless components', t => {
49 | assert.deepEqual(hwb(rgb('hwb(60deg 75% 50%)')), {
50 | mode: 'hwb',
51 | w: 0.6,
52 | b: 0.4
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/interpolatorLinear.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { samples, interpolatorLinear } from '../src/index.js';
4 |
5 | test('interpolatorLinear', t => {
6 | let data = [3, 2.8, 2.5, 1, 0.95, 0.8, 0.5, 0.1, 0.05];
7 | assert.deepEqual(
8 | samples(10).map(interpolatorLinear(data)),
9 | [
10 | 3, 2.822222222222222, 2.5666666666666664, 1.5000000000000002,
11 | 0.9722222222222222, 0.8833333333333333, 0.7000000000000002,
12 | 0.4111111111111111, 0.09444444444444447, 0.05
13 | ]
14 | );
15 | });
16 |
17 | test('outside [0, 1] range', t => {
18 | let it = interpolatorLinear([3, 10, 1]);
19 | assert.equal(it(-0.5), -4);
20 | assert.equal(it(-1), -11);
21 | assert.equal(it(1.5), -8);
22 | assert.equal(it(2), -17);
23 | });
24 |
--------------------------------------------------------------------------------
/test/interpolatorSplineBasis.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import {
4 | samples,
5 | interpolatorSplineBasis,
6 | interpolatorSplineBasisClosed
7 | } from '../src/index.js';
8 |
9 | test('interpolatorSplineBasis', t => {
10 | let data = [3, 2.8, 2.5, 1, 0.95, 0.8, 0.5, 0.1, 0.05];
11 | assert.deepEqual(
12 | samples(10).map(interpolatorSplineBasis(data)),
13 | [
14 | 3, 2.810516689529035, 2.472382258802012, 1.5641975308641978,
15 | 0.9905807041609512, 0.8782807498856883, 0.6919753086419754,
16 | 0.4039094650205761, 0.13541380887059906, 0.05000000000000001
17 | ]
18 | );
19 | });
20 |
21 | test('interpolatorSplineBasisClosed', t => {
22 | let data = [3, 2.8, 2.5, 1, 0.95, 0.8, 0.5, 0.1, 0.05];
23 | assert.deepEqual(
24 | samples(10).map(interpolatorSplineBasisClosed(data)),
25 | [
26 | 2.475, 2.8097965249199817, 2.472382258802012, 1.5641975308641978,
27 | 0.9905807041609512, 0.8782807498856883, 0.6919753086419754,
28 | 0.4039094650205761, 0.1360996799268405, 0.5499999999999999
29 | ]
30 | );
31 | });
32 |
33 | test('interpolatorSplineBasis: outside [0, 1] range', t => {
34 | let it = interpolatorSplineBasis([3, 10, 1]);
35 | assert.deepEqual(
36 | [-0.5, 1, 1.5, 2].map(it),
37 | [-1.3333333333333333, 1, -5.333333333333333, 4.333333333333333]
38 | );
39 | });
40 |
41 | test('interpolatorSplineBasisClosed: outside [0, 1] range', t => {
42 | let it = interpolatorSplineBasisClosed([3, 10, 1]);
43 | assert.deepEqual(
44 | [-0.5, 1, 1.5, 2].map(it),
45 | [
46 | 2.8333333333333335, 2.8333333333333335, 3.8333333333333335,
47 | 7.333333333333333
48 | ]
49 | );
50 | });
51 |
--------------------------------------------------------------------------------
/test/interpolatorSplineNatural.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import {
4 | samples,
5 | interpolatorSplineNatural,
6 | interpolatorSplineNaturalClosed
7 | } from '../src/index.js';
8 |
9 | test('interpolatorSplineNatural', t => {
10 | let arr = [10, 20, 0, 40, 70];
11 | let it = interpolatorSplineNatural(arr);
12 | assert.deepEqual(samples(5).map(it), arr);
13 | });
14 |
15 | test('interpolatorSplineNaturalClosed', t => {
16 | let arr = [10, 20, 0, 40, 70];
17 | let it = interpolatorSplineNaturalClosed(arr);
18 | assert.deepEqual(samples(5).map(it), [23.75, 20, 0, 40, 56.25]);
19 | });
20 |
21 | test('interpolatorSplineNatural: outside [0, 1] range', t => {
22 | let it = interpolatorSplineNatural([3, 10, 1]);
23 | assert.deepEqual(
24 | [-0.5, 1, 1.5, 2].map(it),
25 | [-4.166666666666667, 1, -8.166666666666666, 7.166666666666667]
26 | );
27 | });
28 |
29 | test('interpolatorSplineNaturalClosed: outside [0, 1] range', t => {
30 | let it = interpolatorSplineNaturalClosed([3, 10, 1]);
31 | assert.deepEqual(
32 | [-0.5, 1, 1.5, 2].map(it),
33 | [
34 | 3.5416666666666665, 3.5416666666666665, 4.541666666666667,
35 | 10.166666666666666
36 | ]
37 | );
38 | });
39 |
--------------------------------------------------------------------------------
/test/itp.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { formatCss, formatHex, itp } from '../src/index.js';
4 |
5 | test('itp', t => {
6 | assert.deepEqual(
7 | itp('white'),
8 | {
9 | mode: 'itp',
10 | i: 0.5806888810416109,
11 | t: 1.1102230246251565e-16,
12 | p: 2.914335439641036e-16
13 | },
14 | 'white'
15 | );
16 | assert.deepEqual(
17 | itp('black'),
18 | {
19 | mode: 'itp',
20 | i: 7.309559025783966e-7,
21 | t: -2.117582368135751e-22,
22 | p: 1.3234889800848443e-23
23 | },
24 | 'black'
25 | );
26 | assert.deepEqual(
27 | itp('red'),
28 | {
29 | mode: 'itp',
30 | i: 0.4278802843622844,
31 | t: -0.11570435976969046,
32 | p: 0.27872894737532694
33 | },
34 | 'red'
35 | );
36 | });
37 |
38 | test('color(--ictcp)', t => {
39 | assert.deepEqual(itp('color(--ictcp 1 0 0 / 0.25)'), {
40 | mode: 'itp',
41 | i: 1,
42 | t: 0,
43 | p: 0,
44 | alpha: 0.25
45 | });
46 | assert.deepEqual(itp('color(--ictcp 0% 50% 0.5 / 25%)'), {
47 | mode: 'itp',
48 | i: 0,
49 | t: 0.5,
50 | p: 0.5,
51 | alpha: 0.25
52 | });
53 | });
54 |
55 | test('formatCss', t => {
56 | assert.equal(
57 | formatCss('color(--ictcp 0% 50% 0.5 / 25%)'),
58 | 'color(--ictcp 0 0.5 0.5 / 0.25)'
59 | );
60 | });
61 |
62 | test('formatCss', t => {
63 | assert.equal(
64 | formatHex({
65 | mode: 'itp',
66 | i: 0.4278802843622844,
67 | t: -0.11570435976969046,
68 | p: 0.27872894737532694
69 | }),
70 | '#ff0000',
71 | 'red'
72 | );
73 | });
74 |
--------------------------------------------------------------------------------
/test/jab.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { jab, rgb, formatHex, formatCss } from '../src/index.js';
4 |
5 | test('jab', t => {
6 | assert.deepEqual(
7 | jab('white'),
8 | { mode: 'jab', j: 0.222065249535743, a: 0, b: 0 },
9 | 'white'
10 | );
11 |
12 | assert.deepEqual(
13 | jab('black'),
14 | { mode: 'jab', j: 3.2311742677852644e-27, a: 0, b: 0 },
15 | 'black'
16 | );
17 |
18 | assert.deepEqual(
19 | jab('red'),
20 | {
21 | mode: 'jab',
22 | j: 0.13438473104350068,
23 | a: 0.11788526260797229,
24 | b: 0.11187810901317238
25 | },
26 | 'red'
27 | );
28 | });
29 |
30 | test('rgb -> jab -> rgb', t => {
31 | assert.equal(formatHex(jab('#cc3302')), '#cc3302', '#cc3302');
32 | });
33 |
34 | test('color(--jzazbz)', t => {
35 | assert.deepEqual(jab('color(--jzazbz 30 -10 +15 / 0.25)'), {
36 | j: 30,
37 | a: -10,
38 | b: +15,
39 | alpha: 0.25,
40 | mode: 'jab'
41 | });
42 | });
43 |
44 | test('formatCss', t => {
45 | assert.equal(
46 | formatCss('color(--jzazbz 30 -1e1 +15 / 0.25)'),
47 | 'color(--jzazbz 30 -10 15 / 0.25)'
48 | );
49 | });
50 |
51 | test('missing components', t => {
52 | assert.ok(rgb('color(--jzazbz none 0.5 none)'), 'jab to rgb is ok');
53 | assert.deepEqual(
54 | rgb('color(--jzazbz none 0.5 none)'),
55 | rgb('color(--jzazbz 0 0.5 0)')
56 | );
57 | assert.ok(jab('rgb(none 100 20)'), 'rgb to jab is ok');
58 | assert.deepEqual(jab('rgb(none 100 20)'), jab('rgb(0 100 20)'));
59 | });
60 |
--------------------------------------------------------------------------------
/test/jch.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { formatRgb, formatCss, jch, rgb } from '../src/index.js';
4 |
5 | test('PQ_inv negative value', t => {
6 | assert.equal(
7 | formatRgb({ mode: 'jch', j: 0.01768, c: 0.095, h: 77 }),
8 | 'rgb(42, 0, 0)'
9 | );
10 | });
11 |
12 | test('color(--jzczhz)', t => {
13 | assert.deepEqual(jch('color(--jzczhz 30 10e1 -15 / 0.25)'), {
14 | j: 30,
15 | c: 100,
16 | h: -15,
17 | alpha: 0.25,
18 | mode: 'jch'
19 | });
20 | });
21 |
22 | test('formatCss', t => {
23 | assert.equal(
24 | formatCss('color(--jzczhz 30 1e1 +15 / 0.25)'),
25 | 'color(--jzczhz 30 10 15 / 0.25)'
26 | );
27 | });
28 |
29 | test('missing components', t => {
30 | assert.ok(rgb('color(--jzczhz none 0.5 none)'), 'jch to rgb is ok');
31 | assert.deepEqual(
32 | rgb('color(--jzczhz none 0.5 none)'),
33 | rgb('color(--jzczhz 0% 0.5 0)')
34 | );
35 | assert.ok(jch('rgb(none 100 20)'), 'rgb to jch is ok');
36 | assert.deepEqual(jch('rgb(none 100 20)'), jch('rgb(0 100 20)'));
37 | });
38 |
--------------------------------------------------------------------------------
/test/lab.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { lab, rgb, formatCss } from '../src/index.js';
4 |
5 | test('lab', t => {
6 | assert.deepEqual(
7 | lab('white'),
8 | { mode: 'lab', l: 100.00000139649632, a: 0, b: 0 },
9 | 'white'
10 | );
11 |
12 | // Tests that achromatic RGB colors get a = b = 0 in CIELab
13 | assert.deepEqual(
14 | lab('#111'),
15 | { mode: 'lab', l: 5.063329676301251, a: 0, b: 0 },
16 | '#111'
17 | );
18 |
19 | assert.deepEqual(lab('black'), { mode: 'lab', l: 0, a: 0, b: 0 }, 'black');
20 | assert.deepEqual(
21 | lab('red'),
22 | {
23 | mode: 'lab',
24 | l: 54.29054294696968,
25 | a: 80.80492033462417,
26 | b: 69.89098825896278
27 | },
28 | 'red'
29 | );
30 |
31 | assert.deepEqual(lab('lab(50% -10% 200% / 10%)'), {
32 | mode: 'lab',
33 | l: 50,
34 | a: -12.5,
35 | b: 250,
36 | alpha: 0.1
37 | });
38 | });
39 |
40 | test('formatCss', t => {
41 | assert.equal(formatCss('lab(40% 10 30 / 50%)'), 'lab(40 10 30 / 0.5)');
42 | assert.equal(formatCss('lab(40% 10 30 / 100%)'), 'lab(40 10 30)');
43 | assert.equal(formatCss('lab(40% 10 30)'), 'lab(40 10 30)');
44 | });
45 |
46 | test('missing components', t => {
47 | assert.ok(rgb('lab(none 0.5 none)'), 'lab to rgb is ok');
48 | assert.deepEqual(rgb('lab(none 0.5 none)'), rgb('lab(0% 0.5 0%)'));
49 | assert.ok(lab('rgb(none 100 20)'), 'rgb to lab is ok');
50 | assert.deepEqual(lab('rgb(none 100 20)'), lab('rgb(0 100 20)'));
51 | });
52 |
--------------------------------------------------------------------------------
/test/lab65.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { lab65, rgb, formatCss } from '../src/index.js';
4 |
5 | test('lab65', t => {
6 | assert.deepEqual(
7 | lab65('white'),
8 | { mode: 'lab65', l: 100, a: 0, b: 0 },
9 | 'white'
10 | );
11 |
12 | // Tests that achromatic RGB colors get a = b = 0 in CIELab D65
13 | assert.deepEqual(
14 | lab65('#111'),
15 | { mode: 'lab65', l: 5.063329493432597, a: 0, b: 0 },
16 | '#111'
17 | );
18 |
19 | assert.deepEqual(
20 | lab65('black'),
21 | { mode: 'lab65', l: 0, a: 0, b: 0 },
22 | 'black'
23 | );
24 | assert.deepEqual(
25 | lab65('red'),
26 | {
27 | mode: 'lab65',
28 | l: 53.237115595429344,
29 | a: 80.09011352310385,
30 | b: 67.20326351172214
31 | },
32 | 'red'
33 | );
34 | });
35 |
36 | test('color(--lab-d65)', t => {
37 | assert.deepEqual(lab65('color(--lab-d65 30 0.5 1 / 0.25)'), {
38 | l: 30,
39 | a: 0.5,
40 | b: 1,
41 | alpha: 0.25,
42 | mode: 'lab65'
43 | });
44 | });
45 |
46 | test('formatCss', t => {
47 | assert.equal(
48 | formatCss('color(--lab-d65 30 0.5 1 / 0.25)'),
49 | 'color(--lab-d65 30 0.5 1 / 0.25)'
50 | );
51 | });
52 |
53 | test('missing components', t => {
54 | assert.ok(rgb('color(--lab-d65 none 0.5 none)'), 'lab65 to rgb is ok');
55 | assert.deepEqual(
56 | rgb('color(--lab-d65 none 0.5 none)'),
57 | rgb('color(--lab-d65 0 0.5 0)')
58 | );
59 | assert.ok(lab65('rgb(none 100 20)'), 'rgb to lab65 is ok');
60 | assert.deepEqual(lab65('rgb(none 100 20)'), lab65('rgb(0 100 20)'));
61 | });
62 |
--------------------------------------------------------------------------------
/test/lch.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { lch, rgb, formatCss } from '../src/index.js';
4 |
5 | test('lch', t => {
6 | assert.deepEqual(
7 | lch('white'),
8 | { mode: 'lch', l: 100.00000139649632, c: 0 },
9 | 'white'
10 | );
11 | assert.deepEqual(
12 | lch('#111'),
13 | { mode: 'lch', l: 5.063329676301251, c: 0 },
14 | '#111'
15 | );
16 | assert.deepEqual(lch('black'), { mode: 'lch', l: 0, c: 0 }, 'black');
17 | assert.deepEqual(
18 | lch('red'),
19 | {
20 | mode: 'lch',
21 | l: 54.29054294696968,
22 | c: 106.83719104365966,
23 | h: 40.85766878213079
24 | },
25 | 'red'
26 | );
27 | assert.deepEqual(lch('lch(20% 30% .5turn / 10%)'), {
28 | mode: 'lch',
29 | l: 20,
30 | c: 45,
31 | h: 180,
32 | alpha: 0.1
33 | });
34 | assert.deepEqual(lch('lch(20% -30% .5turn / 10%)'), {
35 | mode: 'lch',
36 | l: 20,
37 | c: 0,
38 | h: 180,
39 | alpha: 0.1
40 | });
41 | });
42 |
43 | test('formatCss', t => {
44 | assert.equal(formatCss('lch(40% 10 30 / 50%)'), 'lch(40 10 30 / 0.5)');
45 | assert.equal(formatCss('lch(40% 10 30 / 100%)'), 'lch(40 10 30)');
46 | assert.equal(formatCss('lch(40% 10 30)'), 'lch(40 10 30)');
47 | assert.equal(
48 | formatCss(lch('#ffffff00')),
49 | 'lch(100.00000139649632 0 none / 0)'
50 | );
51 | });
52 |
53 | test('missing components', t => {
54 | assert.ok(rgb('lch(none 0.5 none)'), 'lch to rgb is ok');
55 | assert.deepEqual(rgb('lch(none 0.5 none)'), rgb('lch(0% 0.5 0)'));
56 | assert.ok(lch('rgb(none 100 20)'), 'rgb to lch is ok');
57 | assert.deepEqual(lch('rgb(none 100 20)'), lch('rgb(0 100 20)'));
58 | });
59 |
--------------------------------------------------------------------------------
/test/lch65.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { lch65, lab65, formatCss } from '../src/index.js';
4 |
5 | test('lch65', t => {
6 | assert.deepEqual(lch65('white'), { mode: 'lch65', l: 100, c: 0 }, 'white');
7 | assert.deepEqual(
8 | lch65('#111'),
9 | { mode: 'lch65', l: 5.063329493432597, c: 0 },
10 | '#111'
11 | );
12 | assert.deepEqual(lch65('black'), { mode: 'lch65', l: 0, c: 0 }, 'black');
13 | assert.deepEqual(
14 | lch65('red'),
15 | {
16 | mode: 'lch65',
17 | l: 53.237115595429344,
18 | c: 104.55001152926587,
19 | h: 39.99986515439813
20 | },
21 | 'red'
22 | );
23 | });
24 |
25 | test('lab65 <-> lch65', t => {
26 | assert.deepEqual(lch65(lab65({ l: 100, a: 0.2, b: 0.2 })), {
27 | mode: 'lch65',
28 | l: 100,
29 | c: 0.28284271247461906,
30 | h: 45
31 | });
32 |
33 | assert.deepEqual(
34 | lab65(
35 | lch65({
36 | mode: 'lch65',
37 | l: 100,
38 | c: 0.28284271247461906,
39 | h: 45
40 | })
41 | ),
42 | { mode: 'lab65', l: 100, a: 0.20000000000000004, b: 0.2 }
43 | );
44 | });
45 |
46 | test('color(--lch-d65)', t => {
47 | assert.deepEqual(lch65('color(--lch-d65 30 0.5 1 / 0.25)'), {
48 | l: 30,
49 | c: 0.5,
50 | h: 1,
51 | alpha: 0.25,
52 | mode: 'lch65'
53 | });
54 | });
55 |
56 | test('formatCss', t => {
57 | assert.equal(
58 | formatCss('color(--lch-d65 30 0.5 1 / 0.25)'),
59 | 'color(--lch-d65 30 0.5 1 / 0.25)'
60 | );
61 | });
62 |
--------------------------------------------------------------------------------
/test/lchuv.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { lchuv, rgb, formatCss } from '../src/index.js';
4 |
5 | test('lchuv', t => {
6 | assert.deepEqual(
7 | lchuv('white'),
8 | {
9 | mode: 'lchuv',
10 | l: 100.00000139649632,
11 | c: 0.000013192899605235416,
12 | h: 129.3684297210339
13 | },
14 | 'white'
15 | );
16 | assert.deepEqual(lchuv('black'), { mode: 'lchuv', l: 0, c: 0 }, 'black');
17 | assert.deepEqual(
18 | lchuv('red'),
19 | {
20 | mode: 'lchuv',
21 | l: 54.29054294696968,
22 | c: 176.94953872495253,
23 | h: 8.434231142939021
24 | },
25 | 'red'
26 | );
27 | assert.deepEqual(
28 | lchuv('#00cc0080'),
29 | {
30 | mode: 'lchuv',
31 | l: 71.74973747305378,
32 | c: 99.4709666171262,
33 | h: 134.23124010020916,
34 | alpha: 0.5019607843137255
35 | },
36 | '#00cc0080'
37 | );
38 | });
39 |
40 | test('color(--lchuv)', t => {
41 | assert.deepEqual(lchuv('color(--lchuv 30 0.5 1 / 0.25)'), {
42 | l: 30,
43 | c: 0.5,
44 | h: 1,
45 | alpha: 0.25,
46 | mode: 'lchuv'
47 | });
48 | });
49 |
50 | test('formatCss', t => {
51 | assert.equal(
52 | formatCss('color(--lchuv 30 0.5 1 / 0.25)'),
53 | 'color(--lchuv 30 0.5 1 / 0.25)'
54 | );
55 | });
56 |
57 | test('missing components', t => {
58 | assert.ok(rgb('color(--lchuv none 0.5 none)'), 'lchuv to rgb is ok');
59 | assert.deepEqual(
60 | rgb('color(--lchuv none 0.5 none)'),
61 | rgb('color(--lchuv 0 0.5 0)')
62 | );
63 | assert.ok(lchuv('rgb(none 100 20)'), 'rgb to lchuv is ok');
64 | assert.deepEqual(lchuv('rgb(none 100 20)'), lchuv('rgb(0 100 20)'));
65 | });
66 |
--------------------------------------------------------------------------------
/test/lerp.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { lerp, unlerp, trilerp } from '../src/index.js';
4 |
5 | test('lerp()', t => {
6 | assert.equal(lerp(10, 2, 0.5), 6);
7 | });
8 |
9 | test('unlerp()', t => {
10 | assert.equal(unlerp(5, 10, 2.5), -0.5);
11 | });
12 |
13 | const RYB_CUBE = [
14 | { mode: 'rgb', r: 1, g: 1, b: 1 }, // white
15 | { mode: 'rgb', r: 1, g: 0, b: 0 }, // red
16 | { mode: 'rgb', r: 1, g: 1, b: 0 }, // yellow
17 | { mode: 'rgb', r: 1, g: 0.5, b: 0 }, // orange
18 | { mode: 'rgb', r: 0.163, g: 0.373, b: 0.6 }, // blue
19 | { mode: 'rgb', r: 0.5, g: 0, b: 0.5 }, // violet
20 | { mode: 'rgb', r: 0, g: 0.66, b: 0.2 }, // green
21 | { mode: 'rgb', r: 0.2, g: 0.094, b: 0 } // black
22 | ];
23 |
24 | test('trilerp()', t => {
25 | const RYB_COLOR = [1, 0.5, 0.25];
26 | const EXPECTED_LINEAR = {
27 | mode: 'rgb',
28 | r: 0.8375,
29 | g: 0.19924999999999998,
30 | b: 0.0625
31 | };
32 | const EXPECTED_BIASED = {
33 | mode: 'rgb',
34 | r: 0.8984375,
35 | g: 0.21828124999999998,
36 | b: 0.0390625
37 | };
38 |
39 | function ryb2rgb(coords, bias = true) {
40 | const biased_coords = bias
41 | ? coords.map(t => t * t * (3 - 2 * t))
42 | : coords;
43 | return {
44 | mode: 'rgb',
45 | r: trilerp(...RYB_CUBE.map(it => it.r), ...biased_coords),
46 | g: trilerp(...RYB_CUBE.map(it => it.g), ...biased_coords),
47 | b: trilerp(...RYB_CUBE.map(it => it.b), ...biased_coords)
48 | };
49 | }
50 |
51 | assert.deepEqual(ryb2rgb(RYB_COLOR, false), EXPECTED_LINEAR, 'ryb: linear');
52 | assert.deepEqual(ryb2rgb(RYB_COLOR), EXPECTED_BIASED, 'ryb: biased');
53 | });
54 |
--------------------------------------------------------------------------------
/test/lrgb.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { rgb, lrgb, formatCss } from '../src/index.js';
4 |
5 | test('round-trip', t => {
6 | let in_gamut = {
7 | mode: 'rgb',
8 | r: 0.1,
9 | g: 0.25,
10 | b: 0.7
11 | };
12 | assert.deepEqual(
13 | lrgb(in_gamut),
14 | {
15 | mode: 'lrgb',
16 | r: 0.010022825574869039,
17 | g: 0.05087608817155679,
18 | b: 0.44798841244188325
19 | },
20 | 'in gamut'
21 | );
22 | assert.deepEqual(rgb(lrgb(in_gamut)), in_gamut, 'in gamut');
23 |
24 | let out_of_gamut = {
25 | mode: 'rgb',
26 | r: -0.2,
27 | g: 0.25,
28 | b: 2.7
29 | };
30 | assert.deepEqual(
31 | lrgb(out_of_gamut),
32 | {
33 | mode: 'lrgb',
34 | r: -0.033104766570885055,
35 | g: 0.05087608817155679,
36 | b: 10.011195548645787
37 | },
38 | 'out of gamut'
39 | );
40 | assert.deepEqual(rgb(lrgb(out_of_gamut)), out_of_gamut, 'out of gamut');
41 | });
42 |
43 | test('color(srgb-linear)', t => {
44 | assert.deepEqual(lrgb('color(srgb-linear 1 0 0 / 0.25)'), {
45 | r: 1,
46 | g: 0,
47 | b: 0,
48 | alpha: 0.25,
49 | mode: 'lrgb'
50 | });
51 | assert.deepEqual(lrgb('color(srgb-linear 0% 50% 0.5 / 25%)'), {
52 | r: 0,
53 | g: 0.5,
54 | b: 0.5,
55 | alpha: 0.25,
56 | mode: 'lrgb'
57 | });
58 | });
59 |
60 | test('formatCss', t => {
61 | assert.equal(
62 | formatCss('color(srgb-linear 0% 50% 0.5 / 25%)'),
63 | 'color(srgb-linear 0 0.5 0.5 / 0.25)'
64 | );
65 | });
66 |
67 | test('missing components', t => {
68 | assert.ok(rgb('color(srgb-linear none 0.5 none)'), 'lrgb to rgb is ok');
69 | assert.deepEqual(
70 | rgb('color(srgb-linear none 0.5 none)'),
71 | rgb('color(srgb-linear 0 0.5 0')
72 | );
73 | assert.ok(lrgb('rgb(none 100 20)'), 'rgb to lrgb is ok');
74 | assert.deepEqual(lrgb('rgb(none 100 20)'), lrgb('rgb(0 100 20)'));
75 | });
76 |
--------------------------------------------------------------------------------
/test/luv.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { luv, rgb } from '../src/index.js';
4 |
5 | test('luv', t => {
6 | assert.deepEqual(
7 | luv('white'),
8 | {
9 | mode: 'luv',
10 | l: 100.00000139649632,
11 | u: -0.00000836831738933014,
12 | v: 0.0000101992089921354
13 | },
14 | 'white'
15 | );
16 | assert.deepEqual(luv('black'), { mode: 'luv', l: 0, u: 0, v: 0 }, 'black');
17 | assert.deepEqual(
18 | luv('red'),
19 | {
20 | mode: 'luv',
21 | l: 54.29054294696968,
22 | u: 175.03580817106865,
23 | v: 25.95390361533953
24 | },
25 | 'red'
26 | );
27 | assert.deepEqual(
28 | luv('#00cc0080'),
29 | {
30 | mode: 'luv',
31 | l: 71.74973747305378,
32 | u: -69.38655858949816,
33 | v: 71.27396920932334,
34 | alpha: 0.5019607843137255
35 | },
36 | '#00cc0080'
37 | );
38 | });
39 |
40 | test('color(--luv)', t => {
41 | assert.deepEqual(luv('color(--luv 30 0.5 1 / 0.25)'), {
42 | l: 30,
43 | u: 0.5,
44 | v: 1,
45 | alpha: 0.25,
46 | mode: 'luv'
47 | });
48 | });
49 |
50 | test('missing components', t => {
51 | assert.ok(rgb('color(--luv none 0.5 none)'), 'luv to rgb is ok');
52 | assert.deepEqual(
53 | rgb('color(--luv none 0.5 none)'),
54 | rgb('color(--luv 0 0.5 0)')
55 | );
56 | assert.ok(luv('rgb(none 100 20)'), 'rgb to luv is ok');
57 | assert.deepEqual(luv('rgb(none 100 20)'), luv('rgb(0 100 20)'));
58 | });
59 |
--------------------------------------------------------------------------------
/test/nearest.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { nearest, colorsNamed } from '../src/index.js';
4 |
5 | let nearestNamedColor = nearest(Object.keys(colorsNamed));
6 |
7 | test('check against named colors', test => {
8 | assert.deepEqual(
9 | nearestNamedColor('red'),
10 | ['red'],
11 | 'Closest named color to red'
12 | );
13 |
14 | assert.deepEqual(
15 | nearestNamedColor('red', Infinity, 0.5),
16 | [
17 | 'red',
18 | 'orangered',
19 | 'crimson',
20 | 'firebrick',
21 | 'brown',
22 | 'darkred',
23 | 'chocolate',
24 | 'tomato',
25 | 'maroon'
26 | ],
27 | 'Close named colors to red, d <= 0.5'
28 | );
29 | });
30 |
31 | test('nearest() with accessor', t => {
32 | let palette = {
33 | Burgundy: '#914e72',
34 | Blue: '#0078bf',
35 | Green: '#00a95c',
36 | 'Medium Blue': '#3255a4',
37 | 'Bright Red': '#f15060'
38 | };
39 | let names = Object.keys(palette);
40 | let nearestColors = nearest(names, undefined, name => palette[name]);
41 | assert.deepEqual(nearestColors('red', 1), ['Bright Red']);
42 | });
43 |
--------------------------------------------------------------------------------
/test/normalizePositions.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import normalize from '../src/util/normalizePositions.js';
4 |
5 | test('util: normalizePositions', t => {
6 | assert.deepEqual(
7 | normalize([undefined, undefined, undefined, undefined, undefined]),
8 | [0, 0.25, 0.5, 0.75, 1]
9 | );
10 |
11 | assert.deepEqual(
12 | normalize([0.2, undefined, undefined, 0.8]),
13 | [0.2, 0.4, 0.6000000000000001, 0.8]
14 | );
15 | });
16 |
--------------------------------------------------------------------------------
/test/okhsl.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { okhsl, rgb, formatHex, formatCss } from '../src/index.js';
4 |
5 | test('rgb → okhsl', t => {
6 | assert.equal(
7 | formatCss(okhsl('red')),
8 | 'color(--okhsl 29.2338851923426 1.0000000001434 0.5680846525040861)',
9 | 'red'
10 | );
11 | assert.equal(
12 | formatCss(okhsl('white')),
13 | 'color(--okhsl none 0 0.9999999923961895)',
14 | 'white'
15 | );
16 | assert.equal(formatCss(okhsl('black')), 'color(--okhsl none 0 0)', 'black');
17 | assert.equal(
18 | formatCss(okhsl('#3333')),
19 | 'color(--okhsl none 0 0.2209950715093747 / 0.2)',
20 | '#333'
21 | );
22 | });
23 |
24 | test('okhsl → rgb', t => {
25 | assert.equal(
26 | formatHex(
27 | 'color(--okhsl 29.233885192342633 1.0000000001433997 0.5680846525040862)'
28 | ),
29 | '#ff0000',
30 | 'red'
31 | );
32 | assert.equal(
33 | formatHex('color(--okhsl 0 0 0.9999999923961898)'),
34 | '#ffffff',
35 | 'white'
36 | );
37 | assert.equal(formatHex('color(--okhsl 0 0 0)'), '#000000', 'black');
38 | assert.equal(
39 | formatHex('color(--okhsl 0 0 0.2209950715093747 / 0.2)'),
40 | '#333333',
41 | '#333'
42 | );
43 | assert.deepEqual(
44 | formatHex('color(--okhsl 0 1 1)'),
45 | '#ffffff',
46 | 'color(--okhsl 0 1 1)'
47 | );
48 | assert.equal(
49 | formatHex('color(--okhsl 0 1 0)'),
50 | '#000000',
51 | 'color(--okhsl 0 1 0)'
52 | );
53 | });
54 |
55 | test('rgb → okhsl → rgb', t => {
56 | assert.equal(formatHex(okhsl('red')), '#ff0000', 'red');
57 | assert.equal(formatHex(okhsl('white')), '#ffffff', 'white');
58 | assert.equal(formatHex(okhsl('black')), '#000000', 'black');
59 | assert.equal(formatHex(okhsl('#3333')), '#333333', '#333');
60 | });
61 |
62 | test('missing components', t => {
63 | assert.ok(rgb('color(--okhsl none 0.5 none)'));
64 | assert.deepEqual(
65 | rgb('color(--okhsl none 0.5 none)'),
66 | rgb('color(--okhsl 0 0.5 0)')
67 | );
68 | assert.ok(okhsl('rgb(none 100 20)'));
69 | assert.deepEqual(okhsl('rgb(none 100 20)'), okhsl('rgb(0 100 20)'));
70 | });
71 |
--------------------------------------------------------------------------------
/test/okhsv.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { okhsv, rgb, formatHex, formatCss } from '../src/index.js';
4 |
5 | test('rgb → okhsv', t => {
6 | assert.equal(
7 | formatCss(okhsv('red')),
8 | 'color(--okhsv 29.2338851923426 0.9995219692256307 0.9999999999999997)',
9 | 'red'
10 | );
11 | assert.equal(
12 | formatCss(okhsv('white')),
13 | 'color(--okhsv none 0 1.00000009386827)',
14 | 'white'
15 | );
16 | assert.equal(formatCss(okhsv('black')), 'color(--okhsv none 0 0)', 'black');
17 | assert.equal(
18 | formatCss(okhsv('#3333')),
19 | 'color(--okhsv none 0 0.220995101721347 / 0.2)',
20 | '#333'
21 | );
22 | });
23 |
24 | test('okhsv → rgb', t => {
25 | assert.equal(
26 | formatHex(
27 | 'color(--okhsv 29.233885192342633 0.9995219692256989 1.0000000001685625)'
28 | ),
29 | '#ff0000',
30 | 'red'
31 | );
32 | assert.equal(
33 | formatHex('color(--okhsv 0 0 0.9999999923961898)'),
34 | '#ffffff',
35 | 'white'
36 | );
37 | assert.equal(formatHex('color(--okhsv 0 0 0)'), '#000000', 'black');
38 | assert.equal(
39 | formatHex('color(--okhsv 0 0 0.2209950715093747 / 0.2)'),
40 | '#333333',
41 | '#333'
42 | );
43 | assert.equal(
44 | formatHex('color(--okhsv 0 1 1)'),
45 | '#ff0088',
46 | 'color(--okhsv 0 1 1)'
47 | );
48 | assert.equal(
49 | formatHex('color(--okhsv 0 1 0)'),
50 | '#000000',
51 | 'color(--okhsv 0 1 0)'
52 | );
53 | });
54 |
55 | test('rgb → okhsv → rgb', t => {
56 | assert.equal(formatHex(okhsv('red')), '#ff0000', 'red');
57 | assert.equal(formatHex(okhsv('white')), '#ffffff', 'white');
58 | assert.equal(formatHex(okhsv('black')), '#000000', 'black');
59 | assert.equal(formatHex(okhsv('#3333')), '#333333', '#333');
60 | });
61 |
62 | test('missing components', t => {
63 | assert.ok(rgb('color(--okhsv none 0.5 none)'));
64 | assert.deepEqual(
65 | rgb('color(--okhsv none 0.5 none)'),
66 | rgb('color(--okhsv 0 0.5 0)')
67 | );
68 | assert.ok(okhsv('rgb(none 100 20)'));
69 | assert.deepEqual(okhsv('rgb(none 100 20)'), okhsv('rgb(0 100 20)'));
70 | });
71 |
--------------------------------------------------------------------------------
/test/p3.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { p3, rgb, formatCss } from '../src/index.js';
4 |
5 | test('p3', t => {
6 | assert.deepEqual(
7 | p3('white'),
8 | {
9 | mode: 'p3',
10 | r: 0.9999999999999999,
11 | g: 0.9999999999999997,
12 | b: 0.9999999999999997
13 | },
14 | 'white'
15 | );
16 | assert.deepEqual(p3('black'), { mode: 'p3', r: 0, g: 0, b: 0 }, 'black');
17 | assert.deepEqual(
18 | p3('red'),
19 | {
20 | mode: 'p3',
21 | r: 0.9174875573251657,
22 | g: 0.20028680774084662,
23 | b: 0.1385605912111141
24 | },
25 | 'red'
26 | );
27 | });
28 |
29 | test('color(display-p3)', t => {
30 | assert.deepEqual(p3('color(display-p3 1 0 0 / 0.25)'), {
31 | r: 1,
32 | g: 0,
33 | b: 0,
34 | alpha: 0.25,
35 | mode: 'p3'
36 | });
37 | assert.deepEqual(p3('color(display-p3 0% 50% 0.5 / 25%)'), {
38 | r: 0,
39 | g: 0.5,
40 | b: 0.5,
41 | alpha: 0.25,
42 | mode: 'p3'
43 | });
44 | });
45 |
46 | test('formatCss', t => {
47 | assert.equal(
48 | formatCss('color(display-p3 0% 50% 0.5 / 25%)'),
49 | 'color(display-p3 0 0.5 0.5 / 0.25)'
50 | );
51 | });
52 |
53 | test('missing components', t => {
54 | assert.ok(rgb('color(display-p3 none 0.5 none)'), 'p3 to rgb is ok');
55 | assert.deepEqual(
56 | rgb('color(display-p3 none 0.5 none)'),
57 | rgb('color(display-p3 0 0.5 0')
58 | );
59 | assert.ok(p3('rgb(none 100 20)'), 'rgb to p3 is ok');
60 | assert.deepEqual(p3('rgb(none 100 20)'), p3('rgb(0 100 20)'));
61 | });
62 |
--------------------------------------------------------------------------------
/test/prophoto.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { prophoto, rgb, formatCss } from '../src/index.js';
4 |
5 | test('prophoto', t => {
6 | assert.deepEqual(
7 | prophoto('white'),
8 | {
9 | mode: 'prophoto',
10 | r: 0.9999999886664714,
11 | g: 1.000000032778334,
12 | b: 0.9999999636791803
13 | },
14 | 'white'
15 | );
16 | assert.deepEqual(
17 | prophoto('black'),
18 | { mode: 'prophoto', r: 0, g: 0, b: 0 },
19 | 'black'
20 | );
21 | assert.deepEqual(
22 | prophoto('red'),
23 | {
24 | mode: 'prophoto',
25 | r: 0.7022480690905015,
26 | g: 0.2757205683151893,
27 | b: 0.10354759457098542
28 | },
29 | 'red'
30 | );
31 | });
32 |
33 | test('color(prophoto-rgb)', t => {
34 | assert.deepEqual(prophoto('color(prophoto-rgb 1 0 0 / 0.25)'), {
35 | r: 1,
36 | g: 0,
37 | b: 0,
38 | alpha: 0.25,
39 | mode: 'prophoto'
40 | });
41 | assert.deepEqual(prophoto('color(prophoto-rgb 0% 50% 0.5 / 25%)'), {
42 | r: 0,
43 | g: 0.5,
44 | b: 0.5,
45 | alpha: 0.25,
46 | mode: 'prophoto'
47 | });
48 | });
49 |
50 | test('formatCss', t => {
51 | assert.equal(
52 | formatCss('color(prophoto-rgb 0% 50% 0.5 / 25%)'),
53 | 'color(prophoto-rgb 0 0.5 0.5 / 0.25)'
54 | );
55 | });
56 |
57 | test('missing components', t => {
58 | assert.ok(
59 | rgb('color(prophoto-rgb none 0.5 none)'),
60 | 'prophoto to rgb is ok'
61 | );
62 | assert.deepEqual(
63 | rgb('color(prophoto-rgb none 0.5 none)'),
64 | rgb('color(prophoto-rgb 0 0.5 0')
65 | );
66 | assert.ok(prophoto('rgb(none 100 20)'), 'rgb to prophoto is ok');
67 | assert.deepEqual(prophoto('rgb(none 100 20)'), prophoto('rgb(0 100 20)'));
68 | });
69 |
--------------------------------------------------------------------------------
/test/random.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { random } from '../src/index.js';
4 |
5 | test('random (rgb)', test => {
6 | let c1 = random();
7 | let c2 = random('rgb', { r: 0.1 });
8 | let c3 = random('rgb', { r: [0.4, 0.6] });
9 |
10 | assert.equal(c1.mode, 'rgb');
11 | assert.ok(c1.r >= 0);
12 | assert.ok(c1.r <= 1);
13 |
14 | assert.equal(c2.r, 0.1);
15 | assert.ok(c3.r >= 0.4);
16 | assert.ok(c3.r <= 0.6);
17 | });
18 |
19 | test('random (lch)', test => {
20 | let c = random('lch');
21 | assert.equal(c.mode, 'lch');
22 | });
23 |
--------------------------------------------------------------------------------
/test/rec2020.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { rec2020, rgb, formatCss } from '../src/index.js';
4 |
5 | test('rec2020', t => {
6 | assert.deepEqual(
7 | rec2020('white'),
8 | { mode: 'rec2020', r: 1, g: 1, b: 1 },
9 | 'white'
10 | );
11 | assert.deepEqual(
12 | rec2020('black'),
13 | { mode: 'rec2020', r: 0, g: 0, b: 0 },
14 | 'black'
15 | );
16 | assert.deepEqual(
17 | rec2020('red'),
18 | {
19 | mode: 'rec2020',
20 | r: 0.7919771358198009,
21 | g: 0.23097568481079728,
22 | b: 0.07376147493817597
23 | },
24 | 'red'
25 | );
26 | });
27 |
28 | test('color(rec2020)', t => {
29 | assert.deepEqual(rec2020('color(rec2020 1 0 0 / 0.25)'), {
30 | r: 1,
31 | g: 0,
32 | b: 0,
33 | alpha: 0.25,
34 | mode: 'rec2020'
35 | });
36 | assert.deepEqual(rec2020('color(rec2020 0% 50% 0.5 / 25%)'), {
37 | r: 0,
38 | g: 0.5,
39 | b: 0.5,
40 | alpha: 0.25,
41 | mode: 'rec2020'
42 | });
43 | });
44 |
45 | test('formatCss', t => {
46 | assert.equal(
47 | formatCss('color(rec2020 0% 50% 0.5 / 25%)'),
48 | 'color(rec2020 0 0.5 0.5 / 0.25)'
49 | );
50 | });
51 |
52 | test('missing components', t => {
53 | assert.ok(rgb('color(rec2020 none 0.5 none)'), 'rec2020 to rgb is ok');
54 | assert.deepEqual(
55 | rgb('color(rec2020 none 0.5 none)'),
56 | rgb('color(rec2020 0 0.5 0')
57 | );
58 | assert.ok(rec2020('rgb(none 100 20)'), 'rgb to rec2020 is ok');
59 | assert.deepEqual(rec2020('rgb(none 100 20)'), rec2020('rgb(0 100 20)'));
60 | });
61 |
--------------------------------------------------------------------------------
/test/tree-shaking/not-tree-shaken.js:
--------------------------------------------------------------------------------
1 | import { parseHex, convertRgbToHsl, serializeHsl } from '../../src/index.js';
2 |
3 | console.log(serializeHsl(convertRgbToHsl(parseHex('#ffcc00'))));
4 |
--------------------------------------------------------------------------------
/test/tree-shaking/tree-shaken.js:
--------------------------------------------------------------------------------
1 | import { parseHex, convertRgbToHsl, serializeHsl } from '../../src/index-fn.js';
2 |
3 | console.log(serializeHsl(convertRgbToHsl(parseHex('#ffcc00'))));
4 |
--------------------------------------------------------------------------------
/test/wcag.test.js:
--------------------------------------------------------------------------------
1 | import { wcagLuminance, wcagContrast } from '../src/index.js';
2 | import test from 'node:test';
3 | import assert from 'node:assert';
4 |
5 | test('wcagLuminance', t => {
6 | assert.equal(wcagLuminance('white'), 1);
7 | assert.equal(wcagLuminance('black'), 0);
8 | assert.equal(wcagLuminance('#999'), 0.31854677812509186);
9 | });
10 |
11 | test('wcagContrast', t => {
12 | assert.equal(wcagContrast('black', 'white'), 21);
13 | assert.equal(wcagContrast('white', 'black'), 21);
14 | assert.equal(wcagContrast('red', 'red'), 1);
15 | });
16 |
--------------------------------------------------------------------------------
/test/xyb.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { xyb, rgb, formatRgb, formatCss } from '../src/index.js';
4 |
5 | test('rgb -> xyb', t => {
6 | assert.deepEqual(xyb('purple'), {
7 | mode: 'xyb',
8 | x: 0.013838974535428816,
9 | y: 0.27055837298918495,
10 | b: 0.1333134464452821
11 | });
12 | });
13 |
14 | test('rgb -> xyb -> rgb', t => {
15 | assert.deepEqual(
16 | formatRgb(xyb('rgb(255, 255, 255)')),
17 | 'rgb(255, 255, 255)',
18 | 'white'
19 | );
20 |
21 | assert.deepEqual(formatRgb(xyb('rgb(0, 0, 0)')), 'rgb(0, 0, 0)', 'black');
22 | assert.deepEqual(formatRgb(xyb('rgb(100, 0, 0)')), 'rgb(100, 0, 0)', 'red');
23 | assert.deepEqual(
24 | formatRgb(xyb('rgb(0, 120, 0)')),
25 | 'rgb(0, 120, 0)',
26 | 'blue'
27 | );
28 | assert.deepEqual(formatRgb(xyb('rgb(0, 0, 89)')), 'rgb(0, 0, 89)', 'green');
29 | });
30 |
31 | test('color(--xyb)', t => {
32 | assert.deepEqual(xyb('color(--xyb 1 0 0 / 0.25)'), {
33 | x: 1,
34 | y: 0,
35 | b: 0,
36 | alpha: 0.25,
37 | mode: 'xyb'
38 | });
39 | assert.deepEqual(xyb('color(--xyb 0% 50% 0.5 / 25%)'), {
40 | x: 0,
41 | y: 0.5,
42 | b: 0.5,
43 | alpha: 0.25,
44 | mode: 'xyb'
45 | });
46 | });
47 |
48 | test('formatCss', t => {
49 | assert.equal(
50 | formatCss('color(--xyb 0% 50% 0.5 / 25%)'),
51 | 'color(--xyb 0 0.5 0.5 / 0.25)'
52 | );
53 | });
54 |
55 | test('missing components', t => {
56 | assert.ok(rgb('color(--xyb none 0.5 none)'), 'xyb to rgb is ok');
57 | assert.deepEqual(
58 | rgb('color(--xyb none 0.5 none)'),
59 | rgb('color(--xyb 0 0.5 0')
60 | );
61 | assert.ok(xyb('rgb(none 100 20)'), 'rgb to xyb is ok');
62 | assert.deepEqual(xyb('rgb(none 100 20)'), xyb('rgb(0 100 20)'));
63 | });
64 |
--------------------------------------------------------------------------------
/test/xyz50.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { xyz50, rgb, formatCss } from '../src/index.js';
4 |
5 | test('xyz50', t => {
6 | assert.deepEqual(
7 | xyz50('white'),
8 | {
9 | mode: 'xyz50',
10 | x: 0.9642956660812442,
11 | y: 1.0000000361162846,
12 | z: 0.8251045485672053
13 | },
14 | 'white'
15 | );
16 | assert.deepEqual(xyz50('black'), { mode: 'xyz50', x: 0, y: 0, z: 0 });
17 | assert.deepEqual(
18 | xyz50('red'),
19 | {
20 | mode: 'xyz50',
21 | x: 0.436065742824811,
22 | y: 0.22249319175623702,
23 | z: 0.013923904500943465
24 | },
25 | 'red'
26 | );
27 | assert.deepEqual(
28 | xyz50('#00cc0080'),
29 | {
30 | mode: 'xyz50',
31 | x: 0.23256498648213272,
32 | y: 0.43287600197031817,
33 | z: 0.05862033437620246,
34 | alpha: 0.5019607843137255
35 | },
36 | '#00cc0080'
37 | );
38 | });
39 |
40 | test('color(xyz-d50)', t => {
41 | assert.deepEqual(xyz50('color(xyz-d50 1 0 0 / 0.25)'), {
42 | x: 1,
43 | y: 0,
44 | z: 0,
45 | alpha: 0.25,
46 | mode: 'xyz50'
47 | });
48 | assert.deepEqual(xyz50('color(xyz-d50 0% 50% 0.5 / 25%)'), {
49 | x: 0,
50 | y: 0.5,
51 | z: 0.5,
52 | alpha: 0.25,
53 | mode: 'xyz50'
54 | });
55 | });
56 |
57 | test('color(--xyz-d50)', t => {
58 | assert.deepEqual(xyz50('color(--xyz-d50 1 0 0 / 0.25)'), undefined);
59 | });
60 |
61 | test('formatCss', t => {
62 | assert.equal(
63 | formatCss('color(xyz-d50 0% 50% 0.5 / 25%)'),
64 | 'color(xyz-d50 0 0.5 0.5 / 0.25)'
65 | );
66 | });
67 |
68 | test('missing components', t => {
69 | assert.ok(rgb('color(xyz-d50 none 0.5 none)'), 'xyz50 to rgb is ok');
70 | assert.deepEqual(
71 | rgb('color(xyz-d50 none 0.5 none)'),
72 | rgb('color(xyz-d50 0 0.5 0')
73 | );
74 | assert.ok(xyz50('rgb(none 100 20)'), 'rgb to xyz50 is ok');
75 | assert.deepEqual(xyz50('rgb(none 100 20)'), xyz50('rgb(0 100 20)'));
76 | });
77 |
--------------------------------------------------------------------------------
/test/yiq.test.js:
--------------------------------------------------------------------------------
1 | import test from 'node:test';
2 | import assert from 'node:assert';
3 | import { yiq, rgb, formatRgb, formatCss } from '../src/index.js';
4 |
5 | test('rgb -> yiq', t => {
6 | assert.deepEqual(yiq('purple'), {
7 | mode: 'yiq',
8 | y: 0.20749931419607845,
9 | i: 0.13762565019607842,
10 | q: 0.26233329443137254
11 | });
12 | });
13 |
14 | test('rgb -> yiq -> rgb', t => {
15 | assert.deepEqual(
16 | formatRgb(yiq('rgb(255, 255, 255)')),
17 | 'rgb(255, 255, 255)',
18 | 'white'
19 | );
20 |
21 | assert.deepEqual(formatRgb(yiq('rgb(0, 0, 0)')), 'rgb(0, 0, 0)', 'black');
22 | assert.deepEqual(formatRgb(yiq('rgb(100, 0, 0)')), 'rgb(100, 0, 0)', 'red');
23 | assert.deepEqual(
24 | formatRgb(yiq('rgb(0, 120, 0)')),
25 | 'rgb(0, 120, 0)',
26 | 'blue'
27 | );
28 | assert.deepEqual(formatRgb(yiq('rgb(0, 0, 89)')), 'rgb(0, 0, 89)', 'green');
29 | });
30 |
31 | test('color(--yiq)', t => {
32 | assert.deepEqual(yiq('color(--yiq 1 0 0 / 0.25)'), {
33 | y: 1,
34 | i: 0,
35 | q: 0,
36 | alpha: 0.25,
37 | mode: 'yiq'
38 | });
39 | assert.deepEqual(yiq('color(--yiq 0% 50% 0.5 / 25%)'), {
40 | y: 0,
41 | i: 0.5,
42 | q: 0.5,
43 | alpha: 0.25,
44 | mode: 'yiq'
45 | });
46 | });
47 |
48 | test('formatCss', t => {
49 | assert.equal(
50 | formatCss('color(--yiq 0% 50% 0.5 / 25%)'),
51 | 'color(--yiq 0 0.5 0.5 / 0.25)'
52 | );
53 | });
54 |
55 | test('missing components', t => {
56 | assert.ok(rgb('color(--yiq none 0.5 none)'), 'yiq to rgb is ok');
57 | assert.deepEqual(
58 | rgb('color(--yiq none 0.5 none)'),
59 | rgb('color(--yiq 0 0.5 0')
60 | );
61 | assert.ok(yiq('rgb(none 100 20)'), 'rgb to yiq is ok');
62 | assert.deepEqual(yiq('rgb(none 100 20)'), yiq('rgb(0 100 20)'));
63 | });
64 |
--------------------------------------------------------------------------------
/tools/math/itp-matrices.py:
--------------------------------------------------------------------------------
1 | import numpy as np;
2 |
3 | np.set_printoptions(precision=16, sign='-', floatmode='fixed');
4 |
5 | XYZ_D65_TO_REC2020 = np.array([
6 | [1.7166511879712683, -0.3556707837763925, -0.2533662813736599],
7 | [-0.6666843518324893, 1.6164812366349395, 0.0157685458139111],
8 | [0.0176398574453108, -0.0427706132578085, 0.9421031212354739]
9 | ]);
10 | REC2020_TO_LMS = np.array([
11 | [1688, 2146, 262],
12 | [683, 2951, 462],
13 | [99, 309, 3688]
14 | ]) / 4096;
15 | C = np.matmul(REC2020_TO_LMS, XYZ_D65_TO_REC2020);
16 | Cinv = np.linalg.inv(C);
17 |
18 | print(C);
19 | print(Cinv);
--------------------------------------------------------------------------------
/tools/math/rec2020-matrices.py:
--------------------------------------------------------------------------------
1 | import numpy as np;
2 | np.set_printoptions(precision=16, sign='-', floatmode='fixed');
3 |
4 | [Xw, Yw, Zw] = [0.3127 / 0.329, 1, (1 - 0.3127 - 0.329) / 0.329];
5 | [xr, yr, xg, yg, xb, yb] = [0.708, 0.292, 0.170, 0.797, 0.131, 0.046];
6 |
7 | # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
8 |
9 | Xr = xr / yr;
10 | Yr = 1;
11 | Zr = (1 - xr - yr) / yr;
12 |
13 | Xg = xg / yg;
14 | Yg = 1;
15 | Zg = (1 - xg - yg) / yg;
16 |
17 | Xb = xb / yb;
18 | Yb = 1;
19 | Zb = (1 - xb - yb) / yb;
20 |
21 | X = np.array([
22 | [Xr, Xg, Xb],
23 | [Yr, Yg, Yb],
24 | [Zr, Zg, Zb]
25 | ]);
26 |
27 | W = np.array([ Xw, Yw, Zw ]);
28 |
29 | [Sr, Sg, Sb] = np.matmul(np.linalg.inv(X), W);
30 |
31 | M = np.array([
32 | [ Sr * Xr, Sg * Xg, Sb * Xb ],
33 | [ Sr * Yr, Sg * Yg, Sb * Yb ],
34 | [ Sr * Zr, Sg * Zg, Sb * Zb ]
35 | ]);
36 |
37 | print(M);
38 | print(np.linalg.inv(M));
--------------------------------------------------------------------------------
/tools/math/requirements.txt:
--------------------------------------------------------------------------------
1 | numpy==1.22.0
--------------------------------------------------------------------------------
/tools/ranges.js:
--------------------------------------------------------------------------------
1 | import { converter, getMode } from '../src/index.js';
2 |
3 | /*
4 | Find the channel value ranges (minimum & maximum)
5 | for a particular color space, by converting lots of
6 | RGB colors to that space.
7 | */
8 | let ranges = (mode, step = 1 / 128) => {
9 | let conv = converter(mode);
10 | let chs = getMode(mode).channels;
11 | let res = chs.reduce(
12 | (acc, ch) => ((acc[ch] = [Infinity, -Infinity]), acc),
13 | {}
14 | );
15 | let r, g, b, c, v;
16 | for (r = 0; r <= 1; r += step) {
17 | for (g = 0; g <= 1; g += step) {
18 | for (b = 0; b <= 1; b += step) {
19 | c = conv({ mode: 'rgb', r, g, b });
20 | chs.forEach(ch => {
21 | v = c[ch];
22 | if (v < res[ch][0]) {
23 | res[ch][0] = v;
24 | }
25 | if (v > res[ch][1]) {
26 | res[ch][1] = v;
27 | }
28 | });
29 | }
30 | }
31 | }
32 | return res;
33 | };
34 |
35 | console.log(ranges(process.argv[2], 1 / 512));
36 |
--------------------------------------------------------------------------------