├── .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 | ![culori](./docs/img/culori.svg) 2 | 3 | npm version bundle size 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 | --------------------------------------------------------------------------------