├── .github ├── FUNDING.yml └── workflows │ ├── node.yml │ └── size.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets ├── divider.png └── logo.png ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── colorModels │ ├── cmyk.ts │ ├── cmykString.ts │ ├── hex.ts │ ├── hsl.ts │ ├── hslString.ts │ ├── hsv.ts │ ├── hwb.ts │ ├── hwbString.ts │ ├── lab.ts │ ├── lch.ts │ ├── lchString.ts │ ├── rgb.ts │ ├── rgbString.ts │ └── xyz.ts ├── colord.ts ├── constants.ts ├── extend.ts ├── get │ ├── getBrightness.ts │ ├── getContrast.ts │ ├── getLuminance.ts │ └── getPerceivedDifference.ts ├── helpers.ts ├── index.ts ├── manipulate │ ├── changeAlpha.ts │ ├── invert.ts │ ├── lighten.ts │ ├── mix.ts │ └── saturate.ts ├── parse.ts ├── plugins │ ├── a11y.ts │ ├── cmyk.ts │ ├── harmonies.ts │ ├── hwb.ts │ ├── lab.ts │ ├── lch.ts │ ├── minify.ts │ ├── mix.ts │ ├── names.ts │ └── xyz.ts ├── random.ts └── types.ts ├── tests ├── benchmark.ts ├── colord.test.ts ├── fixtures.ts └── plugins.test.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: omgovich 2 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [12.x, 14.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm run lint 23 | - run: npm run check-types 24 | - run: npm run test 25 | - run: npm run size 26 | 27 | - name: Code coverage report 28 | uses: codecov/codecov-action@v1 29 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: preactjs/compressed-size-action@v2 12 | with: 13 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 14 | pattern: "{./dist/index.js,./dist/plugins/*.js}" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM 2 | npm-debug.log* 3 | node_modules/ 4 | .npm 5 | /yarn.lock 6 | 7 | # tests 8 | /coverage 9 | /bench 10 | 11 | # IDEs 12 | .vscode/ 13 | *.code-workspace 14 | .idea/ 15 | 16 | # bundler 17 | .cache 18 | dist/ 19 | 20 | # OSX 21 | .DS_Store 22 | .LSOverride 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.9.3 2 | 3 | - Fix types export for TypeScript 4.7 ❤️ [@pkishorez](https://github.com/pkishorez) 4 | 5 | ### 2.9.2 6 | 7 | - Fix: Add "package.json" to exports map 8 | 9 | ### 2.9.1 10 | 11 | - Fix: Make minification lossless 12 | - Fix: Minify to name only if color is opaque 13 | 14 | ### 2.9.0 15 | 16 | - New plugin: Color string minification 🗜 17 | 18 | ### 2.8.0 19 | 20 | - New `delta` method to calculate the perceived color difference between two colors ❤️ [@EricRovell](https://github.com/EricRovell) 21 | 22 | ### 2.7.0 23 | 24 | - Improve `mix` plugin by adding new `tints`, `tones` and `shades` methods ❤️ [@EricRovell](https://github.com/EricRovell) 25 | 26 | ### 2.6.0 27 | 28 | - Support "double split complementary" color harmony generation ❤️ [@EricRovell](https://github.com/EricRovell) & [@lbragile](https://github.com/lbragile) 29 | 30 | ### 2.5.0 31 | 32 | - New `closest` option of `toName` method allows you to find the closest color if there is no exact match 33 | 34 | ### 2.4.0 35 | 36 | - New plugin: Color harmonies generator ❤️ [@EricRovell](https://github.com/EricRovell) 37 | 38 | ### 2.3.0 39 | 40 | - Add new `isEqual` method ❤️ [@EricRovell](https://github.com/EricRovell) 41 | 42 | ### 2.2.0 43 | 44 | - New plugin: CMYK color space ❤️ [@EricRovell](https://github.com/EricRovell) 45 | 46 | ### 2.1.0 47 | 48 | - Add new `hue` and `rotate` methods 49 | 50 | ### 2.0.1 51 | 52 | - Improve the precision of alpha values 53 | 54 | ### 2.0.0 55 | 56 | - Strict string color parsing conforming to the CSS Color Level specifications 57 | 58 | ### 1.7.2 59 | 60 | - Simplify package "exports" field to improve different environments support 61 | 62 | ### 1.7.1 63 | 64 | - Parse a color name disregarding the case 65 | 66 | ### 1.7.0 67 | 68 | - New `getFormat` utility 69 | - Support HWB color strings (CSS functional notation) 70 | - Clamp LAB values as defined in CSS Color Level 4 specs 71 | 72 | ### 1.6.0 73 | 74 | - Improvement: You can now use every angle unit supported by CSS (`deg`, `rad`, `grad`, `turn`) 75 | 76 | ### 1.5.0 77 | 78 | - New utility: Random color generation 79 | 80 | ### 1.4.1 81 | 82 | - Mix colors through CIE LAB color space 83 | 84 | ### 1.4.0 85 | 86 | - New plugin: Color mixing 87 | - Adjust XYZ, LAB and LCH conversions to the D50 white point ([according to the latest CSS specs](https://drafts.csswg.org/css-color-5/#color-spaces)). 88 | 89 | ### 1.3.1 90 | 91 | - Support modern CSS notations of RGB, HSL and LCH color functions 92 | 93 | ### 1.3.0 94 | 95 | - New plugin: CIE LCH color space 96 | 97 | ### 1.2.1 98 | 99 | - Fix: Do not treat 7-digit hex as a valid color ❤️ [@subzey](https://github.com/subzey) 100 | - Parser update: Turn NaN input values into valid numbers ❤️ [@subzey](https://github.com/subzey) 101 | 102 | ### 1.2.0 103 | 104 | - New plugin: CIE LAB color space 105 | 106 | ### 1.1.1 107 | 108 | - Make bundle 1% lighter 109 | 110 | ### 1.1.0 111 | 112 | - Add `isValid` method 113 | 114 | ### 1.0 115 | 116 | - An official production-ready release 117 | 118 | ### 0.10.2 119 | 120 | - Sort named colors dictionary for better compression ❤️ [@subzey](https://github.com/subzey) 121 | 122 | ### 0.10.1 123 | 124 | - Ignore `null` input in the parsers 125 | 126 | ### 0.10 127 | 128 | - Shorten conversion method names (`toRgba` to `toRgb`, etc) 129 | 130 | ### 0.9.3 131 | 132 | - New plugin: HWB color model 133 | - More accurate HSL and HSV conversions 134 | 135 | ### 0.9.2 136 | 137 | - Names plugin: Support "transparent" keyword 138 | 139 | ### 0.9.1 140 | 141 | - Improve package exports 142 | 143 | ### 0.9 144 | 145 | - Add CommonJS exports 146 | 147 | ### 0.8 148 | 149 | - New plugin: a11y (Accessibility) 150 | 151 | ### 0.7 152 | 153 | - New plugin: CIE XYZ color space 154 | 155 | ### 0.6.2 156 | 157 | - 20% speed improvement ❤️ [@jeetiss](https://github.com/jeetiss) 158 | 159 | ### 0.6.1 160 | 161 | - 100% code coverage 162 | 163 | ### 0.6 164 | 165 | - Make plugin available in Parcel which doesn't support exports map yet 166 | - Fix names plugin TS declarations export 167 | - Documentation 168 | 169 | ### 0.5 170 | 171 | - New plugin: CSS color names 172 | 173 | ### 0.4 174 | 175 | - Make the library ESM-first 176 | - Add code coverage reports 177 | 178 | ### 0.3 179 | 180 | - Implement Plugin API 181 | 182 | ### 0.2 183 | 184 | - Support 4 and 8 digit Hex 185 | 186 | ### 0.1 187 | 188 | - Basic API 189 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vlad Shilov omgovich@ya.ru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | colord 4 | 5 |
6 | 7 |
8 | 9 | npm 10 | 11 | 12 | build 13 | 14 | 15 | coverage 16 | 17 | 18 | no dependencies 19 | 20 | 21 | types included 22 | 23 |
24 | 25 |
26 | Colord is a tiny yet powerful tool for high-performance color manipulations and conversions. 27 |
28 | 29 | ## Features 30 | 31 | - 📦 **Small**: Just **1.7 KB** gzipped ([3x+ lighter](#benchmarks) than **color** and **tinycolor2**) 32 | - 🚀 **Fast**: [3x+ faster](#benchmarks) than **color** and **tinycolor2** 33 | - 😍 **Simple**: Chainable API and familiar patterns 34 | - 💪 **Immutable**: No need to worry about data mutations 35 | - 🛡 **Bulletproof**: Written in strict TypeScript and has 100% test coverage 36 | - 🗂 **Typed**: Ships with [types included](#types) 37 | - 🏗 **Extendable**: Built-in [plugin system](#plugins) to add new functionality 38 | - 📚 **CSS-compliant**: Strictly follows CSS Color Level specifications 39 | - 👫 **Works everywhere**: Supports all browsers and Node.js 40 | - 💨 **Dependency-free** 41 | 42 |
---
43 | 44 | ## Benchmarks 45 | 46 | | Library | Operations/sec | Size
(minified) | Size
(gzipped) | Dependencies | Type declarations | 47 | | ----------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | 48 | | colord 👑 | 3,524,989 | [![](https://badgen.net/bundlephobia/min/colord?color=6ead0a&label=)](https://bundlephobia.com/result?p=colord) | [![](https://badgen.net/bundlephobia/minzip/colord?color=6ead0a&label=)](https://bundlephobia.com/result?p=colord) | [![](https://badgen.net/bundlephobia/dependency-count/colord?color=6ead0a&label=)](https://bundlephobia.com/result?p=colord) | [![](https://badgen.net/npm/types/colord?color=6ead0a&label=)](https://bundlephobia.com/result?p=colord) | 49 | | color | 744,263 | [![](https://badgen.net/bundlephobia/min/color?color=red&label=)](https://bundlephobia.com/result?p=color) | [![](https://badgen.net/bundlephobia/minzip/color?color=red&label=)](https://bundlephobia.com/result?p=color) | [![](https://badgen.net/bundlephobia/dependency-count/color?color=red&label=)](https://bundlephobia.com/result?p=color) | [![](https://badgen.net/npm/types/color?color=e6591d&label=)](https://bundlephobia.com/result?p=color) | 50 | | tinycolor2 | 971,312 | [![](https://badgen.net/bundlephobia/min/tinycolor2?color=red&label=)](https://bundlephobia.com/result?p=tinycolor2) | [![](https://badgen.net/bundlephobia/minzip/tinycolor2?color=red&label=)](https://bundlephobia.com/result?p=tinycolor2) | [![](https://badgen.net/bundlephobia/dependency-count/tinycolor2?color=6ead0a&label=)](https://bundlephobia.com/result?p=tinycolor2) | [![](https://badgen.net/npm/types/tinycolor2?color=e6591d&label=)](https://bundlephobia.com/result?p=tinycolor2) | 51 | | ac-colors | 660,722 | [![](https://badgen.net/bundlephobia/min/ac-colors?color=e6591d&label=)](https://bundlephobia.com/result?p=ac-colors) | [![](https://badgen.net/bundlephobia/minzip/ac-colors?color=e6591d&label=)](https://bundlephobia.com/result?p=ac-colors) | [![](https://badgen.net/bundlephobia/dependency-count/ac-colors?color=6ead0a&label=)](https://bundlephobia.com/result?p=ac-colors) | [![](https://badgen.net/npm/types/ac-colors?color=red&label=)](https://bundlephobia.com/result?p=ac-colors) | 52 | | chroma-js | 962,967 | [![](https://badgen.net/bundlephobia/min/chroma-js?color=red&label=)](https://bundlephobia.com/result?p=chroma-js) | [![](https://badgen.net/bundlephobia/minzip/chroma-js?color=red&label=)](https://bundlephobia.com/result?p=chroma-js) | [![](https://badgen.net/bundlephobia/dependency-count/chroma-js?color=red&label=)](https://bundlephobia.com/result?p=chroma-js) | [![](https://badgen.net/npm/types/chroma-js?color=e6591d&label=)](https://bundlephobia.com/result?p=chroma-js) | 53 | 54 | The performance results were generated on a MBP 2019, 2,6 GHz Intel Core i7 by running `npm run benchmark` in the library folder. See [tests/benchmark.ts](https://github.com/omgovich/colord/blob/master/tests/benchmark.ts). 55 | 56 |
---
57 | 58 | ## Getting Started 59 | 60 | ``` 61 | npm i colord 62 | ``` 63 | 64 | ```js 65 | import { colord } from "colord"; 66 | 67 | colord("#ff0000").grayscale().alpha(0.25).toRgbString(); // "rgba(128, 128, 128, 0.25)" 68 | colord("rgb(192, 192, 192)").isLight(); // true 69 | colord("hsl(0, 50%, 50%)").darken(0.25).toHex(); // "#602020" 70 | ``` 71 | 72 |
---
73 | 74 | ## Supported Color Models 75 | 76 | - Hexadecimal strings (including 3, 4 and 8 digit notations) 77 | - RGB strings and objects 78 | - HSL strings and objects 79 | - HSV objects 80 | - Color names ([via plugin](#plugins)) 81 | - HWB objects and strings ([via plugin](#plugins)) 82 | - CMYK objects and strings ([via plugin](#plugins)) 83 | - LCH objects and strings ([via plugin](#plugins)) 84 | - LAB objects ([via plugin](#plugins)) 85 | - XYZ objects ([via plugin](#plugins)) 86 | 87 |
---
88 | 89 | ## API 90 | 91 | ### Color parsing 92 | 93 |
94 | colord(input) 95 | 96 | Parses the given input and creates a new Colord instance. String parsing strictly conforms to [CSS Color Level Specifications](https://www.w3.org/TR/css-color-4/#color-type). 97 | 98 | ```js 99 | import { colord } from "colord"; 100 | 101 | // String input examples 102 | colord("#FFF"); 103 | colord("#ffffff"); 104 | colord("#ffffffff"); 105 | colord("rgb(255, 255, 255)"); 106 | colord("rgba(255, 255, 255, 0.5)"); 107 | colord("rgba(100% 100% 100% / 50%)"); 108 | colord("hsl(90, 100%, 100%)"); 109 | colord("hsla(90, 100%, 100%, 0.5)"); 110 | colord("hsla(90deg 100% 100% / 50%)"); 111 | colord("tomato"); // requires "names" plugin 112 | 113 | // Object input examples 114 | colord({ r: 255, g: 255, b: 255 }); 115 | colord({ r: 255, g: 255, b: 255, a: 1 }); 116 | colord({ h: 360, s: 100, l: 100 }); 117 | colord({ h: 360, s: 100, l: 100, a: 1 }); 118 | colord({ h: 360, s: 100, v: 100 }); 119 | colord({ h: 360, s: 100, v: 100, a: 1 }); 120 | ``` 121 | 122 | Check out the ["Plugins"](#plugins) section for more input format examples. 123 | 124 |
125 | 126 |
127 | getFormat(input) 128 | 129 | Returns a color model name for the input passed to the function. Uses the same parsing system as `colord` function. 130 | 131 | ```js 132 | import { getFormat } from "colord"; 133 | 134 | getFormat("#aabbcc"); // "hex" 135 | getFormat({ r: 13, g: 237, b: 162, a: 0.5 }); // "rgb" 136 | getFormat("hsl(180deg, 50%, 50%)"); // "hsl" 137 | getFormat("WUT?"); // undefined 138 | ``` 139 | 140 |
141 | 142 | ### Color conversion 143 | 144 |
145 | .toHex() 146 | 147 | Returns the [hexadecimal representation](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb_colors) of a color. When the alpha channel value of the color is less than 1, it outputs `#rrggbbaa` format instead of `#rrggbb`. 148 | 149 | ```js 150 | colord("rgb(0, 255, 0)").toHex(); // "#00ff00" 151 | colord({ h: 300, s: 100, l: 50 }).toHex(); // "#ff00ff" 152 | colord({ r: 255, g: 255, b: 255, a: 0 }).toHex(); // "#ffffff00" 153 | ``` 154 | 155 |
156 | 157 |
158 | .toRgb() 159 | 160 | ```js 161 | colord("#ff0000").toRgb(); // { r: 255, g: 0, b: 0, a: 1 } 162 | colord({ h: 180, s: 100, l: 50, a: 0.5 }).toRgb(); // { r: 0, g: 255, b: 255, a: 0.5 } 163 | ``` 164 | 165 |
166 | 167 |
168 | .toRgbString() 169 | 170 | ```js 171 | colord("#ff0000").toRgbString(); // "rgb(255, 0, 0)" 172 | colord({ h: 180, s: 100, l: 50, a: 0.5 }).toRgbString(); // "rgba(0, 255, 255, 0.5)" 173 | ``` 174 | 175 |
176 | 177 |
178 | .toHsl() 179 | 180 | Converts a color to [HSL color space](https://en.wikipedia.org/wiki/HSL_and_HSV) and returns an object. 181 | 182 | ```js 183 | colord("#ffff00").toHsl(); // { h: 60, s: 100, l: 50, a: 1 } 184 | colord("rgba(0, 0, 255, 0.5) ").toHsl(); // { h: 240, s: 100, l: 50, a: 0.5 } 185 | ``` 186 | 187 |
188 | 189 |
190 | .toHslString() 191 | 192 | Converts a color to [HSL color space](https://en.wikipedia.org/wiki/HSL_and_HSV) and expresses it through the [functional notation](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#hsl_colors). 193 | 194 | ```js 195 | colord("#ffff00").toHslString(); // "hsl(60, 100%, 50%)" 196 | colord("rgba(0, 0, 255, 0.5)").toHslString(); // "hsla(240, 100%, 50%, 0.5)" 197 | ``` 198 | 199 |
200 | 201 |
202 | .toHsv() 203 | 204 | Converts a color to [HSV color space](https://en.wikipedia.org/wiki/HSL_and_HSV) and returns an object. 205 | 206 | ```js 207 | colord("#ffff00").toHsv(); // { h: 60, s: 100, v: 100, a: 1 } 208 | colord("rgba(0, 255, 255, 0.5) ").toHsv(); // { h: 180, s: 100, v: 100, a: 1 } 209 | ``` 210 | 211 |
212 | 213 |
214 | .toName(options?) (names plugin) 215 | 216 | Converts a color to a [CSS keyword](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#color_keywords). Returns `undefined` if the color is not specified in the specs. 217 | 218 | ```js 219 | import { colord, extend } from "colord"; 220 | import namesPlugin from "colord/plugins/names"; 221 | 222 | extend([namesPlugin]); 223 | 224 | colord("#ff6347").toName(); // "tomato" 225 | colord("#00ffff").toName(); // "cyan" 226 | colord("rgba(0, 0, 0, 0)").toName(); // "transparent" 227 | 228 | colord("#fe0000").toName(); // undefined (the color is not specified in CSS specs) 229 | colord("#fe0000").toName({ closest: true }); // "red" (closest color available) 230 | ``` 231 | 232 |
233 | 234 |
235 | .toCmyk() (cmyk plugin) 236 | 237 | Converts a color to [CMYK](https://en.wikipedia.org/wiki/CMYK_color_model) color space. 238 | 239 | ```js 240 | import { colord, extend } from "colord"; 241 | import cmykPlugin from "colord/plugins/cmyk"; 242 | 243 | extend([cmykPlugin]); 244 | 245 | colord("#ffffff").toCmyk(); // { c: 0, m: 0, y: 0, k: 0, a: 1 } 246 | colord("#555aaa").toCmyk(); // { c: 50, m: 47, y: 0, k: 33, a: 1 } 247 | ``` 248 | 249 |
250 | 251 |
252 | .toCmykString() (cmyk plugin) 253 | 254 | Converts a color to color space. 255 | 256 | Converts a color to [CMYK](https://en.wikipedia.org/wiki/CMYK_color_model) color space and expresses it through the [functional notation](https://www.w3.org/TR/css-color-4/#device-cmyk) 257 | 258 | ```js 259 | import { colord, extend } from "colord"; 260 | import cmykPlugin from "colord/plugins/cmyk"; 261 | 262 | extend([cmykPlugin]); 263 | 264 | colord("#99ffff").toCmykString(); // "device-cmyk(40% 0% 0% 0%)" 265 | colord("#00336680").toCmykString(); // "device-cmyk(100% 50% 0% 60% / 0.5)" 266 | ``` 267 | 268 |
269 | 270 |
271 | .toHwb() (hwb plugin) 272 | 273 | Converts a color to [HWB (Hue-Whiteness-Blackness)](https://en.wikipedia.org/wiki/HWB_color_model) color space. 274 | 275 | ```js 276 | import { colord, extend } from "colord"; 277 | import hwbPlugin from "colord/plugins/hwb"; 278 | 279 | extend([hwbPlugin]); 280 | 281 | colord("#ffffff").toHwb(); // { h: 0, w: 100, b: 0, a: 1 } 282 | colord("#555aaa").toHwb(); // { h: 236, w: 33, b: 33, a: 1 } 283 | ``` 284 | 285 |
286 | 287 |
288 | .toHwbString() (hwb plugin) 289 | 290 | Converts a color to [HWB (Hue-Whiteness-Blackness)](https://en.wikipedia.org/wiki/HWB_color_model) color space and expresses it through the [functional notation](https://www.w3.org/TR/css-color-4/#the-hwb-notation). 291 | 292 | ```js 293 | import { colord, extend } from "colord"; 294 | import hwbPlugin from "colord/plugins/hwb"; 295 | 296 | extend([hwbPlugin]); 297 | 298 | colord("#999966").toHwbString(); // "hwb(60 40% 40%)" 299 | colord("#99ffff").toHwbString(); // "hwb(180 60% 0%)" 300 | colord("#003366").alpha(0.5).toHwbString(); // "hwb(210 0% 60% / 0.5)" 301 | ``` 302 | 303 |
304 | 305 |
306 | .toLab() (lab plugin) 307 | 308 | Converts a color to [CIE LAB](https://en.wikipedia.org/wiki/CIELAB_color_space) color space. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). 309 | 310 | ```js 311 | import { colord, extend } from "colord"; 312 | import labPlugin from "colord/plugins/lab"; 313 | 314 | extend([labPlugin]); 315 | 316 | colord("#ffffff").toLab(); // { l: 100, a: 0, b: 0, alpha: 1 } 317 | colord("#33221180").toLab(); // { l: 14.89, a: 5.77, b: 14.41, alpha: 0.5 } 318 | ``` 319 | 320 |
321 | 322 |
323 | .toLch() (lch plugin) 324 | 325 | Converts a color to [CIE LCH](https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/) color space. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). 326 | 327 | ```js 328 | import { colord, extend } from "colord"; 329 | import lchPlugin from "colord/plugins/lch"; 330 | 331 | extend([lchPlugin]); 332 | 333 | colord("#ffffff").toLch(); // { l: 100, c: 0, h: 0, a: 1 } 334 | colord("#213b0b").toLch(); // { l: 21.85, c: 31.95, h: 127.77, a: 1 } 335 | ``` 336 | 337 |
338 | 339 |
340 | .toLchString() (lch plugin) 341 | 342 | Converts a color to [CIE LCH](https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/) color space and expresses it through the [functional notation](https://www.w3.org/TR/css-color-4/#specifying-lab-lch). 343 | 344 | ```js 345 | import { colord, extend } from "colord"; 346 | import lchPlugin from "colord/plugins/lch"; 347 | 348 | extend([lchPlugin]); 349 | 350 | colord("#ffffff").toLchString(); // "lch(100% 0 0)" 351 | colord("#213b0b").alpha(0.5).toLchString(); // "lch(21.85% 31.95 127.77 / 0.5)" 352 | ``` 353 | 354 |
355 | 356 |
357 | .toXyz() (xyz plugin) 358 | 359 | Converts a color to [CIE XYZ](https://www.sttmedia.com/colormodel-xyz) color space. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). 360 | 361 | ```js 362 | import { colord, extend } from "colord"; 363 | import xyzPlugin from "colord/plugins/xyz"; 364 | 365 | extend([xyzPlugin]); 366 | 367 | colord("#ffffff").toXyz(); // { x: 95.047, y: 100, z: 108.883, a: 1 } 368 | ``` 369 | 370 |
371 | 372 | ### Color manipulation 373 | 374 |
375 | .alpha(value) 376 | 377 | Changes the alpha channel value and returns a new `Colord` instance. 378 | 379 | ```js 380 | colord("rgb(0, 0, 0)").alpha(0.5).toRgbString(); // "rgba(0, 0, 0, 0.5)" 381 | ``` 382 | 383 |
384 | 385 |
386 | .invert() 387 | 388 | Creates a new `Colord` instance containing an inverted (opposite) version of the color. 389 | 390 | ```js 391 | colord("#ffffff").invert().toHex(); // "#000000" 392 | colord("#aabbcc").invert().toHex(); // "#554433" 393 | ``` 394 | 395 |
396 | 397 |
398 | .saturate(amount = 0.1) 399 | 400 | Increases the [HSL saturation](https://en.wikipedia.org/wiki/HSL_and_HSV) of a color by the given amount. 401 | 402 | ```js 403 | colord("#bf4040").saturate(0.25).toHex(); // "#df2020" 404 | colord("hsl(0, 50%, 50%)").saturate(0.5).toHslString(); // "hsl(0, 100%, 50%)" 405 | ``` 406 | 407 |
408 | 409 |
410 | .desaturate(amount = 0.1) 411 | 412 | Decreases the [HSL saturation](https://en.wikipedia.org/wiki/HSL_and_HSV) of a color by the given amount. 413 | 414 | ```js 415 | colord("#df2020").saturate(0.25).toHex(); // "#bf4040" 416 | colord("hsl(0, 100%, 50%)").saturate(0.5).toHslString(); // "hsl(0, 50%, 50%)" 417 | ``` 418 | 419 |
420 | 421 |
422 | .grayscale() 423 | 424 | Makes a gray color with the same lightness as a source color. Same as calling `desaturate(1)`. 425 | 426 | ```js 427 | colord("#bf4040").grayscale().toHex(); // "#808080" 428 | colord("hsl(0, 100%, 50%)").grayscale().toHslString(); // "hsl(0, 0%, 50%)" 429 | ``` 430 | 431 |
432 | 433 |
434 | .lighten(amount = 0.1) 435 | 436 | Increases the [HSL lightness](https://en.wikipedia.org/wiki/HSL_and_HSV) of a color by the given amount. 437 | 438 | ```js 439 | colord("#000000").lighten(0.5).toHex(); // "#808080" 440 | colord("#223344").lighten(0.3).toHex(); // "#5580aa" 441 | colord("hsl(0, 50%, 50%)").lighten(0.5).toHslString(); // "hsl(0, 50%, 100%)" 442 | ``` 443 | 444 |
445 | 446 |
447 | .darken(amount = 0.1) 448 | 449 | Decreases the [HSL lightness](https://en.wikipedia.org/wiki/HSL_and_HSV) of a color by the given amount. 450 | 451 | ```js 452 | colord("#ffffff").darken(0.5).toHex(); // "#808080" 453 | colord("#5580aa").darken(0.3).toHex(); // "#223344" 454 | colord("hsl(0, 50%, 100%)").lighten(0.5).toHslString(); // "hsl(0, 50%, 50%)" 455 | ``` 456 | 457 |
458 | 459 |
460 | .hue(value) 461 | 462 | Changes the hue value and returns a new `Colord` instance. 463 | 464 | ```js 465 | colord("hsl(90, 50%, 50%)").hue(180).toHslString(); // "hsl(180, 50%, 50%)" 466 | colord("hsl(90, 50%, 50%)").hue(370).toHslString(); // "hsl(10, 50%, 50%)" 467 | ``` 468 | 469 |
470 | 471 |
472 | .rotate(amount = 15) 473 | 474 | Increases the [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) hue value of a color by the given amount. 475 | 476 | ```js 477 | colord("hsl(90, 50%, 50%)").rotate(90).toHslString(); // "hsl(180, 50%, 50%)" 478 | colord("hsl(90, 50%, 50%)").rotate(-180).toHslString(); // "hsl(270, 50%, 50%)" 479 | ``` 480 | 481 |
482 | 483 |
484 | .mix(color2, ratio = 0.5) (mix plugin) 485 | 486 | Produces a mixture of two colors and returns the result of mixing them (new Colord instance). 487 | 488 | In contrast to other libraries that perform RGB values mixing, Colord mixes colors through [LAB color space](https://en.wikipedia.org/wiki/CIELAB_color_space). This approach produces better results and doesn't have the drawbacks the legacy way has. 489 | 490 | → [Online demo](https://3cg7o.csb.app/) 491 | 492 | ```js 493 | import { colord, extend } from "colord"; 494 | import mixPlugin from "colord/plugins/mix"; 495 | 496 | extend([mixPlugin]); 497 | 498 | colord("#ffffff").mix("#000000").toHex(); // "#777777" 499 | colord("#800080").mix("#dda0dd").toHex(); // "#af5cae" 500 | colord("#cd853f").mix("#eee8aa", 0.6).toHex(); // "#e3c07e" 501 | colord("#008080").mix("#808000", 0.35).toHex(); // "#50805d" 502 | ``` 503 | 504 |
505 | 506 |
507 | .tints(count = 5) (mix plugin) 508 | 509 | Provides functionality to generate [tints](https://en.wikipedia.org/wiki/Tints_and_shades) of a color. Returns an array of `Colord` instances, including the original color. 510 | 511 | ```js 512 | import { colord, extend } from "colord"; 513 | import mixPlugin from "colord/plugins/mix"; 514 | 515 | extend([mixPlugin]); 516 | 517 | const color = colord("#ff0000"); 518 | color.tints(3).map((c) => c.toHex()); // ["#ff0000", "#ff9f80", "#ffffff"]; 519 | ``` 520 | 521 |
522 | 523 |
524 | .shades(count = 5) (mix plugin) 525 | 526 | Provides functionality to generate [shades](https://en.wikipedia.org/wiki/Tints_and_shades) of a color. Returns an array of `Colord` instances, including the original color. 527 | 528 | ```js 529 | import { colord, extend } from "colord"; 530 | import mixPlugin from "colord/plugins/mix"; 531 | 532 | extend([mixPlugin]); 533 | 534 | const color = colord("#ff0000"); 535 | color.shades(3).map((c) => c.toHex()); // ["#ff0000", "#7a1b0b", "#000000"]; 536 | ``` 537 | 538 |
539 | 540 |
541 | .tones(count = 5) (mix plugin) 542 | 543 | Provides functionality to generate [tones](https://en.wikipedia.org/wiki/Tints_and_shades) of a color. Returns an array of `Colord` instances, including the original color. 544 | 545 | ```js 546 | import { colord, extend } from "colord"; 547 | import mixPlugin from "colord/plugins/mix"; 548 | 549 | extend([mixPlugin]); 550 | 551 | const color = colord("#ff0000"); 552 | color.tones(3).map((c) => c.toHex()); // ["#ff0000", "#c86147", "#808080"]; 553 | ``` 554 | 555 |
556 | 557 |
558 | .harmonies(type = "complementary") (harmonies plugin) 559 | 560 | Provides functionality to generate [harmony colors](). Returns an array of `Colord` instances. 561 | 562 | ```js 563 | import { colord, extend } from "colord"; 564 | import harmoniesPlugin from "colord/plugins/harmonies"; 565 | 566 | extend([harmoniesPlugin]); 567 | 568 | const color = colord("#ff0000"); 569 | color.harmonies("analogous").map((c) => c.toHex()); // ["#ff0080", "#ff0000", "#ff8000"] 570 | color.harmonies("complementary").map((c) => c.toHex()); // ["#ff0000", "#00ffff"] 571 | color.harmonies("double-split-complementary").map((c) => c.toHex()); // ["#ff0080", "#ff0000", "#ff8000", "#00ff80", "#0080ff"] 572 | color.harmonies("rectangle").map((c) => c.toHex()); // ["#ff0000", "#ffff00", "#00ffff", "#0000ff"] 573 | color.harmonies("split-complementary").map((c) => c.toHex()); // ["#ff0000", "#00ff80", "#0080ff"] 574 | color.harmonies("tetradic").map((c) => c.toHex()); // ["#ff0000", "#80ff00", "#00ffff", "#8000ff"] 575 | color.harmonies("triadic").map((c) => c.toHex()); // ["#ff0000", "#00ff00", "#0000ff"] 576 | ``` 577 | 578 |
579 | 580 | ### Color analysis 581 | 582 |
583 | .isValid() 584 | 585 | Returns a boolean indicating whether or not an input has been parsed successfully. 586 | Note: If parsing is unsuccessful, Colord defaults to black (does not throws an error). 587 | 588 | ```js 589 | colord("#ffffff").isValid(); // true 590 | colord("#wwuutt").isValid(); // false 591 | colord("abracadabra").isValid(); // false 592 | colord({ r: 0, g: 0, b: 0 }).isValid(); // true 593 | colord({ r: 0, g: 0, v: 0 }).isValid(); // false 594 | ``` 595 | 596 |
597 | 598 |
599 | .isEqual(color2) 600 | 601 | Determines whether two values are the same color. 602 | 603 | ```js 604 | colord("#000000").isEqual("rgb(0, 0, 0)"); // true 605 | colord("#000000").isEqual("rgb(255, 255, 255)"); // false 606 | ``` 607 | 608 |
609 | 610 |
611 | .alpha() 612 | 613 | ```js 614 | colord("#ffffff").alpha(); // 1 615 | colord("rgba(50, 100, 150, 0.5)").alpha(); // 0.5 616 | ``` 617 | 618 |
619 | 620 |
621 | .hue() 622 | 623 | ```js 624 | colord("hsl(90, 50%, 50%)").hue(); // 90 625 | colord("hsl(-10, 50%, 50%)").hue(); // 350 626 | ``` 627 | 628 |
629 | 630 |
631 | .brightness() 632 | 633 | Returns the brightness of a color (from 0 to 1). The calculation logic is modified from [Web Content Accessibility Guidelines](https://www.w3.org/TR/AERT/#color-contrast). 634 | 635 | ```js 636 | colord("#000000").brightness(); // 0 637 | colord("#808080").brightness(); // 0.5 638 | colord("#ffffff").brightness(); // 1 639 | ``` 640 | 641 |
642 | 643 |
644 | .isLight() 645 | 646 | Same as calling `brightness() >= 0.5`. 647 | 648 | ```js 649 | colord("#111111").isLight(); // false 650 | colord("#aabbcc").isLight(); // true 651 | colord("#ffffff").isLight(); // true 652 | ``` 653 | 654 |
655 | 656 |
657 | .isDark() 658 | 659 | Same as calling `brightness() < 0.5`. 660 | 661 | ```js 662 | colord("#111111").isDark(); // true 663 | colord("#aabbcc").isDark(); // false 664 | colord("#ffffff").isDark(); // false 665 | ``` 666 | 667 |
668 | 669 |
670 | .luminance() (a11y plugin) 671 | 672 | Returns the relative luminance of a color, normalized to 0 for darkest black and 1 for lightest white as defined by [WCAG 2.0](https://www.w3.org/TR/WCAG20/#relativeluminancedef). 673 | 674 | ```js 675 | colord("#000000").luminance(); // 0 676 | colord("#808080").luminance(); // 0.22 677 | colord("#ccddee").luminance(); // 0.71 678 | colord("#ffffff").luminance(); // 1 679 | ``` 680 | 681 |
682 | 683 |
684 | .contrast(color2 = "#FFF") (a11y plugin) 685 | 686 | Calculates a contrast ratio for a color pair. This luminance difference is expressed as a ratio ranging from 1 (e.g. white on white) to 21 (e.g., black on a white). [WCAG Accessibility Level AA requires](https://webaim.org/articles/contrast/) a ratio of at least 4.5 for normal text and 3 for large text. 687 | 688 | ```js 689 | colord("#000000").contrast(); // 21 (black on white) 690 | colord("#ffffff").contrast("#000000"); // 21 (white on black) 691 | colord("#777777").contrast(); // 4.47 (gray on white) 692 | colord("#ff0000").contrast(); // 3.99 (red on white) 693 | colord("#0000ff").contrast("#ff000"); // 2.14 (blue on red) 694 | ``` 695 | 696 |
697 | 698 |
699 | .isReadable(color2 = "#FFF", options?) (a11y plugin) 700 | 701 | Checks that a background and text color pair is readable according to [WCAG 2.0 Contrast and Color Requirements](https://webaim.org/articles/contrast/). 702 | 703 | ```js 704 | colord("#000000").isReadable(); // true (normal black text on white bg conforms to WCAG AA) 705 | colord("#777777").isReadable(); // false (normal gray text on white bg conforms to WCAG AA) 706 | colord("#ffffff").isReadable("#000000"); // true (normal white text on black bg conforms to WCAG AA) 707 | colord("#e60000").isReadable("#ffff47"); // true (normal red text on yellow bg conforms to WCAG AA) 708 | colord("#e60000").isReadable("#ffff47", { level: "AAA" }); // false (normal red text on yellow bg does not conform to WCAG AAA) 709 | colord("#e60000").isReadable("#ffff47", { level: "AAA", size: "large" }); // true (large red text on yellow bg conforms to WCAG AAA) 710 | ``` 711 | 712 |
713 | 714 |
715 | .delta(color2 = "#FFF") (lab plugin) 716 | 717 | Calculates the perceived color difference between two colors. 718 | The difference calculated according to [Delta E2000](https://en.wikipedia.org/wiki/Color_difference#CIEDE2000). 719 | The return value is `0` if the colors are equal, `1` if they are entirely different. 720 | 721 | ```js 722 | colord("#3296fa").delta("#197dc8"); // 0.099 723 | colord("#faf0c8").delta("#ffffff"); // 0.148 724 | colord("#afafaf").delta("#b4b4b4"); // 0.014 725 | colord("#000000").delta("#ffffff"); // 1 726 | ``` 727 | 728 |
729 | 730 | ### Color utilities 731 | 732 |
733 | random() 734 | 735 | Returns a new Colord instance with a random color value inside. 736 | 737 | ```js 738 | import { random } from "colord"; 739 | 740 | random().toHex(); // "#01c8ec" 741 | random().alpha(0.5).toRgb(); // { r: 13, g: 237, b: 162, a: 0.5 } 742 | ``` 743 | 744 |
745 | 746 |
747 | .minify(options?) 748 | 749 | Converts a color to its shortest string representation. 750 | 751 | ```js 752 | import { colord, extend } from "colord"; 753 | import minifyPlugin from "colord/plugins/minify"; 754 | 755 | extend([minifyPlugin]); 756 | 757 | colord("black").minify(); // "#000" 758 | colord("#112233").minify(); // "#123" 759 | colord("darkgray").minify(); // "#a9a9a9" 760 | colord("rgba(170,170,170,0.4)").minify(); // "hsla(0,0%,67%,.4)" 761 | colord("rgba(170,170,170,0.4)").minify({ alphaHex: true }); // "#aaa6" 762 | ``` 763 | 764 | | Option | Default | Description | 765 | | ------------- | ------- | ------------------------------------------------------------ | 766 | | `hex` | `true` | Enable `#rrggbb` and `#rgb` notations | 767 | | `alphaHex` | `false` | Enable `#rrggbbaa` and `#rgba` notations | 768 | | `rgb` | `true` | Enable `rgb()` and `rgba()` functional notations | 769 | | `hsl` | `true` | Enable `hsl()` and `hsla()` functional notations | 770 | | `name` | `false` | Enable CSS color keywords. Requires `names` plugin installed | 771 | | `transparent` | `false` | Enable `"transparent"` color keyword | 772 | 773 |
774 | 775 |
---
776 | 777 | ## Plugins 778 | 779 | **Colord** has a built-in plugin system that allows new features and functionality to be easily added. 780 | 781 |
782 | a11y (Accessibility) 0.38 KB 783 | 784 | Adds accessibility and color contrast utilities working according to [Web Content Accessibility Guidelines 2.0](https://www.w3.org/TR/WCAG20/). 785 | 786 | ```js 787 | import { colord, extend } from "colord"; 788 | import a11yPlugin from "colord/plugins/a11y"; 789 | 790 | extend([a11yPlugin]); 791 | 792 | colord("#000000").luminance(); // 0 793 | colord("#ccddee").luminance(); // 0.71 794 | colord("#ffffff").luminance(); // 1 795 | 796 | colord("#000000").contrast(); // 21 (black on white) 797 | colord("#ffffff").contrast("#000000"); // 21 (white on black) 798 | colord("#0000ff").contrast("#ff000"); // 2.14 (blue on red) 799 | 800 | colord("#000000").isReadable(); // true (black on white) 801 | colord("#ffffff").isReadable("#000000"); // true (white on black) 802 | colord("#777777").isReadable(); // false (gray on white) 803 | colord("#e60000").isReadable("#ffff47"); // true (normal red text on yellow bg conforms to WCAG AA) 804 | colord("#e60000").isReadable("#ffff47", { level: "AAA" }); // false (normal red text on yellow bg does not conform to WCAG AAA) 805 | colord("#e60000").isReadable("#ffff47", { level: "AAA", size: "large" }); // true (large red text on yellow bg conforms to WCAG AAA) 806 | ``` 807 | 808 |
809 | 810 |
811 | cmyk (CMYK color space) 0.6 KB 812 | 813 | Adds support of [CMYK](https://www.sttmedia.com/colormodel-cmyk) color model. 814 | 815 | ```js 816 | import { colord, extend } from "colord"; 817 | import cmykPlugin from "colord/plugins/cmyk"; 818 | 819 | extend([cmykPlugin]); 820 | 821 | colord("#ffffff").toCmyk(); // { c: 0, m: 0, y: 0, k: 0, a: 1 } 822 | colord("#999966").toCmykString(); // "device-cmyk(0% 0% 33% 40%)" 823 | colord({ c: 0, m: 0, y: 0, k: 100, a: 1 }).toHex(); // "#000000" 824 | colord("device-cmyk(0% 61% 72% 0% / 50%)").toHex(); // "#ff634780" 825 | ``` 826 | 827 |
828 | 829 |
830 | harmonies (Color harmonies) 0.15 KB 831 | 832 | Provides functionality to generate [harmony colors](). 833 | 834 | ```js 835 | import { colord, extend } from "colord"; 836 | import harmonies from "colord/plugins/harmonies"; 837 | 838 | extend([harmonies]); 839 | 840 | const color = colord("#ff0000"); 841 | color.harmonies("analogous").map((c) => c.toHex()); // ["#ff0080", "#ff0000", "#ff8000"] 842 | color.harmonies("complementary").map((c) => c.toHex()); // ["#ff0000", "#00ffff"] 843 | color.harmonies("double-split-complementary").map((c) => c.toHex()); // ["#ff0080", "#ff0000", "#ff8000", "#00ff80", "#0080ff"] 844 | color.harmonies("rectangle").map((c) => c.toHex()); // ["#ff0000", "#ffff00", "#00ffff", "#0000ff"] 845 | color.harmonies("split-complementary").map((c) => c.toHex()); // ["#ff0000", "#00ff80", "#0080ff"] 846 | color.harmonies("tetradic").map((c) => c.toHex()); // ["#ff0000", "#80ff00", "#00ffff", "#8000ff"] 847 | color.harmonies("triadic").map((c) => c.toHex()); // ["#ff0000", "#00ff00", "#0000ff"] 848 | ``` 849 | 850 |
851 | 852 |
853 | hwb (HWB color model) 0.8 KB 854 | 855 | Adds support of [Hue-Whiteness-Blackness](https://en.wikipedia.org/wiki/HWB_color_model) color model. 856 | 857 | ```js 858 | import { colord, extend } from "colord"; 859 | import hwbPlugin from "colord/plugins/hwb"; 860 | 861 | extend([hwbPlugin]); 862 | 863 | colord("#999966").toHwb(); // { h: 60, w: 40, b: 40, a: 1 } 864 | colord("#003366").toHwbString(); // "hwb(210 0% 60%)" 865 | 866 | colord({ h: 60, w: 40, b: 40 }).toHex(); // "#999966" 867 | colord("hwb(210 0% 60% / 50%)").toHex(); // "#00336680" 868 | ``` 869 | 870 |
871 | 872 |
873 | lab (CIE LAB color space) 1.4 KB 874 | 875 | Adds support of [CIE LAB](https://en.wikipedia.org/wiki/CIELAB_color_space) color model. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). 876 | 877 | Also plugin provides `.delta` method for [perceived color difference calculations](https://en.wikipedia.org/wiki/Color_difference#CIEDE2000). 878 | 879 | ```js 880 | import { colord, extend } from "colord"; 881 | import labPlugin from "colord/plugins/lab"; 882 | 883 | extend([labPlugin]); 884 | 885 | colord({ l: 53.24, a: 80.09, b: 67.2 }).toHex(); // "#ff0000" 886 | colord("#ffffff").toLab(); // { l: 100, a: 0, b: 0, alpha: 1 } 887 | 888 | colord("#afafaf").delta("#b4b4b4"); // 0.014 889 | colord("#000000").delta("#ffffff"); // 1 890 | ``` 891 | 892 |
893 | 894 |
895 | lch (CIE LCH color space) 1.3 KB 896 | 897 | Adds support of [CIE LCH](https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/) color space. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). 898 | 899 | ```js 900 | import { colord, extend } from "colord"; 901 | import lchPlugin from "colord/plugins/lch"; 902 | 903 | extend([lchPlugin]); 904 | 905 | colord({ l: 100, c: 0, h: 0 }).toHex(); // "#ffffff" 906 | colord("lch(48.25% 30.07 196.38)").toHex(); // "#008080" 907 | 908 | colord("#646464").toLch(); // { l: 42.37, c: 0, h: 0, a: 1 } 909 | colord("#646464").alpha(0.5).toLchString(); // "lch(42.37% 0 0 / 0.5)" 910 | ``` 911 | 912 |
913 | 914 |
915 | minify (Color string minification) 0.5 KB 916 | 917 | A plugin adding color string minification utilities. 918 | 919 | ```js 920 | import { colord, extend } from "colord"; 921 | import minifyPlugin from "colord/plugins/minify"; 922 | 923 | extend([minifyPlugin]); 924 | 925 | colord("black").minify(); // "#000" 926 | colord("#112233").minify(); // "#123" 927 | colord("darkgray").minify(); // "#a9a9a9" 928 | colord("rgba(170,170,170,0.4)").minify(); // "hsla(0,0%,67%,.4)" 929 | colord("rgba(170,170,170,0.4)").minify({ alphaHex: true }); // "#aaa6" 930 | ``` 931 | 932 |
933 | 934 |
935 | mix (Color mixing) 0.96 KB 936 | 937 | A plugin adding color mixing utilities. 938 | 939 | In contrast to other libraries that perform RGB values mixing, Colord mixes colors through [LAB color space](https://en.wikipedia.org/wiki/CIELAB_color_space). This approach produces better results and doesn't have the drawbacks the legacy way has. 940 | 941 | → [Online demo](https://3cg7o.csb.app/) 942 | 943 | ```js 944 | import { colord, extend } from "colord"; 945 | import mixPlugin from "colord/plugins/mix"; 946 | 947 | extend([mixPlugin]); 948 | 949 | colord("#ffffff").mix("#000000").toHex(); // "#777777" 950 | colord("#800080").mix("#dda0dd").toHex(); // "#af5cae" 951 | colord("#cd853f").mix("#eee8aa", 0.6).toHex(); // "#e3c07e" 952 | colord("#008080").mix("#808000", 0.35).toHex(); // "#50805d" 953 | ``` 954 | 955 | Also, the plugin provides special mixtures such as [tints, shades, and tones](https://en.wikipedia.org/wiki/Tints_and_shades): 956 | 957 |
958 | tints, shades, and tones mixtures 959 |
960 | 961 | ```js 962 | const color = colord("#ff0000"); 963 | color.tints(3).map((c) => c.toHex()); // ["#ff0000", "#ff9f80", "#ffffff"]; 964 | color.shades(3).map((c) => c.toHex()); // ["#ff0000", "#7a1b0b", "#000000"]; 965 | color.tones(3).map((c) => c.toHex()); // ["#ff0000", "#c86147", "#808080"]; 966 | ``` 967 | 968 |
969 | 970 |
971 | names (CSS color keywords) 1.45 KB 972 | 973 | Provides options to convert a color into a [CSS color keyword](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#color_keywords) and vice versa. 974 | 975 | ```js 976 | import { colord, extend } from "colord"; 977 | import namesPlugin from "colord/plugins/names"; 978 | 979 | extend([namesPlugin]); 980 | 981 | colord("tomato").toHex(); // "#ff6347" 982 | colord("#00ffff").toName(); // "cyan" 983 | colord("rgba(0, 0, 0, 0)").toName(); // "transparent" 984 | colord("#fe0000").toName(); // undefined (the color is not specified in CSS specs) 985 | colord("#fe0000").toName({ closest: true }); // "red" (closest color) 986 | ``` 987 | 988 |
989 | 990 |
991 | xyz (CIE XYZ color space) 0.7 KB 992 | 993 | Adds support of [CIE XYZ](https://www.sttmedia.com/colormodel-xyz) color model. The conversion logic is ported from [CSS Color Module Level 4 Specification](https://www.w3.org/TR/css-color-4/#color-conversion-code). 994 | 995 | ```js 996 | import { colord, extend } from "colord"; 997 | import xyzPlugin from "colord/plugins/xyz"; 998 | 999 | extend([xyzPlugin]); 1000 | 1001 | colord("#ffffff").toXyz(); // { x: 95.047, y: 100, z: 108.883, a: 1 } 1002 | colord({ x: 0, y: 0, z: 0 }).toHex(); // "#000000" 1003 | ``` 1004 | 1005 |
1006 | 1007 |
---
1008 | 1009 | ## Types 1010 | 1011 | **Colord** is written in strict TypeScript and ships with types in the library itself — no need for any other install. We provide everything you need in one tiny package. 1012 | 1013 | While not only typing its own functions and variables, **Colord** can also help you type yours. Depending on the color space you are using, you can also import and use the type that is associated with it. 1014 | 1015 | ```ts 1016 | import { RgbColor, RgbaColor, HslColor, HslaColor, HsvColor, HsvaColor } from "colord"; 1017 | 1018 | const foo: HslColor = { h: 0, s: 0, l: 0 }; 1019 | const bar: RgbColor = { r: 0, g: 0, v: 0 }; // ERROR 1020 | ``` 1021 | 1022 |
---
1023 | 1024 | ## Projects using Colord 1025 | 1026 | - [cssnano](https://github.com/cssnano/cssnano) — the most popular CSS minification tool 1027 | - [Resume.io](https://resume.io/) — online resume builder with over 12,000,000 users worldwide 1028 | - [Leva](https://github.com/pmndrs/leva) — open source extensible GUI panel made for React 1029 | - [Qui Max](https://github.com/Qvant-lab/qui-max) — Vue.js design system and component library 1030 | - and [thousands more](https://github.com/omgovich/colord/network/dependents)... 1031 | 1032 |
---
1033 | 1034 | ## Roadmap 1035 | 1036 | - [x] Parse and convert Hex, RGB(A), HSL(A), HSV(A) 1037 | - [x] Saturate, desaturate, grayscale 1038 | - [x] Trim an input value 1039 | - [x] Clamp input numbers to resolve edge cases (e.g. `rgb(256, -1, 999, 2)`) 1040 | - [x] `brightness`, `isDark`, `isLight` 1041 | - [x] Set and get `alpha` 1042 | - [x] Plugin API 1043 | - [x] 4 and 8 digit Hex 1044 | - [x] `lighten`, `darken` 1045 | - [x] `invert` 1046 | - [x] CSS color names (via plugin) 1047 | - [x] A11y and contrast utils (via plugin) 1048 | - [x] XYZ color space (via plugin) 1049 | - [x] [HWB](https://drafts.csswg.org/css-color/#the-hwb-notation) color space (via plugin) 1050 | - [x] [LAB](https://www.w3.org/TR/css-color-4/#resolving-lab-lch-values) color space (via plugin) 1051 | - [x] [LCH](https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/) color space (via plugin) 1052 | - [x] Mix colors (via plugin) 1053 | - [x] CMYK color space (via plugin) 1054 | -------------------------------------------------------------------------------- /assets/divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omgovich/colord/3f859e03b0ca622eb15480f611371a0f15c9427f/assets/divider.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omgovich/colord/3f859e03b0ca622eb15480f611371a0f15c9427f/assets/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "colord", 3 | "version": "2.9.3", 4 | "description": "👑 A tiny yet powerful tool for high-performance color manipulations and conversions", 5 | "keywords": [ 6 | "color", 7 | "parser", 8 | "convert", 9 | "tiny", 10 | "hex", 11 | "rgb", 12 | "hsl", 13 | "hsv", 14 | "hwb", 15 | "lab", 16 | "lch", 17 | "xyz", 18 | "css", 19 | "color-names", 20 | "a11y", 21 | "cmyk", 22 | "mix", 23 | "minify", 24 | "harmonies" 25 | ], 26 | "repository": "omgovich/colord", 27 | "author": "Vlad Shilov ", 28 | "license": "MIT", 29 | "sideEffects": false, 30 | "main": "./index.js", 31 | "module": "./index.mjs", 32 | "exports": { 33 | ".": { 34 | "types": "./index.d.ts", 35 | "import": "./index.mjs", 36 | "require": "./index.js", 37 | "default": "./index.mjs" 38 | }, 39 | "./plugins/a11y": { 40 | "types": "./plugins/a11y.d.ts", 41 | "import": "./plugins/a11y.mjs", 42 | "require": "./plugins/a11y.js", 43 | "default": "./plugins/a11y.mjs" 44 | }, 45 | "./plugins/cmyk": { 46 | "types": "./plugins/cmyk.d.ts", 47 | "import": "./plugins/cmyk.mjs", 48 | "require": "./plugins/cmyk.js", 49 | "default": "./plugins/cmyk.mjs" 50 | }, 51 | "./plugins/harmonies": { 52 | "types": "./plugins/harmonies.d.ts", 53 | "import": "./plugins/harmonies.mjs", 54 | "require": "./plugins/harmonies.js", 55 | "default": "./plugins/harmonies.mjs" 56 | }, 57 | "./plugins/hwb": { 58 | "types": "./plugins/hwb.d.ts", 59 | "import": "./plugins/hwb.mjs", 60 | "require": "./plugins/hwb.js", 61 | "default": "./plugins/hwb.mjs" 62 | }, 63 | "./plugins/lab": { 64 | "types": "./plugins/lab.d.ts", 65 | "import": "./plugins/lab.mjs", 66 | "require": "./plugins/lab.js", 67 | "default": "./plugins/lab.mjs" 68 | }, 69 | "./plugins/lch": { 70 | "types": "./plugins/lch.d.ts", 71 | "import": "./plugins/lch.mjs", 72 | "require": "./plugins/lch.js", 73 | "default": "./plugins/lch.mjs" 74 | }, 75 | "./plugins/minify": { 76 | "types": "./plugins/minify.d.ts", 77 | "import": "./plugins/minify.mjs", 78 | "require": "./plugins/minify.js", 79 | "default": "./plugins/minify.mjs" 80 | }, 81 | "./plugins/mix": { 82 | "types": "./plugins/mix.d.ts", 83 | "import": "./plugins/mix.mjs", 84 | "require": "./plugins/mix.js", 85 | "default": "./plugins/mix.mjs" 86 | }, 87 | "./plugins/names": { 88 | "types": "./plugins/names.d.ts", 89 | "import": "./plugins/names.mjs", 90 | "require": "./plugins/names.js", 91 | "default": "./plugins/names.mjs" 92 | }, 93 | "./plugins/xyz": { 94 | "types": "./plugins/xyz.d.ts", 95 | "import": "./plugins/xyz.mjs", 96 | "require": "./plugins/xyz.js", 97 | "default": "./plugins/xyz.mjs" 98 | }, 99 | "./package.json": "./package.json" 100 | }, 101 | "files": [ 102 | "*.{js,mjs,ts,map}", 103 | "plugins/*.{js,mjs,ts,map}" 104 | ], 105 | "types": "index.d.ts", 106 | "scripts": { 107 | "lint": "eslint src/**/*.ts", 108 | "size": "npm run build && size-limit", 109 | "check-types": "tsc --noEmit true", 110 | "test": "jest tests --coverage", 111 | "benchmark": "tsc --outDir bench --skipLibCheck --esModuleInterop ./tests/benchmark.ts && node ./bench/tests/benchmark.js && rm -rf ./bench", 112 | "build": "rm -rf ./dist/* && rollup --config", 113 | "release": "npm run build && cp *.json dist && cp *.md dist && npm publish dist", 114 | "check-release": "npm run release -- --dry-run" 115 | }, 116 | "dependencies": {}, 117 | "devDependencies": { 118 | "@size-limit/preset-small-lib": "^4.10.1", 119 | "@types/jest": "^26.0.22", 120 | "@typescript-eslint/eslint-plugin": "^4.19.0", 121 | "@typescript-eslint/parser": "^4.19.0", 122 | "ac-colors": "^1.4.2", 123 | "benny": "^3.6.15", 124 | "chroma-js": "^2.1.1", 125 | "color": "^3.1.3", 126 | "eslint": "^7.14.0", 127 | "eslint-config-prettier": "^6.15.0", 128 | "eslint-plugin-prettier": "^3.1.4", 129 | "glob": "^7.1.6", 130 | "jest": "^26.6.3", 131 | "prettier": "^2.2.0", 132 | "rollup": "^2.43.1", 133 | "rollup-plugin-terser": "^7.0.2", 134 | "rollup-plugin-typescript2": "^0.30.0", 135 | "size-limit": "^4.10.1", 136 | "tinycolor2": "^1.4.2", 137 | "ts-jest": "^26.5.4", 138 | "ts-node": "^9.1.1", 139 | "tslib": "^2.1.0", 140 | "typescript": "^4.2.3" 141 | }, 142 | "jest": { 143 | "verbose": true, 144 | "transform": { 145 | "^.+\\.ts$": "ts-jest" 146 | } 147 | }, 148 | "eslintConfig": { 149 | "plugins": [ 150 | "prettier" 151 | ], 152 | "extends": [ 153 | "eslint:recommended", 154 | "plugin:@typescript-eslint/eslint-recommended", 155 | "plugin:@typescript-eslint/recommended", 156 | "plugin:prettier/recommended", 157 | "prettier/@typescript-eslint" 158 | ] 159 | }, 160 | "prettier": { 161 | "printWidth": 100 162 | }, 163 | "size-limit": [ 164 | { 165 | "path": "dist/index.mjs", 166 | "import": "{ colord }", 167 | "limit": "2 KB" 168 | }, 169 | { 170 | "path": "dist/plugins/a11y.mjs", 171 | "limit": "0.5 KB" 172 | }, 173 | { 174 | "path": "dist/plugins/cmyk.mjs", 175 | "limit": "1 KB" 176 | }, 177 | { 178 | "path": "dist/plugins/harmonies.mjs", 179 | "limit": "0.5 KB" 180 | }, 181 | { 182 | "path": "dist/plugins/hwb.mjs", 183 | "limit": "1 KB" 184 | }, 185 | { 186 | "path": "dist/plugins/lab.mjs", 187 | "limit": "1.5 KB" 188 | }, 189 | { 190 | "path": "dist/plugins/lch.mjs", 191 | "limit": "1.5 KB" 192 | }, 193 | { 194 | "path": "dist/plugins/minify.mjs", 195 | "limit": "0.6 KB" 196 | }, 197 | { 198 | "path": "dist/plugins/mix.mjs", 199 | "limit": "1 KB" 200 | }, 201 | { 202 | "path": "dist/plugins/names.mjs", 203 | "limit": "1.5 KB" 204 | }, 205 | { 206 | "path": "dist/plugins/xyz.mjs", 207 | "limit": "1 KB" 208 | } 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import glob from "glob"; 3 | import typescript from "rollup-plugin-typescript2"; 4 | import { terser } from "rollup-plugin-terser"; 5 | 6 | const getRollupPluginsConfig = (compilerOptions) => { 7 | return [ 8 | typescript({ 9 | tsconfigOverride: { compilerOptions }, 10 | }), 11 | terser({ 12 | ecma: 5, 13 | module: true, 14 | toplevel: true, 15 | compress: { pure_getters: true }, 16 | format: { wrap_func_args: false }, 17 | }), 18 | ]; 19 | }; 20 | 21 | // Find available plugins 22 | const colordPluginPaths = glob.sync("./src/plugins/*.ts"); 23 | 24 | // Bundle both formats according to NodeJS guide 25 | // https://nodejs.org/api/packages.html#packages_approach_2_isolate_state 26 | export default [ 27 | // Build the main bundle in both ESM and CJS modules 28 | { 29 | input: "src/index.ts", 30 | output: { 31 | file: "dist/index.mjs", 32 | format: "es", 33 | }, 34 | plugins: getRollupPluginsConfig({ declaration: true }), 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: { 39 | file: "dist/index.js", 40 | format: "cjs", 41 | }, 42 | plugins: getRollupPluginsConfig({ declaration: false }), 43 | }, 44 | 45 | // Bundle all library plugins as ESM modules 46 | ...colordPluginPaths.map((input) => ({ 47 | input, 48 | output: { 49 | file: `dist/plugins/${path.parse(input).name}.mjs`, 50 | format: "es", 51 | }, 52 | plugins: getRollupPluginsConfig({ declaration: false }), 53 | })), 54 | 55 | // Bundle all library plugins as CommonJS modules 56 | ...colordPluginPaths.map((input) => ({ 57 | input, 58 | output: { 59 | file: `dist/plugins/${path.parse(input).name}.js`, 60 | format: "cjs", 61 | exports: "default", 62 | }, 63 | plugins: getRollupPluginsConfig({ declaration: false }), 64 | })), 65 | ]; 66 | -------------------------------------------------------------------------------- /src/colorModels/cmyk.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor, InputObject, CmykaColor } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, isPresent, round } from "../helpers"; 4 | 5 | /** 6 | * Clamps the CMYK color object values. 7 | */ 8 | export const clampCmyka = (cmyka: CmykaColor): CmykaColor => ({ 9 | c: clamp(cmyka.c, 0, 100), 10 | m: clamp(cmyka.m, 0, 100), 11 | y: clamp(cmyka.y, 0, 100), 12 | k: clamp(cmyka.k, 0, 100), 13 | a: clamp(cmyka.a), 14 | }); 15 | 16 | /** 17 | * Rounds the CMYK color object values. 18 | */ 19 | export const roundCmyka = (cmyka: CmykaColor): CmykaColor => ({ 20 | c: round(cmyka.c, 2), 21 | m: round(cmyka.m, 2), 22 | y: round(cmyka.y, 2), 23 | k: round(cmyka.k, 2), 24 | a: round(cmyka.a, ALPHA_PRECISION), 25 | }); 26 | 27 | /** 28 | * Transforms the CMYK color object to RGB. 29 | * https://www.rapidtables.com/convert/color/cmyk-to-rgb.html 30 | */ 31 | export function cmykaToRgba(cmyka: CmykaColor): RgbaColor { 32 | return { 33 | r: round(255 * (1 - cmyka.c / 100) * (1 - cmyka.k / 100)), 34 | g: round(255 * (1 - cmyka.m / 100) * (1 - cmyka.k / 100)), 35 | b: round(255 * (1 - cmyka.y / 100) * (1 - cmyka.k / 100)), 36 | a: cmyka.a, 37 | }; 38 | } 39 | 40 | /** 41 | * Convert RGB Color Model object to CMYK. 42 | * https://www.rapidtables.com/convert/color/rgb-to-cmyk.html 43 | */ 44 | export function rgbaToCmyka(rgba: RgbaColor): CmykaColor { 45 | const k = 1 - Math.max(rgba.r / 255, rgba.g / 255, rgba.b / 255); 46 | const c = (1 - rgba.r / 255 - k) / (1 - k); 47 | const m = (1 - rgba.g / 255 - k) / (1 - k); 48 | const y = (1 - rgba.b / 255 - k) / (1 - k); 49 | 50 | return { 51 | c: isNaN(c) ? 0 : round(c * 100), 52 | m: isNaN(m) ? 0 : round(m * 100), 53 | y: isNaN(y) ? 0 : round(y * 100), 54 | k: round(k * 100), 55 | a: rgba.a, 56 | }; 57 | } 58 | 59 | /** 60 | * Parses the CMYK color object into RGB. 61 | */ 62 | export function parseCmyka({ c, m, y, k, a = 1 }: InputObject): RgbaColor | null { 63 | if (!isPresent(c) || !isPresent(m) || !isPresent(y) || !isPresent(k)) return null; 64 | 65 | const cmyk = clampCmyka({ 66 | c: Number(c), 67 | m: Number(m), 68 | y: Number(y), 69 | k: Number(k), 70 | a: Number(a), 71 | }); 72 | 73 | return cmykaToRgba(cmyk); 74 | } 75 | -------------------------------------------------------------------------------- /src/colorModels/cmykString.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | import { clampCmyka, cmykaToRgba, rgbaToCmyka, roundCmyka } from "./cmyk"; 3 | 4 | const cmykMatcher = /^device-cmyk\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 5 | 6 | /** 7 | * Parses a valid CMYK CSS color function/string 8 | * https://www.w3.org/TR/css-color-4/#device-cmyk 9 | */ 10 | export const parseCmykaString = (input: string): RgbaColor | null => { 11 | const match = cmykMatcher.exec(input); 12 | 13 | if (!match) return null; 14 | 15 | const cmyka = clampCmyka({ 16 | c: Number(match[1]) * (match[2] ? 1 : 100), 17 | m: Number(match[3]) * (match[4] ? 1 : 100), 18 | y: Number(match[5]) * (match[6] ? 1 : 100), 19 | k: Number(match[7]) * (match[8] ? 1 : 100), 20 | a: match[9] === undefined ? 1 : Number(match[9]) / (match[10] ? 100 : 1), 21 | }); 22 | 23 | return cmykaToRgba(cmyka); 24 | }; 25 | 26 | export function rgbaToCmykaString(rgb: RgbaColor): string { 27 | const { c, m, y, k, a } = roundCmyka(rgbaToCmyka(rgb)); 28 | 29 | return a < 1 30 | ? `device-cmyk(${c}% ${m}% ${y}% ${k}% / ${a})` 31 | : `device-cmyk(${c}% ${m}% ${y}% ${k}%)`; 32 | } 33 | -------------------------------------------------------------------------------- /src/colorModels/hex.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | import { round } from "../helpers"; 3 | import { roundRgba } from "./rgb"; 4 | 5 | const hexMatcher = /^#([0-9a-f]{3,8})$/i; 6 | 7 | /** Parses any valid Hex3, Hex4, Hex6 or Hex8 string and converts it to an RGBA object */ 8 | export const parseHex = (hex: string): RgbaColor | null => { 9 | const hexMatch = hexMatcher.exec(hex); 10 | 11 | if (!hexMatch) return null; 12 | 13 | hex = hexMatch[1]; 14 | 15 | if (hex.length <= 4) { 16 | return { 17 | r: parseInt(hex[0] + hex[0], 16), 18 | g: parseInt(hex[1] + hex[1], 16), 19 | b: parseInt(hex[2] + hex[2], 16), 20 | a: hex.length === 4 ? round(parseInt(hex[3] + hex[3], 16) / 255, 2) : 1, 21 | }; 22 | } 23 | 24 | if (hex.length === 6 || hex.length === 8) { 25 | return { 26 | r: parseInt(hex.substr(0, 2), 16), 27 | g: parseInt(hex.substr(2, 2), 16), 28 | b: parseInt(hex.substr(4, 2), 16), 29 | a: hex.length === 8 ? round(parseInt(hex.substr(6, 2), 16) / 255, 2) : 1, 30 | }; 31 | } 32 | 33 | return null; 34 | }; 35 | 36 | /** Formats any decimal number (e.g. 128) as a hexadecimal string (e.g. "08") */ 37 | const format = (number: number): string => { 38 | const hex = number.toString(16); 39 | return hex.length < 2 ? "0" + hex : hex; 40 | }; 41 | 42 | /** Converts RGBA object to Hex6 or (if it has alpha channel) Hex8 string */ 43 | export const rgbaToHex = (rgba: RgbaColor): string => { 44 | const { r, g, b, a } = roundRgba(rgba); 45 | const alphaHex = a < 1 ? format(round(a * 255)) : ""; 46 | return "#" + format(r) + format(g) + format(b) + alphaHex; 47 | }; 48 | -------------------------------------------------------------------------------- /src/colorModels/hsl.ts: -------------------------------------------------------------------------------- 1 | import { InputObject, RgbaColor, HslaColor, HsvaColor } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, clampHue, round, isPresent } from "../helpers"; 4 | import { hsvaToRgba, rgbaToHsva } from "./hsv"; 5 | 6 | export const clampHsla = (hsla: HslaColor): HslaColor => ({ 7 | h: clampHue(hsla.h), 8 | s: clamp(hsla.s, 0, 100), 9 | l: clamp(hsla.l, 0, 100), 10 | a: clamp(hsla.a), 11 | }); 12 | 13 | export const roundHsla = (hsla: HslaColor): HslaColor => ({ 14 | h: round(hsla.h), 15 | s: round(hsla.s), 16 | l: round(hsla.l), 17 | a: round(hsla.a, ALPHA_PRECISION), 18 | }); 19 | 20 | export const parseHsla = ({ h, s, l, a = 1 }: InputObject): RgbaColor | null => { 21 | if (!isPresent(h) || !isPresent(s) || !isPresent(l)) return null; 22 | 23 | const hsla = clampHsla({ 24 | h: Number(h), 25 | s: Number(s), 26 | l: Number(l), 27 | a: Number(a), 28 | }); 29 | 30 | return hslaToRgba(hsla); 31 | }; 32 | 33 | export const hslaToHsva = ({ h, s, l, a }: HslaColor): HsvaColor => { 34 | s *= (l < 50 ? l : 100 - l) / 100; 35 | 36 | return { 37 | h: h, 38 | s: s > 0 ? ((2 * s) / (l + s)) * 100 : 0, 39 | v: l + s, 40 | a, 41 | }; 42 | }; 43 | 44 | export const hsvaToHsla = ({ h, s, v, a }: HsvaColor): HslaColor => { 45 | const hh = ((200 - s) * v) / 100; 46 | 47 | return { 48 | h, 49 | s: hh > 0 && hh < 200 ? ((s * v) / 100 / (hh <= 100 ? hh : 200 - hh)) * 100 : 0, 50 | l: hh / 2, 51 | a, 52 | }; 53 | }; 54 | 55 | export const hslaToRgba = (hsla: HslaColor): RgbaColor => { 56 | return hsvaToRgba(hslaToHsva(hsla)); 57 | }; 58 | 59 | export const rgbaToHsla = (rgba: RgbaColor): HslaColor => { 60 | return hsvaToHsla(rgbaToHsva(rgba)); 61 | }; 62 | -------------------------------------------------------------------------------- /src/colorModels/hslString.ts: -------------------------------------------------------------------------------- 1 | import { parseHue } from "../helpers"; 2 | import { RgbaColor } from "../types"; 3 | import { clampHsla, rgbaToHsla, hslaToRgba, roundHsla } from "./hsl"; 4 | 5 | // Functional syntax 6 | // hsl( , , , ? ) 7 | const commaHslaMatcher = /^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*,\s*([+-]?\d*\.?\d+)%\s*,\s*([+-]?\d*\.?\d+)%\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 8 | 9 | // Whitespace syntax 10 | // hsl( [ / ]? ) 11 | const spaceHslaMatcher = /^hsla?\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 12 | 13 | /** 14 | * Parses a valid HSL[A] CSS color function/string 15 | * https://www.w3.org/TR/css-color-4/#the-hsl-notation 16 | */ 17 | export const parseHslaString = (input: string): RgbaColor | null => { 18 | const match = commaHslaMatcher.exec(input) || spaceHslaMatcher.exec(input); 19 | 20 | if (!match) return null; 21 | 22 | const hsla = clampHsla({ 23 | h: parseHue(match[1], match[2]), 24 | s: Number(match[3]), 25 | l: Number(match[4]), 26 | a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1), 27 | }); 28 | 29 | return hslaToRgba(hsla); 30 | }; 31 | 32 | export const rgbaToHslaString = (rgba: RgbaColor): string => { 33 | const { h, s, l, a } = roundHsla(rgbaToHsla(rgba)); 34 | return a < 1 ? `hsla(${h}, ${s}%, ${l}%, ${a})` : `hsl(${h}, ${s}%, ${l}%)`; 35 | }; 36 | -------------------------------------------------------------------------------- /src/colorModels/hsv.ts: -------------------------------------------------------------------------------- 1 | import { InputObject, RgbaColor, HsvaColor } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, clampHue, isPresent, round } from "../helpers"; 4 | 5 | export const clampHsva = (hsva: HsvaColor): HsvaColor => ({ 6 | h: clampHue(hsva.h), 7 | s: clamp(hsva.s, 0, 100), 8 | v: clamp(hsva.v, 0, 100), 9 | a: clamp(hsva.a), 10 | }); 11 | 12 | export const roundHsva = (hsva: HsvaColor): HsvaColor => ({ 13 | h: round(hsva.h), 14 | s: round(hsva.s), 15 | v: round(hsva.v), 16 | a: round(hsva.a, ALPHA_PRECISION), 17 | }); 18 | 19 | export const parseHsva = ({ h, s, v, a = 1 }: InputObject): RgbaColor | null => { 20 | if (!isPresent(h) || !isPresent(s) || !isPresent(v)) return null; 21 | 22 | const hsva = clampHsva({ 23 | h: Number(h), 24 | s: Number(s), 25 | v: Number(v), 26 | a: Number(a), 27 | }); 28 | 29 | return hsvaToRgba(hsva); 30 | }; 31 | 32 | export const rgbaToHsva = ({ r, g, b, a }: RgbaColor): HsvaColor => { 33 | const max = Math.max(r, g, b); 34 | const delta = max - Math.min(r, g, b); 35 | 36 | const hh = delta 37 | ? max === r 38 | ? (g - b) / delta 39 | : max === g 40 | ? 2 + (b - r) / delta 41 | : 4 + (r - g) / delta 42 | : 0; 43 | 44 | return { 45 | h: 60 * (hh < 0 ? hh + 6 : hh), 46 | s: max ? (delta / max) * 100 : 0, 47 | v: (max / 255) * 100, 48 | a, 49 | }; 50 | }; 51 | 52 | export const hsvaToRgba = ({ h, s, v, a }: HsvaColor): RgbaColor => { 53 | h = (h / 360) * 6; 54 | s = s / 100; 55 | v = v / 100; 56 | 57 | const hh = Math.floor(h), 58 | b = v * (1 - s), 59 | c = v * (1 - (h - hh) * s), 60 | d = v * (1 - (1 - h + hh) * s), 61 | module = hh % 6; 62 | 63 | return { 64 | r: [v, c, b, b, d, v][module] * 255, 65 | g: [d, v, v, c, b, b][module] * 255, 66 | b: [b, b, d, v, v, c][module] * 255, 67 | a: a, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /src/colorModels/hwb.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor, HwbaColor, InputObject } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, clampHue, round, isPresent } from "../helpers"; 4 | import { hsvaToRgba, rgbaToHsva } from "./hsv"; 5 | 6 | export const clampHwba = (hwba: HwbaColor): HwbaColor => ({ 7 | h: clampHue(hwba.h), 8 | w: clamp(hwba.w, 0, 100), 9 | b: clamp(hwba.b, 0, 100), 10 | a: clamp(hwba.a), 11 | }); 12 | 13 | export const roundHwba = (hwba: HwbaColor): HwbaColor => ({ 14 | h: round(hwba.h), 15 | w: round(hwba.w), 16 | b: round(hwba.b), 17 | a: round(hwba.a, ALPHA_PRECISION), 18 | }); 19 | 20 | export const rgbaToHwba = (rgba: RgbaColor): HwbaColor => { 21 | const { h } = rgbaToHsva(rgba); 22 | const w = (Math.min(rgba.r, rgba.g, rgba.b) / 255) * 100; 23 | const b = 100 - (Math.max(rgba.r, rgba.g, rgba.b) / 255) * 100; 24 | return { h, w, b, a: rgba.a }; 25 | }; 26 | 27 | export const hwbaToRgba = (hwba: HwbaColor): RgbaColor => { 28 | return hsvaToRgba({ 29 | h: hwba.h, 30 | s: hwba.b === 100 ? 0 : 100 - (hwba.w / (100 - hwba.b)) * 100, 31 | v: 100 - hwba.b, 32 | a: hwba.a, 33 | }); 34 | }; 35 | 36 | export const parseHwba = ({ h, w, b, a = 1 }: InputObject): RgbaColor | null => { 37 | if (!isPresent(h) || !isPresent(w) || !isPresent(b)) return null; 38 | 39 | const hwba = clampHwba({ 40 | h: Number(h), 41 | w: Number(w), 42 | b: Number(b), 43 | a: Number(a), 44 | }); 45 | 46 | return hwbaToRgba(hwba); 47 | }; 48 | -------------------------------------------------------------------------------- /src/colorModels/hwbString.ts: -------------------------------------------------------------------------------- 1 | import { parseHue } from "../helpers"; 2 | import { RgbaColor } from "../types"; 3 | import { clampHwba, rgbaToHwba, hwbaToRgba, roundHwba } from "./hwb"; 4 | 5 | // The only valid HWB syntax 6 | // hwb( [ / ]? ) 7 | const hwbaMatcher = /^hwb\(\s*([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s+([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)%\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 8 | 9 | /** 10 | * Parses a valid HWB[A] CSS color function/string 11 | * https://www.w3.org/TR/css-color-4/#the-hwb-notation 12 | */ 13 | export const parseHwbaString = (input: string): RgbaColor | null => { 14 | const match = hwbaMatcher.exec(input); 15 | 16 | if (!match) return null; 17 | 18 | const hwba = clampHwba({ 19 | h: parseHue(match[1], match[2]), 20 | w: Number(match[3]), 21 | b: Number(match[4]), 22 | a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1), 23 | }); 24 | 25 | return hwbaToRgba(hwba); 26 | }; 27 | 28 | export const rgbaToHwbaString = (rgba: RgbaColor): string => { 29 | const { h, w, b, a } = roundHwba(rgbaToHwba(rgba)); 30 | return a < 1 ? `hwb(${h} ${w}% ${b}% / ${a})` : `hwb(${h} ${w}% ${b}%)`; 31 | }; 32 | -------------------------------------------------------------------------------- /src/colorModels/lab.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor, LabaColor, InputObject } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, isPresent, round } from "../helpers"; 4 | import { D50, rgbaToXyza, xyzaToRgba } from "./xyz"; 5 | 6 | // Conversion factors from https://en.wikipedia.org/wiki/CIELAB_color_space 7 | const e = 216 / 24389; 8 | const k = 24389 / 27; 9 | 10 | /** 11 | * Clamps LAB axis values as defined in CSS Color Level 4 specs. 12 | * https://www.w3.org/TR/css-color-4/#specifying-lab-lch 13 | */ 14 | export const clampLaba = (laba: LabaColor): LabaColor => ({ 15 | // CIE Lightness values less than 0% must be clamped to 0%. 16 | // Values greater than 100% are permitted for forwards compatibility with HDR. 17 | l: clamp(laba.l, 0, 400), 18 | // A and B axis values are signed (allow both positive and negative values) 19 | // and theoretically unbounded (but in practice do not exceed ±160). 20 | a: laba.a, 21 | b: laba.b, 22 | alpha: clamp(laba.alpha), 23 | }); 24 | 25 | export const roundLaba = (laba: LabaColor): LabaColor => ({ 26 | l: round(laba.l, 2), 27 | a: round(laba.a, 2), 28 | b: round(laba.b, 2), 29 | alpha: round(laba.alpha, ALPHA_PRECISION), 30 | }); 31 | 32 | export const parseLaba = ({ l, a, b, alpha = 1 }: InputObject): RgbaColor | null => { 33 | if (!isPresent(l) || !isPresent(a) || !isPresent(b)) return null; 34 | 35 | const laba = clampLaba({ 36 | l: Number(l), 37 | a: Number(a), 38 | b: Number(b), 39 | alpha: Number(alpha), 40 | }); 41 | 42 | return labaToRgba(laba); 43 | }; 44 | 45 | /** 46 | * Performs RGB → CIEXYZ → LAB color conversion 47 | * https://www.w3.org/TR/css-color-4/#color-conversion-code 48 | */ 49 | export const rgbaToLaba = (rgba: RgbaColor): LabaColor => { 50 | // Compute XYZ scaled relative to D50 reference white 51 | const xyza = rgbaToXyza(rgba); 52 | let x = xyza.x / D50.x; 53 | let y = xyza.y / D50.y; 54 | let z = xyza.z / D50.z; 55 | 56 | x = x > e ? Math.cbrt(x) : (k * x + 16) / 116; 57 | y = y > e ? Math.cbrt(y) : (k * y + 16) / 116; 58 | z = z > e ? Math.cbrt(z) : (k * z + 16) / 116; 59 | 60 | return { 61 | l: 116 * y - 16, 62 | a: 500 * (x - y), 63 | b: 200 * (y - z), 64 | alpha: xyza.a, 65 | }; 66 | }; 67 | 68 | /** 69 | * Performs LAB → CIEXYZ → RGB color conversion 70 | * https://www.w3.org/TR/css-color-4/#color-conversion-code 71 | */ 72 | export const labaToRgba = (laba: LabaColor): RgbaColor => { 73 | const y = (laba.l + 16) / 116; 74 | const x = laba.a / 500 + y; 75 | const z = y - laba.b / 200; 76 | 77 | return xyzaToRgba({ 78 | x: (Math.pow(x, 3) > e ? Math.pow(x, 3) : (116 * x - 16) / k) * D50.x, 79 | y: (laba.l > k * e ? Math.pow((laba.l + 16) / 116, 3) : laba.l / k) * D50.y, 80 | z: (Math.pow(z, 3) > e ? Math.pow(z, 3) : (116 * z - 16) / k) * D50.z, 81 | a: laba.alpha, 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /src/colorModels/lch.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor, InputObject, LchaColor } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, clampHue, isPresent, round } from "../helpers"; 4 | import { labaToRgba, rgbaToLaba } from "./lab"; 5 | 6 | /** 7 | * Limits LCH axis values. 8 | * https://www.w3.org/TR/css-color-4/#specifying-lab-lch 9 | * https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/#how-does-lch-work 10 | */ 11 | export const clampLcha = (laba: LchaColor): LchaColor => ({ 12 | l: clamp(laba.l, 0, 100), 13 | c: laba.c, // chroma is theoretically unbounded in LCH 14 | h: clampHue(laba.h), 15 | a: laba.a, 16 | }); 17 | 18 | export const roundLcha = (laba: LchaColor): LchaColor => ({ 19 | l: round(laba.l, 2), 20 | c: round(laba.c, 2), 21 | h: round(laba.h, 2), 22 | a: round(laba.a, ALPHA_PRECISION), 23 | }); 24 | 25 | export const parseLcha = ({ l, c, h, a = 1 }: InputObject): RgbaColor | null => { 26 | if (!isPresent(l) || !isPresent(c) || !isPresent(h)) return null; 27 | 28 | const lcha = clampLcha({ 29 | l: Number(l), 30 | c: Number(c), 31 | h: Number(h), 32 | a: Number(a), 33 | }); 34 | 35 | return lchaToRgba(lcha); 36 | }; 37 | 38 | /** 39 | * Performs RGB → CIEXYZ → CIELAB → CIELCH color conversion 40 | * https://www.w3.org/TR/css-color-4/#color-conversion-code 41 | */ 42 | export const rgbaToLcha = (rgba: RgbaColor): LchaColor => { 43 | const laba = rgbaToLaba(rgba); 44 | 45 | // Round axis values to get proper values for grayscale colors 46 | const a = round(laba.a, 3); 47 | const b = round(laba.b, 3); 48 | 49 | const hue = 180 * (Math.atan2(b, a) / Math.PI); 50 | 51 | return { 52 | l: laba.l, 53 | c: Math.sqrt(a * a + b * b), 54 | h: hue < 0 ? hue + 360 : hue, 55 | a: laba.alpha, 56 | }; 57 | }; 58 | 59 | /** 60 | * Performs CIELCH → CIELAB → CIEXYZ → RGB color conversion 61 | * https://www.w3.org/TR/css-color-4/#color-conversion-code 62 | */ 63 | export const lchaToRgba = (lcha: LchaColor): RgbaColor => { 64 | return labaToRgba({ 65 | l: lcha.l, 66 | a: lcha.c * Math.cos((lcha.h * Math.PI) / 180), 67 | b: lcha.c * Math.sin((lcha.h * Math.PI) / 180), 68 | alpha: lcha.a, 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /src/colorModels/lchString.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | import { parseHue } from "../helpers"; 3 | import { clampLcha, rgbaToLcha, lchaToRgba, roundLcha } from "./lch"; 4 | 5 | // The only valid LCH syntax 6 | // lch() = lch( [ / ]? ) 7 | const lchaMatcher = /^lch\(\s*([+-]?\d*\.?\d+)%\s+([+-]?\d*\.?\d+)\s+([+-]?\d*\.?\d+)(deg|rad|grad|turn)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 8 | 9 | /** 10 | * Parses a valid LCH CSS color function/string 11 | * https://www.w3.org/TR/css-color-4/#specifying-lab-lch 12 | */ 13 | export const parseLchaString = (input: string): RgbaColor | null => { 14 | const match = lchaMatcher.exec(input); 15 | 16 | if (!match) return null; 17 | 18 | const lcha = clampLcha({ 19 | l: Number(match[1]), 20 | c: Number(match[2]), 21 | h: parseHue(match[3], match[4]), 22 | a: match[5] === undefined ? 1 : Number(match[5]) / (match[6] ? 100 : 1), 23 | }); 24 | 25 | return lchaToRgba(lcha); 26 | }; 27 | 28 | export const rgbaToLchaString = (rgba: RgbaColor): string => { 29 | const { l, c, h, a } = roundLcha(rgbaToLcha(rgba)); 30 | return a < 1 ? `lch(${l}% ${c} ${h} / ${a})` : `lch(${l}% ${c} ${h})`; 31 | }; 32 | -------------------------------------------------------------------------------- /src/colorModels/rgb.ts: -------------------------------------------------------------------------------- 1 | import { InputObject, RgbaColor } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { round, clamp, isPresent } from "../helpers"; 4 | 5 | export const clampRgba = (rgba: RgbaColor): RgbaColor => ({ 6 | r: clamp(rgba.r, 0, 255), 7 | g: clamp(rgba.g, 0, 255), 8 | b: clamp(rgba.b, 0, 255), 9 | a: clamp(rgba.a), 10 | }); 11 | 12 | export const roundRgba = (rgba: RgbaColor): RgbaColor => ({ 13 | r: round(rgba.r), 14 | g: round(rgba.g), 15 | b: round(rgba.b), 16 | a: round(rgba.a, ALPHA_PRECISION), 17 | }); 18 | 19 | export const parseRgba = ({ r, g, b, a = 1 }: InputObject): RgbaColor | null => { 20 | if (!isPresent(r) || !isPresent(g) || !isPresent(b)) return null; 21 | 22 | return clampRgba({ 23 | r: Number(r), 24 | g: Number(g), 25 | b: Number(b), 26 | a: Number(a), 27 | }); 28 | }; 29 | 30 | /** 31 | * Converts an RGB channel [0-255] to its linear light (un-companded) form [0-1]. 32 | * Linearized RGB values are widely used for color space conversions and contrast calculations 33 | */ 34 | export const linearizeRgbChannel = (value: number): number => { 35 | const ratio = value / 255; 36 | return ratio < 0.04045 ? ratio / 12.92 : Math.pow((ratio + 0.055) / 1.055, 2.4); 37 | }; 38 | 39 | /** 40 | * Converts an linear-light sRGB channel [0-1] back to its gamma corrected form [0-255] 41 | */ 42 | export const unlinearizeRgbChannel = (ratio: number): number => { 43 | const value = ratio > 0.0031308 ? 1.055 * Math.pow(ratio, 1 / 2.4) - 0.055 : 12.92 * ratio; 44 | return value * 255; 45 | }; 46 | -------------------------------------------------------------------------------- /src/colorModels/rgbString.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | import { roundRgba, clampRgba } from "./rgb"; 3 | 4 | // Functional syntax 5 | // rgb( #{3} , ? ) 6 | // rgb( #{3} , ? ) 7 | const commaRgbaMatcher = /^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*,\s*([+-]?\d*\.?\d+)(%)?\s*(?:,\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 8 | 9 | // Whitespace syntax 10 | // rgb( {3} [ / ]? ) 11 | // rgb( {3} [ / ]? ) 12 | const spaceRgbaMatcher = /^rgba?\(\s*([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s+([+-]?\d*\.?\d+)(%)?\s*(?:\/\s*([+-]?\d*\.?\d+)(%)?\s*)?\)$/i; 13 | 14 | /** 15 | * Parses a valid RGB[A] CSS color function/string 16 | * https://www.w3.org/TR/css-color-4/#rgb-functions 17 | */ 18 | export const parseRgbaString = (input: string): RgbaColor | null => { 19 | const match = commaRgbaMatcher.exec(input) || spaceRgbaMatcher.exec(input); 20 | 21 | if (!match) return null; 22 | 23 | // Mixing numbers and percentages is not allowed 24 | // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb_syntax_variations 25 | if (match[2] !== match[4] || match[4] !== match[6]) return null; 26 | 27 | return clampRgba({ 28 | r: Number(match[1]) / (match[2] ? 100 / 255 : 1), 29 | g: Number(match[3]) / (match[4] ? 100 / 255 : 1), 30 | b: Number(match[5]) / (match[6] ? 100 / 255 : 1), 31 | a: match[7] === undefined ? 1 : Number(match[7]) / (match[8] ? 100 : 1), 32 | }); 33 | }; 34 | 35 | export const rgbaToRgbaString = (rgba: RgbaColor): string => { 36 | const { r, g, b, a } = roundRgba(rgba); 37 | return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; 38 | }; 39 | -------------------------------------------------------------------------------- /src/colorModels/xyz.ts: -------------------------------------------------------------------------------- 1 | import { InputObject, RgbaColor, XyzColor, XyzaColor } from "../types"; 2 | import { ALPHA_PRECISION } from "../constants"; 3 | import { clamp, isPresent, round } from "../helpers"; 4 | import { clampRgba, linearizeRgbChannel, unlinearizeRgbChannel } from "./rgb"; 5 | 6 | // Theoretical light source that approximates "warm daylight" and follows the CIE standard. 7 | // https://en.wikipedia.org/wiki/Standard_illuminant 8 | export const D50 = { 9 | x: 96.422, 10 | y: 100, 11 | z: 82.521, 12 | }; 13 | 14 | /** 15 | * Limits XYZ axis values assuming XYZ is relative to D50. 16 | */ 17 | export const clampXyza = (xyza: XyzaColor): XyzaColor => ({ 18 | x: clamp(xyza.x, 0, D50.x), 19 | y: clamp(xyza.y, 0, D50.y), 20 | z: clamp(xyza.z, 0, D50.z), 21 | a: clamp(xyza.a), 22 | }); 23 | 24 | export const roundXyza = (xyza: XyzaColor): XyzaColor => ({ 25 | x: round(xyza.x, 2), 26 | y: round(xyza.y, 2), 27 | z: round(xyza.z, 2), 28 | a: round(xyza.a, ALPHA_PRECISION), 29 | }); 30 | 31 | export const parseXyza = ({ x, y, z, a = 1 }: InputObject): RgbaColor | null => { 32 | if (!isPresent(x) || !isPresent(y) || !isPresent(z)) return null; 33 | 34 | const xyza = clampXyza({ 35 | x: Number(x), 36 | y: Number(y), 37 | z: Number(z), 38 | a: Number(a), 39 | }); 40 | 41 | return xyzaToRgba(xyza); 42 | }; 43 | 44 | /** 45 | * Performs Bradford chromatic adaptation from D65 to D50 46 | */ 47 | export const adaptXyzaToD50 = (xyza: XyzaColor): XyzaColor => ({ 48 | x: xyza.x * 1.0478112 + xyza.y * 0.0228866 + xyza.z * -0.050127, 49 | y: xyza.x * 0.0295424 + xyza.y * 0.9904844 + xyza.z * -0.0170491, 50 | z: xyza.x * -0.0092345 + xyza.y * 0.0150436 + xyza.z * 0.7521316, 51 | a: xyza.a, 52 | }); 53 | 54 | /** 55 | * Performs Bradford chromatic adaptation from D50 to D65 56 | */ 57 | export const adaptXyzToD65 = (xyza: XyzColor): XyzColor => ({ 58 | x: xyza.x * 0.9555766 + xyza.y * -0.0230393 + xyza.z * 0.0631636, 59 | y: xyza.x * -0.0282895 + xyza.y * 1.0099416 + xyza.z * 0.0210077, 60 | z: xyza.x * 0.0122982 + xyza.y * -0.020483 + xyza.z * 1.3299098, 61 | }); 62 | 63 | /** 64 | * Converts an CIE XYZ color (D50) to RGBA color space (D65) 65 | * https://www.w3.org/TR/css-color-4/#color-conversion-code 66 | */ 67 | export const xyzaToRgba = (sourceXyza: XyzaColor): RgbaColor => { 68 | const xyz = adaptXyzToD65(sourceXyza); 69 | 70 | return clampRgba({ 71 | r: unlinearizeRgbChannel(0.032404542 * xyz.x - 0.015371385 * xyz.y - 0.004985314 * xyz.z), 72 | g: unlinearizeRgbChannel(-0.00969266 * xyz.x + 0.018760108 * xyz.y + 0.00041556 * xyz.z), 73 | b: unlinearizeRgbChannel(0.000556434 * xyz.x - 0.002040259 * xyz.y + 0.010572252 * xyz.z), 74 | a: sourceXyza.a, 75 | }); 76 | }; 77 | 78 | /** 79 | * Converts an RGB color (D65) to CIE XYZ (D50) 80 | * https://image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz 81 | */ 82 | export const rgbaToXyza = (rgba: RgbaColor): XyzaColor => { 83 | const sRed = linearizeRgbChannel(rgba.r); 84 | const sGreen = linearizeRgbChannel(rgba.g); 85 | const sBlue = linearizeRgbChannel(rgba.b); 86 | 87 | // Convert an array of linear-light sRGB values to CIE XYZ 88 | // using sRGB own white (D65 no chromatic adaptation) 89 | const xyza: XyzaColor = { 90 | x: (sRed * 0.4124564 + sGreen * 0.3575761 + sBlue * 0.1804375) * 100, 91 | y: (sRed * 0.2126729 + sGreen * 0.7151522 + sBlue * 0.072175) * 100, 92 | z: (sRed * 0.0193339 + sGreen * 0.119192 + sBlue * 0.9503041) * 100, 93 | a: rgba.a, 94 | }; 95 | 96 | return clampXyza(adaptXyzaToD50(xyza)); 97 | }; 98 | -------------------------------------------------------------------------------- /src/colord.ts: -------------------------------------------------------------------------------- 1 | import { Input, AnyColor, RgbaColor, HslaColor, HsvaColor } from "./types"; 2 | import { round } from "./helpers"; 3 | import { ALPHA_PRECISION } from "./constants"; 4 | import { parse } from "./parse"; 5 | import { rgbaToHex } from "./colorModels/hex"; 6 | import { roundRgba } from "./colorModels/rgb"; 7 | import { rgbaToRgbaString } from "./colorModels/rgbString"; 8 | import { rgbaToHsla, roundHsla } from "./colorModels/hsl"; 9 | import { rgbaToHslaString } from "./colorModels/hslString"; 10 | import { rgbaToHsva, roundHsva } from "./colorModels/hsv"; 11 | import { changeAlpha } from "./manipulate/changeAlpha"; 12 | import { saturate } from "./manipulate/saturate"; 13 | import { getBrightness } from "./get/getBrightness"; 14 | import { lighten } from "./manipulate/lighten"; 15 | import { invert } from "./manipulate/invert"; 16 | 17 | export class Colord { 18 | private readonly parsed: RgbaColor | null; 19 | readonly rgba: RgbaColor; 20 | 21 | constructor(input: AnyColor) { 22 | // Internal color format is RGBA object. 23 | // We do not round the internal RGBA numbers for better conversion accuracy. 24 | this.parsed = parse(input as Input)[0]; 25 | this.rgba = this.parsed || { r: 0, g: 0, b: 0, a: 1 }; 26 | } 27 | 28 | /** 29 | * Returns a boolean indicating whether or not an input has been parsed successfully. 30 | * Note: If parsing is unsuccessful, Colord defaults to black (does not throws an error). 31 | */ 32 | public isValid(): boolean { 33 | return this.parsed !== null; 34 | } 35 | 36 | /** 37 | * Returns the brightness of a color (from 0 to 1). 38 | * The calculation logic is modified from WCAG. 39 | * https://www.w3.org/TR/AERT/#color-contrast 40 | */ 41 | public brightness(): number { 42 | return round(getBrightness(this.rgba), 2); 43 | } 44 | 45 | /** 46 | * Same as calling `brightness() < 0.5`. 47 | */ 48 | public isDark(): boolean { 49 | return getBrightness(this.rgba) < 0.5; 50 | } 51 | 52 | /** 53 | * Same as calling `brightness() >= 0.5`. 54 | * */ 55 | public isLight(): boolean { 56 | return getBrightness(this.rgba) >= 0.5; 57 | } 58 | 59 | /** 60 | * Returns the hexadecimal representation of a color. 61 | * When the alpha channel value of the color is less than 1, 62 | * it outputs #rrggbbaa format instead of #rrggbb. 63 | */ 64 | public toHex(): string { 65 | return rgbaToHex(this.rgba); 66 | } 67 | 68 | /** 69 | * Converts a color to RGB color space and returns an object. 70 | * Always includes an alpha value from 0 to 1. 71 | */ 72 | public toRgb(): RgbaColor { 73 | return roundRgba(this.rgba); 74 | } 75 | 76 | /** 77 | * Converts a color to RGB color space and returns a string representation. 78 | * Outputs an alpha value only if it is less than 1. 79 | */ 80 | public toRgbString(): string { 81 | return rgbaToRgbaString(this.rgba); 82 | } 83 | 84 | /** 85 | * Converts a color to HSL color space and returns an object. 86 | * Always includes an alpha value from 0 to 1. 87 | */ 88 | public toHsl(): HslaColor { 89 | return roundHsla(rgbaToHsla(this.rgba)); 90 | } 91 | 92 | /** 93 | * Converts a color to HSL color space and returns a string representation. 94 | * Always includes an alpha value from 0 to 1. 95 | */ 96 | public toHslString(): string { 97 | return rgbaToHslaString(this.rgba); 98 | } 99 | 100 | /** 101 | * Converts a color to HSV color space and returns an object. 102 | * Always includes an alpha value from 0 to 1. 103 | */ 104 | public toHsv(): HsvaColor { 105 | return roundHsva(rgbaToHsva(this.rgba)); 106 | } 107 | 108 | /** 109 | * Creates a new instance containing an inverted (opposite) version of the color. 110 | */ 111 | public invert(): Colord { 112 | return colord(invert(this.rgba)); 113 | } 114 | 115 | /** 116 | * Increases the HSL saturation of a color by the given amount. 117 | */ 118 | public saturate(amount = 0.1): Colord { 119 | return colord(saturate(this.rgba, amount)); 120 | } 121 | 122 | /** 123 | * Decreases the HSL saturation of a color by the given amount. 124 | */ 125 | public desaturate(amount = 0.1): Colord { 126 | return colord(saturate(this.rgba, -amount)); 127 | } 128 | 129 | /** 130 | * Makes a gray color with the same lightness as a source color. 131 | */ 132 | public grayscale(): Colord { 133 | return colord(saturate(this.rgba, -1)); 134 | } 135 | 136 | /** 137 | * Increases the HSL lightness of a color by the given amount. 138 | */ 139 | public lighten(amount = 0.1): Colord { 140 | return colord(lighten(this.rgba, amount)); 141 | } 142 | 143 | /** 144 | * Increases the HSL lightness of a color by the given amount. 145 | */ 146 | public darken(amount = 0.1): Colord { 147 | return colord(lighten(this.rgba, -amount)); 148 | } 149 | 150 | /** 151 | * Changes the HSL hue of a color by the given amount. 152 | */ 153 | public rotate(amount = 15): Colord { 154 | return this.hue(this.hue() + amount); 155 | } 156 | 157 | /** 158 | * Allows to get or change an alpha channel value. 159 | */ 160 | public alpha(): number; 161 | public alpha(value: number): Colord; 162 | public alpha(value?: number): Colord | number { 163 | if (typeof value === "number") return colord(changeAlpha(this.rgba, value)); 164 | return round(this.rgba.a, ALPHA_PRECISION); 165 | } 166 | 167 | /** 168 | * Allows to get or change a hue value. 169 | */ 170 | public hue(): number; 171 | public hue(value: number): Colord; 172 | public hue(value?: number): Colord | number { 173 | const hsla = rgbaToHsla(this.rgba); 174 | if (typeof value === "number") return colord({ h: value, s: hsla.s, l: hsla.l, a: hsla.a }); 175 | return round(hsla.h); 176 | } 177 | 178 | /** 179 | * Determines whether two values are the same color. 180 | */ 181 | public isEqual(color: AnyColor | Colord): boolean { 182 | return this.toHex() === colord(color).toHex(); 183 | } 184 | } 185 | 186 | /** 187 | * Parses the given input color and creates a new `Colord` instance. 188 | * See accepted input formats: https://github.com/omgovich/colord#color-parsing 189 | */ 190 | export const colord = (input: AnyColor | Colord): Colord => { 191 | if (input instanceof Colord) return input; 192 | return new Colord(input); 193 | }; 194 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * We used to work with 2 digits after the decimal point, but it wasn't accurate enough, 3 | * so the library produced colors that were perceived differently. 4 | */ 5 | export const ALPHA_PRECISION = 3; 6 | 7 | /** 8 | * Valid CSS units. 9 | * https://developer.mozilla.org/en-US/docs/Web/CSS/angle 10 | */ 11 | export const ANGLE_UNITS: Record = { 12 | grad: 360 / 400, 13 | turn: 360, 14 | rad: 360 / (Math.PI * 2), 15 | }; 16 | -------------------------------------------------------------------------------- /src/extend.ts: -------------------------------------------------------------------------------- 1 | import { Colord } from "./colord"; 2 | import { parsers } from "./parse"; 3 | import { Parsers } from "./types"; 4 | 5 | export type Plugin = (ColordClass: typeof Colord, parsers: Parsers) => void; 6 | 7 | const activePlugins: Plugin[] = []; 8 | 9 | export const extend = (plugins: Plugin[]): void => { 10 | plugins.forEach((plugin) => { 11 | if (activePlugins.indexOf(plugin) < 0) { 12 | plugin(Colord, parsers); 13 | activePlugins.push(plugin); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/get/getBrightness.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | 3 | /** 4 | * Returns the brightness of a color [0-1]. 5 | * https://www.w3.org/TR/AERT/#color-contrast 6 | * https://en.wikipedia.org/wiki/YIQ 7 | */ 8 | export const getBrightness = (rgba: RgbaColor): number => { 9 | return (rgba.r * 299 + rgba.g * 587 + rgba.b * 114) / 1000 / 255; 10 | }; 11 | -------------------------------------------------------------------------------- /src/get/getContrast.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | import { getLuminance } from "./getLuminance"; 3 | 4 | /** 5 | * Returns a contrast ratio for a color pair [1-21]. 6 | * http://www.w3.org/TR/WCAG20/#contrast-ratiodef 7 | */ 8 | export const getContrast = (rgb1: RgbaColor, rgb2: RgbaColor): number => { 9 | const l1 = getLuminance(rgb1); 10 | const l2 = getLuminance(rgb2); 11 | return l1 > l2 ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05); 12 | }; 13 | -------------------------------------------------------------------------------- /src/get/getLuminance.ts: -------------------------------------------------------------------------------- 1 | import { linearizeRgbChannel } from "../colorModels/rgb"; 2 | import { RgbaColor } from "../types"; 3 | 4 | /** 5 | * Returns the perceived luminance of a color [0-1] according to WCAG 2.0. 6 | * https://www.w3.org/TR/WCAG20/#relativeluminancedef 7 | */ 8 | export const getLuminance = (rgba: RgbaColor): number => { 9 | const sRed = linearizeRgbChannel(rgba.r); 10 | const sGreen = linearizeRgbChannel(rgba.g); 11 | const sBlue = linearizeRgbChannel(rgba.b); 12 | 13 | return 0.2126 * sRed + 0.7152 * sGreen + 0.0722 * sBlue; 14 | }; 15 | -------------------------------------------------------------------------------- /src/get/getPerceivedDifference.ts: -------------------------------------------------------------------------------- 1 | import { LabaColor } from "../types"; 2 | 3 | /** 4 | * Calculates the perceived color difference according to [Delta E2000](https://en.wikipedia.org/wiki/Color_difference#CIEDE2000). 5 | * 6 | * ΔE - (Delta E, dE) The measure of change in visual perception of two given colors. 7 | * 8 | * Delta E is a metric for understanding how the human eye perceives color difference. 9 | * The term delta comes from mathematics, meaning change in a variable or function. 10 | * The suffix E references the German word Empfindung, which broadly means sensation. 11 | * 12 | * On a typical scale, the Delta E value will range from 0 to 100. 13 | * 14 | * | Delta E | Perception | 15 | * |---------|----------------------------------------| 16 | * | <= 1.0 | Not perceptible by human eyes | 17 | * | 1 - 2 | Perceptible through close observation | 18 | * | 2 - 10 | Perceptible at a glance | 19 | * | 11 - 49 | Colors are more similar than opposite | 20 | * | 100 | Colors are exact opposite | 21 | * 22 | * [Source](http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html) 23 | * [Read about Delta E](https://zschuessler.github.io/DeltaE/learn/#toc-delta-e-2000) 24 | */ 25 | export function getDeltaE00(color1: LabaColor, color2: LabaColor): number { 26 | const { l: l1, a: a1, b: b1 } = color1; 27 | const { l: l2, a: a2, b: b2 } = color2; 28 | 29 | const rad2deg = 180 / Math.PI; 30 | const deg2rad = Math.PI / 180; 31 | 32 | // dc -> delta c; 33 | // ml -> median l; 34 | const c1 = (a1 ** 2 + b1 ** 2) ** 0.5; 35 | const c2 = (a2 ** 2 + b2 ** 2) ** 0.5; 36 | const mc = (c1 + c2) / 2; 37 | const ml = (l1 + l2) / 2; 38 | 39 | // reuse 40 | const c7 = mc ** 7; 41 | const g = 0.5 * (1 - (c7 / (c7 + 25 ** 7)) ** 0.5); 42 | 43 | const a11 = a1 * (1 + g); 44 | const a22 = a2 * (1 + g); 45 | 46 | const c11 = (a11 ** 2 + b1 ** 2) ** 0.5; 47 | const c22 = (a22 ** 2 + b2 ** 2) ** 0.5; 48 | const mc1 = (c11 + c22) / 2; 49 | 50 | let h1 = a11 === 0 && b1 === 0 ? 0 : Math.atan2(b1, a11) * rad2deg; 51 | let h2 = a22 === 0 && b2 === 0 ? 0 : Math.atan2(b2, a22) * rad2deg; 52 | 53 | if (h1 < 0) h1 += 360; 54 | if (h2 < 0) h2 += 360; 55 | 56 | let dh = h2 - h1; 57 | const dhAbs = Math.abs(h2 - h1); 58 | 59 | if (dhAbs > 180 && h2 <= h1) { 60 | dh += 360; 61 | } else if (dhAbs > 180 && h2 > h1) { 62 | dh -= 360; 63 | } 64 | 65 | let H = h1 + h2; 66 | 67 | if (dhAbs <= 180) { 68 | H /= 2; 69 | } else { 70 | H = (h1 + h2 < 360 ? H + 360 : H - 360) / 2; 71 | } 72 | 73 | const T = 74 | 1 - 75 | 0.17 * Math.cos(deg2rad * (H - 30)) + 76 | 0.24 * Math.cos(deg2rad * 2 * H) + 77 | 0.32 * Math.cos(deg2rad * (3 * H + 6)) - 78 | 0.2 * Math.cos(deg2rad * (4 * H - 63)); 79 | 80 | const dL = l2 - l1; 81 | const dC = c22 - c11; 82 | const dH = 2 * Math.sin((deg2rad * dh) / 2) * (c11 * c22) ** 0.5; 83 | 84 | const sL = 1 + (0.015 * (ml - 50) ** 2) / (20 + (ml - 50) ** 2) ** 0.5; 85 | const sC = 1 + 0.045 * mc1; 86 | const sH = 1 + 0.015 * mc1 * T; 87 | 88 | const dTheta = 30 * Math.exp(-1 * ((H - 275) / 25) ** 2); 89 | const Rc = 2 * (c7 / (c7 + 25 ** 7)) ** 0.5; 90 | const Rt = -Rc * Math.sin(deg2rad * 2 * dTheta); 91 | 92 | const kl = 1; // 1 for graphic arts, 2 for textiles 93 | const kc = 1; // unity factor 94 | const kh = 1; // weighting factor 95 | 96 | return ( 97 | ((dL / kl / sL) ** 2 + 98 | (dC / kc / sC) ** 2 + 99 | (dH / kh / sH) ** 2 + 100 | (Rt * dC * dH) / (kc * sC * kh * sH)) ** 101 | 0.5 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ANGLE_UNITS } from "./constants"; 2 | 3 | export const isPresent = (value: unknown): boolean => { 4 | if (typeof value === "string") return value.length > 0; 5 | if (typeof value === "number") return true; 6 | return false; 7 | }; 8 | 9 | export const round = (number: number, digits = 0, base = Math.pow(10, digits)): number => { 10 | return Math.round(base * number) / base + 0; 11 | }; 12 | 13 | export const floor = (number: number, digits = 0, base = Math.pow(10, digits)): number => { 14 | return Math.floor(base * number) / base + 0; 15 | }; 16 | 17 | /** 18 | * Clamps a value between an upper and lower bound. 19 | * We use ternary operators because it makes the minified code 20 | * is 2 times shorter then `Math.min(Math.max(a,b),c)` 21 | * NaN is clamped to the lower bound 22 | */ 23 | export const clamp = (number: number, min = 0, max = 1): number => { 24 | return number > max ? max : number > min ? number : min; 25 | }; 26 | 27 | /** 28 | * Processes and clamps a degree (angle) value properly. 29 | * Any `NaN` or `Infinity` will be converted to `0`. 30 | * Examples: -1 => 359, 361 => 1 31 | */ 32 | export const clampHue = (degrees: number): number => { 33 | degrees = isFinite(degrees) ? degrees % 360 : 0; 34 | return degrees > 0 ? degrees : degrees + 360; 35 | }; 36 | 37 | /** 38 | * Converts a hue value to degrees from 0 to 360 inclusive. 39 | */ 40 | export const parseHue = (value: string, unit = "deg"): number => { 41 | return Number(value) * (ANGLE_UNITS[unit] || 1); 42 | }; 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { colord, Colord } from "./colord"; 2 | export { extend, Plugin } from "./extend"; 3 | export { getFormat } from "./parse"; 4 | export { random } from "./random"; 5 | 6 | export { 7 | HslColor, 8 | HslaColor, 9 | HsvColor, 10 | HsvaColor, 11 | HwbColor, 12 | HwbaColor, 13 | LabColor, 14 | LabaColor, 15 | LchColor, 16 | LchaColor, 17 | RgbColor, 18 | RgbaColor, 19 | XyzColor, 20 | XyzaColor, 21 | AnyColor, 22 | } from "./types"; 23 | -------------------------------------------------------------------------------- /src/manipulate/changeAlpha.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | 3 | export const changeAlpha = (rgba: RgbaColor, a: number): RgbaColor => ({ 4 | r: rgba.r, 5 | g: rgba.g, 6 | b: rgba.b, 7 | a, 8 | }); 9 | -------------------------------------------------------------------------------- /src/manipulate/invert.ts: -------------------------------------------------------------------------------- 1 | import { RgbaColor } from "../types"; 2 | 3 | export const invert = (rgba: RgbaColor): RgbaColor => ({ 4 | r: 255 - rgba.r, 5 | g: 255 - rgba.g, 6 | b: 255 - rgba.b, 7 | a: rgba.a, 8 | }); 9 | -------------------------------------------------------------------------------- /src/manipulate/lighten.ts: -------------------------------------------------------------------------------- 1 | import { rgbaToHsla } from "../colorModels/hsl"; 2 | import { HslaColor, RgbaColor } from "../types"; 3 | import { clamp } from "../helpers"; 4 | 5 | export const lighten = (rgba: RgbaColor, amount: number): HslaColor => { 6 | const hsla = rgbaToHsla(rgba); 7 | 8 | return { 9 | h: hsla.h, 10 | s: hsla.s, 11 | l: clamp(hsla.l + amount * 100, 0, 100), 12 | a: hsla.a, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/manipulate/mix.ts: -------------------------------------------------------------------------------- 1 | import { clampLaba, labaToRgba, rgbaToLaba } from "../colorModels/lab"; 2 | import { RgbaColor } from "../types"; 3 | 4 | export const mix = (rgba1: RgbaColor, rgba2: RgbaColor, ratio: number): RgbaColor => { 5 | const laba1 = rgbaToLaba(rgba1); 6 | const laba2 = rgbaToLaba(rgba2); 7 | 8 | const mixture = clampLaba({ 9 | l: laba1.l * (1 - ratio) + laba2.l * ratio, 10 | a: laba1.a * (1 - ratio) + laba2.a * ratio, 11 | b: laba1.b * (1 - ratio) + laba2.b * ratio, 12 | alpha: laba1.alpha * (1 - ratio) + laba2.alpha * ratio, 13 | }); 14 | 15 | return labaToRgba(mixture); 16 | }; 17 | -------------------------------------------------------------------------------- /src/manipulate/saturate.ts: -------------------------------------------------------------------------------- 1 | import { rgbaToHsla } from "../colorModels/hsl"; 2 | import { HslaColor, RgbaColor } from "../types"; 3 | import { clamp } from "../helpers"; 4 | 5 | export const saturate = (rgba: RgbaColor, amount: number): HslaColor => { 6 | const hsla = rgbaToHsla(rgba); 7 | 8 | return { 9 | h: hsla.h, 10 | s: clamp(hsla.s + amount * 100, 0, 100), 11 | l: hsla.l, 12 | a: hsla.a, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parsers, ParseResult, Input, InputObject, Format } from "./types"; 2 | import { parseHex } from "./colorModels/hex"; 3 | import { parseRgba } from "./colorModels/rgb"; 4 | import { parseHsla } from "./colorModels/hsl"; 5 | import { parseHslaString } from "./colorModels/hslString"; 6 | import { parseHsva } from "./colorModels/hsv"; 7 | import { parseRgbaString } from "./colorModels/rgbString"; 8 | 9 | // The built-in input parsing functions. 10 | // We use array instead of object to keep the bundle size lighter. 11 | export const parsers: Parsers = { 12 | string: [ 13 | [parseHex, "hex"], 14 | [parseRgbaString, "rgb"], 15 | [parseHslaString, "hsl"], 16 | ], 17 | object: [ 18 | [parseRgba, "rgb"], 19 | [parseHsla, "hsl"], 20 | [parseHsva, "hsv"], 21 | ], 22 | }; 23 | 24 | const findValidColor = ( 25 | input: I, 26 | parsers: Parser[] 27 | ): ParseResult | [null, undefined] => { 28 | for (let index = 0; index < parsers.length; index++) { 29 | const result = parsers[index][0](input); 30 | if (result) return [result, parsers[index][1]]; 31 | } 32 | 33 | return [null, undefined]; 34 | }; 35 | 36 | /** Tries to convert an incoming value into RGBA color by going through all color model parsers */ 37 | export const parse = (input: Input): ParseResult | [null, undefined] => { 38 | if (typeof input === "string") { 39 | return findValidColor(input.trim(), parsers.string); 40 | } 41 | 42 | // Don't forget that the type of `null` is "object" in JavaScript 43 | // https://bitsofco.de/javascript-typeof/ 44 | if (typeof input === "object" && input !== null) { 45 | return findValidColor(input, parsers.object); 46 | } 47 | 48 | return [null, undefined]; 49 | }; 50 | 51 | /** 52 | * Returns a color model name for the input passed to the function. 53 | */ 54 | export const getFormat = (input: Input): Format | undefined => parse(input)[1]; 55 | -------------------------------------------------------------------------------- /src/plugins/a11y.ts: -------------------------------------------------------------------------------- 1 | import { AnyColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { getContrast } from "../get/getContrast"; 4 | import { getLuminance } from "../get/getLuminance"; 5 | import { round, floor } from "../helpers"; 6 | 7 | // https://webaim.org/resources/contrastchecker/ 8 | interface ReadabilityOptions { 9 | level?: "AA" | "AAA"; 10 | size?: "normal" | "large"; 11 | } 12 | 13 | declare module "../colord" { 14 | interface Colord { 15 | /** 16 | * Returns the relative luminance of a color, 17 | * normalized to 0 for darkest black and 1 for lightest white. 18 | * https://www.w3.org/TR/WCAG20/#relativeluminancedef 19 | * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_Colors_and_Luminance 20 | */ 21 | luminance(): number; 22 | /** 23 | * Calculates a contrast ratio for a color pair. 24 | * This luminance difference is expressed as a ratio ranging 25 | * from 1 (e.g. white on white) to 21 (e.g., black on a white). 26 | * WCAG requires a ratio of at least 4.5 for normal text and 3 for large text. 27 | * https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html 28 | * https://webaim.org/articles/contrast/ 29 | */ 30 | contrast(color2?: AnyColor | Colord): number; 31 | /** 32 | * Checks that a background and text color pair conforms to WCAG 2.0 requirements. 33 | * https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html 34 | */ 35 | isReadable(color2?: AnyColor | Colord, options?: ReadabilityOptions): boolean; 36 | } 37 | } 38 | 39 | /** 40 | * A plugin adding accessibility and color contrast utilities. 41 | * Follows Web Content Accessibility Guidelines 2.0. 42 | * https://www.w3.org/TR/WCAG20/ 43 | */ 44 | const a11yPlugin: Plugin = (ColordClass): void => { 45 | /** 46 | * Returns WCAG text color contrast requirement. 47 | * Read explanation here https://webaim.org/resources/contrastchecker/ 48 | */ 49 | const getMinimalContrast = ({ level = "AA", size = "normal" }: ReadabilityOptions) => { 50 | if (level === "AAA" && size === "normal") return 7; 51 | if (level === "AA" && size === "large") return 3; 52 | return 4.5; 53 | }; 54 | 55 | ColordClass.prototype.luminance = function () { 56 | return round(getLuminance(this.rgba), 2); 57 | }; 58 | 59 | ColordClass.prototype.contrast = function (color2 = "#FFF") { 60 | const instance2 = color2 instanceof ColordClass ? color2 : new ColordClass(color2); 61 | return floor(getContrast(this.rgba, instance2.toRgb()), 2); 62 | }; 63 | 64 | ColordClass.prototype.isReadable = function (color2 = "#FFF", options = {}) { 65 | return this.contrast(color2) >= getMinimalContrast(options); 66 | }; 67 | }; 68 | 69 | export default a11yPlugin; 70 | -------------------------------------------------------------------------------- /src/plugins/cmyk.ts: -------------------------------------------------------------------------------- 1 | import { CmykaColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { parseCmyka, roundCmyka, rgbaToCmyka } from "../colorModels/cmyk"; 4 | import { parseCmykaString, rgbaToCmykaString } from "../colorModels/cmykString"; 5 | 6 | declare module "../colord" { 7 | interface Colord { 8 | /** 9 | * Converts a color to CMYK color space and returns an object. 10 | * https://drafts.csswg.org/css-color/#cmyk-colors 11 | * https://lea.verou.me/2009/03/cmyk-colors-in-css-useful-or-useless/ 12 | */ 13 | toCmyk(): CmykaColor; 14 | /** 15 | * Converts a color to CMYK color space and returns a string. 16 | * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/device-cmyk() 17 | */ 18 | toCmykString(): string; 19 | } 20 | } 21 | 22 | /** 23 | * A plugin adding support for CMYK color space. 24 | * https://lea.verou.me/2009/03/cmyk-colors-in-css-useful-or-useless/ 25 | * https://en.wikipedia.org/wiki/CMYK_color_model 26 | */ 27 | const cmykPlugin: Plugin = (ColordClass, parsers): void => { 28 | ColordClass.prototype.toCmyk = function () { 29 | return roundCmyka(rgbaToCmyka(this.rgba)); 30 | }; 31 | 32 | ColordClass.prototype.toCmykString = function () { 33 | return rgbaToCmykaString(this.rgba); 34 | }; 35 | 36 | parsers.object.push([parseCmyka, "cmyk"]); 37 | parsers.string.push([parseCmykaString, "cmyk"]); 38 | }; 39 | 40 | export default cmykPlugin; 41 | -------------------------------------------------------------------------------- /src/plugins/harmonies.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "../extend"; 2 | 3 | export type HarmonyType = 4 | | "analogous" 5 | | "complementary" 6 | | "double-split-complementary" 7 | | "rectangle" 8 | | "split-complementary" 9 | | "tetradic" 10 | | "triadic"; 11 | 12 | declare module "../colord" { 13 | interface Colord { 14 | /** 15 | * Returns an array of harmony colors as `Colord` instances. 16 | */ 17 | harmonies(type?: HarmonyType): Colord[]; 18 | } 19 | } 20 | 21 | /** 22 | * A plugin adding functionality to generate harmony colors. 23 | * https://en.wikipedia.org/wiki/Harmony_(color) 24 | */ 25 | const harmoniesPlugin: Plugin = (ColordClass): void => { 26 | /** 27 | * Harmony colors are colors with particular hue shift of the original color. 28 | */ 29 | const hueShifts: Record = { 30 | analogous: [-30, 0, 30], 31 | complementary: [0, 180], 32 | "double-split-complementary": [-30, 0, 30, 150, 210], 33 | rectangle: [0, 60, 180, 240], 34 | tetradic: [0, 90, 180, 270], 35 | triadic: [0, 120, 240], 36 | "split-complementary": [0, 150, 210], 37 | }; 38 | 39 | ColordClass.prototype.harmonies = function (type = "complementary") { 40 | return hueShifts[type].map((shift) => this.rotate(shift)); 41 | }; 42 | }; 43 | 44 | export default harmoniesPlugin; 45 | -------------------------------------------------------------------------------- /src/plugins/hwb.ts: -------------------------------------------------------------------------------- 1 | import { HwbaColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { parseHwba, rgbaToHwba, roundHwba } from "../colorModels/hwb"; 4 | import { parseHwbaString, rgbaToHwbaString } from "../colorModels/hwbString"; 5 | 6 | declare module "../colord" { 7 | interface Colord { 8 | /** 9 | * Converts a color to HWB (Hue-Whiteness-Blackness) color space and returns an object. 10 | * https://en.wikipedia.org/wiki/HWB_color_model 11 | */ 12 | toHwb(): HwbaColor; 13 | /** 14 | * Converts a color to HWB (Hue-Whiteness-Blackness) color space and returns a string. 15 | * https://www.w3.org/TR/css-color-4/#the-hwb-notation 16 | */ 17 | toHwbString(): string; 18 | } 19 | } 20 | 21 | /** 22 | * A plugin adding support for HWB (Hue-Whiteness-Blackness) color model. 23 | * https://en.wikipedia.org/wiki/HWB_color_model 24 | * https://www.w3.org/TR/css-color-4/#the-hwb-notation 25 | */ 26 | const hwbPlugin: Plugin = (ColordClass, parsers): void => { 27 | ColordClass.prototype.toHwb = function () { 28 | return roundHwba(rgbaToHwba(this.rgba)); 29 | }; 30 | 31 | ColordClass.prototype.toHwbString = function () { 32 | return rgbaToHwbaString(this.rgba); 33 | }; 34 | 35 | parsers.string.push([parseHwbaString, "hwb"]); 36 | parsers.object.push([parseHwba, "hwb"]); 37 | }; 38 | 39 | export default hwbPlugin; 40 | -------------------------------------------------------------------------------- /src/plugins/lab.ts: -------------------------------------------------------------------------------- 1 | import { LabaColor, AnyColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { parseLaba, roundLaba, rgbaToLaba } from "../colorModels/lab"; 4 | import { getDeltaE00 } from "../get/getPerceivedDifference"; 5 | import { clamp, round } from "../helpers"; 6 | 7 | declare module "../colord" { 8 | interface Colord { 9 | /** 10 | * Converts a color to CIELAB color space and returns an object. 11 | * The object always includes `alpha` value [0, 1]. 12 | */ 13 | toLab(): LabaColor; 14 | 15 | /** 16 | * Calculates the perceived color difference for two colors according to 17 | * [Delta E2000](https://en.wikipedia.org/wiki/Color_difference#CIEDE2000). 18 | * Returns a value in [0, 1] range. 19 | */ 20 | delta(color?: AnyColor | Colord): number; 21 | } 22 | } 23 | 24 | /** 25 | * A plugin adding support for CIELAB color space. 26 | * https://en.wikipedia.org/wiki/CIELAB_color_space 27 | */ 28 | const labPlugin: Plugin = (ColordClass, parsers): void => { 29 | ColordClass.prototype.toLab = function () { 30 | return roundLaba(rgbaToLaba(this.rgba)); 31 | }; 32 | 33 | ColordClass.prototype.delta = function (color = "#FFF") { 34 | const compared = color instanceof ColordClass ? color : new ColordClass(color); 35 | const delta = getDeltaE00(this.toLab(), compared.toLab()) / 100; 36 | return clamp(round(delta, 3)); 37 | }; 38 | 39 | parsers.object.push([parseLaba, "lab"]); 40 | }; 41 | 42 | export default labPlugin; 43 | -------------------------------------------------------------------------------- /src/plugins/lch.ts: -------------------------------------------------------------------------------- 1 | import { LchaColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { parseLcha, roundLcha, rgbaToLcha } from "../colorModels/lch"; 4 | import { parseLchaString, rgbaToLchaString } from "../colorModels/lchString"; 5 | 6 | declare module "../colord" { 7 | interface Colord { 8 | /** 9 | * Converts a color to CIELCH (Lightness-Chroma-Hue) color space and returns an object. 10 | * https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/ 11 | * https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_model 12 | */ 13 | toLch(): LchaColor; 14 | /** 15 | * Converts a color to CIELCH (Lightness-Chroma-Hue) color space and returns a string. 16 | * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch() 17 | */ 18 | toLchString(): string; 19 | } 20 | } 21 | 22 | /** 23 | * A plugin adding support for CIELCH color space. 24 | * https://lea.verou.me/2020/04/lch-colors-in-css-what-why-and-how/ 25 | * https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_model 26 | */ 27 | const lchPlugin: Plugin = (ColordClass, parsers): void => { 28 | ColordClass.prototype.toLch = function () { 29 | return roundLcha(rgbaToLcha(this.rgba)); 30 | }; 31 | 32 | ColordClass.prototype.toLchString = function () { 33 | return rgbaToLchaString(this.rgba); 34 | }; 35 | 36 | parsers.string.push([parseLchaString, "lch"]); 37 | parsers.object.push([parseLcha, "lch"]); 38 | }; 39 | 40 | export default lchPlugin; 41 | -------------------------------------------------------------------------------- /src/plugins/minify.ts: -------------------------------------------------------------------------------- 1 | import { Colord } from "../colord"; 2 | import { Plugin } from "../extend"; 3 | import { round } from "../helpers"; 4 | 5 | interface MinificationOptions { 6 | hex?: boolean; 7 | alphaHex?: boolean; 8 | rgb?: boolean; 9 | hsl?: boolean; 10 | name?: boolean; 11 | transparent?: boolean; 12 | } 13 | 14 | declare module "../colord" { 15 | interface Colord { 16 | /** Returns the shortest string representation of the color */ 17 | minify(options?: MinificationOptions): string; 18 | } 19 | } 20 | 21 | /** 22 | * A plugin adding a color minification utilities. 23 | */ 24 | const minifyPlugin: Plugin = (ColordClass): void => { 25 | // Finds the shortest hex representation 26 | const minifyHex = (instance: Colord): string | null => { 27 | const hex = instance.toHex(); 28 | const alpha = instance.alpha(); 29 | const [, r1, r2, g1, g2, b1, b2, a1, a2] = hex.split(""); 30 | 31 | // Make sure conversion is lossless 32 | if (alpha > 0 && alpha < 1 && round(parseInt(a1 + a2, 16) / 255, 2) !== alpha) return null; 33 | 34 | // Check if the string can be shorten 35 | if (r1 === r2 && g1 === g2 && b1 === b2) { 36 | if (alpha === 1) { 37 | // Express as 3 digit hexadecimal string if the color doesn't have an alpha channel 38 | return "#" + r1 + g1 + b1; 39 | } else if (a1 === a2) { 40 | // Format 4 digit hex 41 | return "#" + r1 + g1 + b1 + a1; 42 | } 43 | } 44 | 45 | return hex; 46 | }; 47 | 48 | // Returns the shortest string in array 49 | const findShortestString = (variants: string[]): string => { 50 | let shortest = variants[0]; 51 | 52 | for (let index = 1; index < variants.length; index++) { 53 | if (variants[index].length < shortest.length) shortest = variants[index]; 54 | } 55 | 56 | return shortest; 57 | }; 58 | 59 | // Removes leading zero before floating point if necessary 60 | const shortenNumber = (number: number): string | number => { 61 | if (number > 0 && number < 1) return number.toString().replace("0.", "."); 62 | return number; 63 | }; 64 | 65 | // Define new public method 66 | ColordClass.prototype.minify = function (options: MinificationOptions = {}) { 67 | const rgb = this.toRgb(); 68 | const r = shortenNumber(rgb.r); 69 | const g = shortenNumber(rgb.g); 70 | const b = shortenNumber(rgb.b); 71 | 72 | const hsl = this.toHsl(); 73 | const h = shortenNumber(hsl.h); 74 | const s = shortenNumber(hsl.s); 75 | const l = shortenNumber(hsl.l); 76 | 77 | const a = shortenNumber(this.alpha()); 78 | 79 | const defaults: MinificationOptions = { 80 | hex: true, 81 | rgb: true, 82 | hsl: true, 83 | }; 84 | 85 | const settings: MinificationOptions = Object.assign(defaults, options); 86 | 87 | const variants: string[] = []; 88 | 89 | // #rrggbb, #rrggbbaa, #rgb or #rgba 90 | if (settings.hex && (a === 1 || settings.alphaHex)) { 91 | const hex = minifyHex(this); 92 | if (hex) variants.push(hex); 93 | } 94 | 95 | // rgb() functional notation with no spaces 96 | if (settings.rgb) { 97 | variants.push(a === 1 ? `rgb(${r},${g},${b})` : `rgba(${r},${g},${b},${a})`); 98 | } 99 | 100 | // hsl() functional notation with no spaces 101 | if (settings.hsl) { 102 | variants.push(a === 1 ? `hsl(${h},${s}%,${l}%)` : `hsla(${h},${s}%,${l}%,${a})`); 103 | } 104 | 105 | if (settings.transparent && r === 0 && g === 0 && b === 0 && a === 0) { 106 | // Convert to transparent keyword if this option is enabled 107 | variants.push("transparent"); 108 | } else if (a === 1 && settings.name && typeof this.toName === "function") { 109 | // CSS color keyword if "names" plugin is installed 110 | const name = this.toName(); 111 | if (name) variants.push(name); 112 | } 113 | 114 | return findShortestString(variants); 115 | }; 116 | }; 117 | 118 | export default minifyPlugin; 119 | -------------------------------------------------------------------------------- /src/plugins/mix.ts: -------------------------------------------------------------------------------- 1 | import { AnyColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { mix } from "../manipulate/mix"; 4 | import { Colord } from "../colord"; 5 | 6 | declare module "../colord" { 7 | interface Colord { 8 | /** 9 | * Produces a mixture of two colors through CIE LAB color space and returns a new Colord instance. 10 | */ 11 | mix(color2: AnyColor | Colord, ratio?: number): Colord; 12 | 13 | /** 14 | * Generates a tints palette based on original color. 15 | */ 16 | tints(count?: number): Colord[]; 17 | 18 | /** 19 | * Generates a shades palette based on original color. 20 | */ 21 | shades(count?: number): Colord[]; 22 | 23 | /** 24 | * Generates a tones palette based on original color. 25 | */ 26 | tones(count?: number): Colord[]; 27 | } 28 | } 29 | 30 | /** 31 | * A plugin adding a color mixing utilities. 32 | */ 33 | const mixPlugin: Plugin = (ColordClass): void => { 34 | ColordClass.prototype.mix = function (color2, ratio = 0.5) { 35 | const instance2 = color2 instanceof ColordClass ? color2 : new ColordClass(color2); 36 | 37 | const mixture = mix(this.toRgb(), instance2.toRgb(), ratio); 38 | return new ColordClass(mixture); 39 | }; 40 | 41 | /** 42 | * Generate a palette from mixing a source color with another. 43 | */ 44 | function mixPalette(source: Colord, hex: string, count = 5): Colord[] { 45 | const palette = []; 46 | const step = 1 / (count - 1); 47 | for (let i = 0; i <= count - 1; i++) { 48 | palette.push(source.mix(hex, step * i)); 49 | } 50 | return palette; 51 | } 52 | 53 | ColordClass.prototype.tints = function (count) { 54 | return mixPalette(this, "#fff", count); 55 | }; 56 | 57 | ColordClass.prototype.shades = function (count) { 58 | return mixPalette(this, "#000", count); 59 | }; 60 | 61 | ColordClass.prototype.tones = function (count) { 62 | return mixPalette(this, "#808080", count); 63 | }; 64 | }; 65 | 66 | export default mixPlugin; 67 | -------------------------------------------------------------------------------- /src/plugins/names.ts: -------------------------------------------------------------------------------- 1 | import { ParseFunction, RgbaColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | 4 | interface ConvertOptions { 5 | closest?: boolean; 6 | } 7 | 8 | declare module "../colord" { 9 | interface Colord { 10 | /** Finds CSS color keyword that matches with the color value */ 11 | toName(options?: ConvertOptions): string | undefined; 12 | } 13 | } 14 | 15 | /** 16 | * Plugin to work with named colors. 17 | * Adds a parser to read CSS color names and `toName` method. 18 | * See https://www.w3.org/TR/css-color-4/#named-colors 19 | * Supports 'transparent' string as defined in 20 | * https://drafts.csswg.org/css-color/#transparent-color 21 | */ 22 | const namesPlugin: Plugin = (ColordClass, parsers): void => { 23 | // The default CSS color names dictionary 24 | // The properties order is optimized for better compression 25 | const NAME_HEX_STORE: Record = { 26 | white: "#ffffff", 27 | bisque: "#ffe4c4", 28 | blue: "#0000ff", 29 | cadetblue: "#5f9ea0", 30 | chartreuse: "#7fff00", 31 | chocolate: "#d2691e", 32 | coral: "#ff7f50", 33 | antiquewhite: "#faebd7", 34 | aqua: "#00ffff", 35 | azure: "#f0ffff", 36 | whitesmoke: "#f5f5f5", 37 | papayawhip: "#ffefd5", 38 | plum: "#dda0dd", 39 | blanchedalmond: "#ffebcd", 40 | black: "#000000", 41 | gold: "#ffd700", 42 | goldenrod: "#daa520", 43 | gainsboro: "#dcdcdc", 44 | cornsilk: "#fff8dc", 45 | cornflowerblue: "#6495ed", 46 | burlywood: "#deb887", 47 | aquamarine: "#7fffd4", 48 | beige: "#f5f5dc", 49 | crimson: "#dc143c", 50 | cyan: "#00ffff", 51 | darkblue: "#00008b", 52 | darkcyan: "#008b8b", 53 | darkgoldenrod: "#b8860b", 54 | darkkhaki: "#bdb76b", 55 | darkgray: "#a9a9a9", 56 | darkgreen: "#006400", 57 | darkgrey: "#a9a9a9", 58 | peachpuff: "#ffdab9", 59 | darkmagenta: "#8b008b", 60 | darkred: "#8b0000", 61 | darkorchid: "#9932cc", 62 | darkorange: "#ff8c00", 63 | darkslateblue: "#483d8b", 64 | gray: "#808080", 65 | darkslategray: "#2f4f4f", 66 | darkslategrey: "#2f4f4f", 67 | deeppink: "#ff1493", 68 | deepskyblue: "#00bfff", 69 | wheat: "#f5deb3", 70 | firebrick: "#b22222", 71 | floralwhite: "#fffaf0", 72 | ghostwhite: "#f8f8ff", 73 | darkviolet: "#9400d3", 74 | magenta: "#ff00ff", 75 | green: "#008000", 76 | dodgerblue: "#1e90ff", 77 | grey: "#808080", 78 | honeydew: "#f0fff0", 79 | hotpink: "#ff69b4", 80 | blueviolet: "#8a2be2", 81 | forestgreen: "#228b22", 82 | lawngreen: "#7cfc00", 83 | indianred: "#cd5c5c", 84 | indigo: "#4b0082", 85 | fuchsia: "#ff00ff", 86 | brown: "#a52a2a", 87 | maroon: "#800000", 88 | mediumblue: "#0000cd", 89 | lightcoral: "#f08080", 90 | darkturquoise: "#00ced1", 91 | lightcyan: "#e0ffff", 92 | ivory: "#fffff0", 93 | lightyellow: "#ffffe0", 94 | lightsalmon: "#ffa07a", 95 | lightseagreen: "#20b2aa", 96 | linen: "#faf0e6", 97 | mediumaquamarine: "#66cdaa", 98 | lemonchiffon: "#fffacd", 99 | lime: "#00ff00", 100 | khaki: "#f0e68c", 101 | mediumseagreen: "#3cb371", 102 | limegreen: "#32cd32", 103 | mediumspringgreen: "#00fa9a", 104 | lightskyblue: "#87cefa", 105 | lightblue: "#add8e6", 106 | midnightblue: "#191970", 107 | lightpink: "#ffb6c1", 108 | mistyrose: "#ffe4e1", 109 | moccasin: "#ffe4b5", 110 | mintcream: "#f5fffa", 111 | lightslategray: "#778899", 112 | lightslategrey: "#778899", 113 | navajowhite: "#ffdead", 114 | navy: "#000080", 115 | mediumvioletred: "#c71585", 116 | powderblue: "#b0e0e6", 117 | palegoldenrod: "#eee8aa", 118 | oldlace: "#fdf5e6", 119 | paleturquoise: "#afeeee", 120 | mediumturquoise: "#48d1cc", 121 | mediumorchid: "#ba55d3", 122 | rebeccapurple: "#663399", 123 | lightsteelblue: "#b0c4de", 124 | mediumslateblue: "#7b68ee", 125 | thistle: "#d8bfd8", 126 | tan: "#d2b48c", 127 | orchid: "#da70d6", 128 | mediumpurple: "#9370db", 129 | purple: "#800080", 130 | pink: "#ffc0cb", 131 | skyblue: "#87ceeb", 132 | springgreen: "#00ff7f", 133 | palegreen: "#98fb98", 134 | red: "#ff0000", 135 | yellow: "#ffff00", 136 | slateblue: "#6a5acd", 137 | lavenderblush: "#fff0f5", 138 | peru: "#cd853f", 139 | palevioletred: "#db7093", 140 | violet: "#ee82ee", 141 | teal: "#008080", 142 | slategray: "#708090", 143 | slategrey: "#708090", 144 | aliceblue: "#f0f8ff", 145 | darkseagreen: "#8fbc8f", 146 | darkolivegreen: "#556b2f", 147 | greenyellow: "#adff2f", 148 | seagreen: "#2e8b57", 149 | seashell: "#fff5ee", 150 | tomato: "#ff6347", 151 | silver: "#c0c0c0", 152 | sienna: "#a0522d", 153 | lavender: "#e6e6fa", 154 | lightgreen: "#90ee90", 155 | orange: "#ffa500", 156 | orangered: "#ff4500", 157 | steelblue: "#4682b4", 158 | royalblue: "#4169e1", 159 | turquoise: "#40e0d0", 160 | yellowgreen: "#9acd32", 161 | salmon: "#fa8072", 162 | saddlebrown: "#8b4513", 163 | sandybrown: "#f4a460", 164 | rosybrown: "#bc8f8f", 165 | darksalmon: "#e9967a", 166 | lightgoldenrodyellow: "#fafad2", 167 | snow: "#fffafa", 168 | lightgrey: "#d3d3d3", 169 | lightgray: "#d3d3d3", 170 | dimgray: "#696969", 171 | dimgrey: "#696969", 172 | olivedrab: "#6b8e23", 173 | olive: "#808000", 174 | }; 175 | 176 | // Second dictionary to provide faster search by HEX value 177 | const HEX_NAME_STORE: Record = {}; 178 | for (const name in NAME_HEX_STORE) HEX_NAME_STORE[NAME_HEX_STORE[name]] = name; 179 | 180 | // Third dictionary to cache RGBA values (useful for distance calculation) 181 | const NAME_RGBA_STORE: Record = {}; 182 | 183 | // Finds a distance between two colors 184 | // See https://www.wikiwand.com/en/Color_difference 185 | const getDistanceBetween = (rgb1: RgbaColor, rgb2: RgbaColor) => { 186 | return (rgb1.r - rgb2.r) ** 2 + (rgb1.g - rgb2.g) ** 2 + (rgb1.b - rgb2.b) ** 2; 187 | }; 188 | 189 | // Define new color conversion method 190 | ColordClass.prototype.toName = function (options) { 191 | // Process "transparent" keyword 192 | // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#transparent_keyword 193 | if (!this.rgba.a && !this.rgba.r && !this.rgba.g && !this.rgba.b) return "transparent"; 194 | 195 | // Return exact match right away 196 | const exactMatch = HEX_NAME_STORE[this.toHex()]; 197 | if (exactMatch) return exactMatch; 198 | 199 | // Find closest color, if there is no exact match and `approximate` flag enabled 200 | if (options?.closest) { 201 | const rgba = this.toRgb(); 202 | let minDistance = Infinity; 203 | let closestMatch = "black"; 204 | 205 | // Fill the dictionary if empty 206 | if (!NAME_RGBA_STORE.length) { 207 | for (const name in NAME_HEX_STORE) { 208 | NAME_RGBA_STORE[name] = new ColordClass(NAME_HEX_STORE[name]).toRgb(); 209 | } 210 | } 211 | 212 | // Find the closest color 213 | for (const name in NAME_HEX_STORE) { 214 | const distance = getDistanceBetween(rgba, NAME_RGBA_STORE[name]); 215 | if (distance < minDistance) { 216 | minDistance = distance; 217 | closestMatch = name; 218 | } 219 | } 220 | 221 | return closestMatch; 222 | } 223 | 224 | return undefined; 225 | }; 226 | 227 | // Add CSS color names parser 228 | const parseColorName: ParseFunction = (input: string): RgbaColor | null => { 229 | // the color names are case-insensitive according to CSS Color Level 3 230 | const name = input.toLowerCase(); 231 | // "transparent" is a shorthand for transparent black 232 | const hex = name === "transparent" ? "#0000" : NAME_HEX_STORE[name]; 233 | if (hex) return new ColordClass(hex).toRgb(); 234 | return null; 235 | }; 236 | 237 | parsers.string.push([parseColorName, "name"]); 238 | }; 239 | 240 | export default namesPlugin; 241 | -------------------------------------------------------------------------------- /src/plugins/xyz.ts: -------------------------------------------------------------------------------- 1 | import { XyzaColor } from "../types"; 2 | import { Plugin } from "../extend"; 3 | import { parseXyza, rgbaToXyza, roundXyza } from "../colorModels/xyz"; 4 | 5 | declare module "../colord" { 6 | interface Colord { 7 | toXyz(): XyzaColor; 8 | } 9 | } 10 | 11 | /** 12 | * A plugin adding support for CIE XYZ colorspace. 13 | * Wikipedia: https://en.wikipedia.org/wiki/CIE_1931_color_space 14 | * Helpful article: https://www.sttmedia.com/colormodel-xyz 15 | */ 16 | const xyzPlugin: Plugin = (ColordClass, parsers): void => { 17 | ColordClass.prototype.toXyz = function () { 18 | return roundXyza(rgbaToXyza(this.rgba)); 19 | }; 20 | 21 | parsers.object.push([parseXyza, "xyz"]); 22 | }; 23 | 24 | export default xyzPlugin; 25 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | import { Colord } from "./colord"; 2 | 3 | export const random = (): Colord => { 4 | return new Colord({ 5 | r: Math.random() * 255, 6 | g: Math.random() * 255, 7 | b: Math.random() * 255, 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type RgbColor = { 2 | r: number; 3 | g: number; 4 | b: number; 5 | }; 6 | 7 | export type HslColor = { 8 | h: number; 9 | s: number; 10 | l: number; 11 | }; 12 | 13 | export type HsvColor = { 14 | h: number; 15 | s: number; 16 | v: number; 17 | }; 18 | 19 | export type HwbColor = { 20 | h: number; 21 | w: number; 22 | b: number; 23 | }; 24 | 25 | export interface XyzColor { 26 | x: number; 27 | y: number; 28 | z: number; 29 | } 30 | 31 | export interface LabColor { 32 | l: number; 33 | a: number; 34 | b: number; 35 | } 36 | 37 | export interface LchColor { 38 | l: number; 39 | c: number; 40 | h: number; 41 | } 42 | 43 | export interface CmykColor { 44 | c: number; 45 | m: number; 46 | y: number; 47 | k: number; 48 | } 49 | 50 | type WithAlpha = O & { a: number }; 51 | export type RgbaColor = WithAlpha; 52 | export type HslaColor = WithAlpha; 53 | export type HsvaColor = WithAlpha; 54 | export type HwbaColor = WithAlpha; 55 | export type XyzaColor = WithAlpha; // Naming is the hardest part https://stackoverflow.com/a/2464027 56 | export type LabaColor = LabColor & { alpha: number }; 57 | export type LchaColor = WithAlpha; 58 | export type CmykaColor = WithAlpha; 59 | 60 | export type ObjectColor = 61 | | RgbColor 62 | | RgbaColor 63 | | HslColor 64 | | HslaColor 65 | | HsvColor 66 | | HsvaColor 67 | | HwbColor 68 | | HwbaColor 69 | | XyzColor 70 | | XyzaColor 71 | | LabColor 72 | | LabaColor 73 | | LchColor 74 | | LchaColor 75 | | CmykColor 76 | | CmykaColor; 77 | 78 | export type AnyColor = string | ObjectColor; 79 | 80 | export type InputObject = Record; 81 | 82 | export type Format = 83 | | "name" 84 | | "hex" 85 | | "rgb" 86 | | "hsl" 87 | | "hsv" 88 | | "hwb" 89 | | "xyz" 90 | | "lab" 91 | | "lch" 92 | | "cmyk"; 93 | 94 | export type Input = string | InputObject; 95 | 96 | export type ParseResult = [RgbaColor, Format]; 97 | 98 | export type ParseFunction = (input: I) => RgbaColor | null; 99 | 100 | export type Parser = [ParseFunction, Format]; 101 | 102 | export type Parsers = { 103 | string: Array>; 104 | object: Array>; 105 | }; 106 | -------------------------------------------------------------------------------- /tests/benchmark.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import b from "benny"; 3 | import { colord } from "../src"; 4 | // @ts-ignore 5 | import tinycolor2 from "tinycolor2"; 6 | // @ts-ignore 7 | import color from "color"; 8 | // @ts-ignore 9 | import chroma from "chroma-js"; 10 | // @ts-ignore 11 | import AcColor from "ac-colors"; 12 | 13 | b.suite( 14 | "Parse HEX and convert to HSLA object/array", 15 | 16 | b.add("colord", () => { 17 | colord("#808080").toHsl(); 18 | }), 19 | 20 | b.add("color", () => { 21 | // @ts-ignore 22 | color("#808080").hsl().object(); 23 | }), 24 | 25 | b.add("tinycolor2", () => { 26 | // @ts-ignore 27 | tinycolor2("#808080").toHsl(); 28 | }), 29 | 30 | b.add("ac-colors", () => { 31 | // @ts-ignore 32 | new AcColor({ color: "#808080", type: "hex" }).hsl; 33 | }), 34 | 35 | b.add("chroma-js", () => { 36 | // @ts-ignore 37 | chroma("#808080").hsl(); 38 | }), 39 | 40 | b.cycle(), 41 | b.complete() 42 | ); 43 | -------------------------------------------------------------------------------- /tests/colord.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { colord, random, getFormat, Colord, AnyColor } from "../src/"; 3 | import { fixtures, lime, saturationLevels } from "./fixtures"; 4 | 5 | it("Converts between HEX, RGB, HSL and HSV color models properly", () => { 6 | for (const fixture of fixtures) { 7 | expect(colord(fixture.rgb).toHex()).toBe(fixture.hex); 8 | expect(colord(fixture.hsl).toHex()).toBe(fixture.hex); 9 | expect(colord(fixture.hsv).toHex()).toBe(fixture.hex); 10 | 11 | expect(colord(fixture.hex).toRgb()).toMatchObject({ ...fixture.rgb, a: 1 }); 12 | expect(colord(fixture.hsl).toRgb()).toMatchObject({ ...fixture.rgb, a: 1 }); 13 | expect(colord(fixture.hsv).toRgb()).toMatchObject({ ...fixture.rgb, a: 1 }); 14 | 15 | expect(colord(fixture.hex).toHsl()).toMatchObject({ ...fixture.hsl, a: 1 }); 16 | expect(colord(fixture.rgb).toHsl()).toMatchObject({ ...fixture.hsl, a: 1 }); 17 | expect(colord(fixture.hsv).toHsl()).toMatchObject({ ...fixture.hsl, a: 1 }); 18 | 19 | expect(colord(fixture.hex).toHsv()).toMatchObject({ ...fixture.hsv, a: 1 }); 20 | expect(colord(fixture.rgb).toHsv()).toMatchObject({ ...fixture.hsv, a: 1 }); 21 | expect(colord(fixture.hsl).toHsv()).toMatchObject({ ...fixture.hsv, a: 1 }); 22 | } 23 | }); 24 | 25 | it("Parses and converts a color", () => { 26 | for (const format in lime) { 27 | const instance = colord(lime[format] as AnyColor); 28 | expect(instance.toHex()).toBe(lime.hex); 29 | expect(instance.toRgb()).toMatchObject(lime.rgba); 30 | expect(instance.toRgbString()).toBe(lime.rgbString); 31 | expect(instance.toHsl()).toMatchObject(lime.hsla); 32 | expect(instance.toHslString()).toBe(lime.hslString); 33 | expect(instance.toHsv()).toMatchObject(lime.hsva); 34 | } 35 | }); 36 | 37 | it("Adds alpha number to RGB and HSL strings only if the color has an opacity", () => { 38 | expect(colord("rgb(0, 0, 0)").toRgbString()).toBe("rgb(0, 0, 0)"); 39 | expect(colord("hsl(0, 0%, 0%)").toHslString()).toBe("hsl(0, 0%, 0%)"); 40 | expect(colord("rgb(0, 0, 0)").alpha(0.5).toRgbString()).toBe("rgba(0, 0, 0, 0.5)"); 41 | expect(colord("hsl(0, 0%, 0%)").alpha(0.5).toHslString()).toBe("hsla(0, 0%, 0%, 0.5)"); 42 | }); 43 | 44 | it("Parses modern RGB functional notations", () => { 45 | expect(colord("rgb(0% 50% 100%)").toRgb()).toMatchObject({ r: 0, g: 128, b: 255, a: 1 }); 46 | expect(colord("rgb(10% 20% 30% / 33%)").toRgb()).toMatchObject({ r: 26, g: 51, b: 77, a: 0.33 }); 47 | expect(colord("rgba(10% 20% 30% / 0.5)").toRgb()).toMatchObject({ r: 26, g: 51, b: 77, a: 0.5 }); 48 | }); 49 | 50 | it("Parses modern HSL functional notations", () => { 51 | expect(colord("hsl(120deg 100% 50%)").toHsl()).toMatchObject({ h: 120, s: 100, l: 50, a: 1 }); 52 | expect(colord("hsl(10deg 20% 30% / 0.1)").toHsl()).toMatchObject({ h: 10, s: 20, l: 30, a: 0.1 }); 53 | expect(colord("hsl(10deg 20% 30% / 90%)").toHsl()).toMatchObject({ h: 10, s: 20, l: 30, a: 0.9 }); 54 | expect(colord("hsl(90deg 50% 50%/50%)").toHsl()).toMatchObject({ h: 90, s: 50, l: 50, a: 0.5 }); 55 | }); 56 | 57 | it("Supports HEX4 and HEX8 color models", () => { 58 | expect(colord("#ffffffff").toRgb()).toMatchObject({ r: 255, g: 255, b: 255, a: 1 }); 59 | expect(colord("#80808080").toRgb()).toMatchObject({ r: 128, g: 128, b: 128, a: 0.5 }); 60 | expect(colord("#AAAF").toRgb()).toMatchObject({ r: 170, g: 170, b: 170, a: 1 }); 61 | expect(colord("#5550").toRgb()).toMatchObject({ r: 85, g: 85, b: 85, a: 0 }); 62 | expect(colord({ r: 255, g: 255, b: 255, a: 1 }).toHex()).toBe("#ffffff"); 63 | expect(colord({ r: 170, g: 170, b: 170, a: 0.5 }).toHex()).toBe("#aaaaaa80"); 64 | expect(colord({ r: 128, g: 128, b: 128, a: 0 }).toHex()).toBe("#80808000"); 65 | }); 66 | 67 | it("Ignores a case and extra whitespace", () => { 68 | expect(colord(" #0a0a0a ").toRgb()).toMatchObject({ r: 10, g: 10, b: 10, a: 1 }); 69 | expect(colord("RGB( 10, 10, 10 )").toRgb()).toMatchObject({ r: 10, g: 10, b: 10, a: 1 }); 70 | expect(colord(" rGb(10,10,10 )").toRgb()).toMatchObject({ r: 10, g: 10, b: 10, a: 1 }); 71 | expect(colord(" Rgb(10, 10, 10) ").toRgb()).toMatchObject({ r: 10, g: 10, b: 10, a: 1 }); 72 | expect(colord(" hSl(10,20%,30%,0.1)").toHsl()).toMatchObject({ h: 10, s: 20, l: 30, a: 0.1 }); 73 | expect(colord("HsLa( 10, 20%, 30%, 1) ").toHsl()).toMatchObject({ h: 10, s: 20, l: 30, a: 1 }); 74 | }); 75 | 76 | it("Parses shorthand alpha values", () => { 77 | expect(colord("rgba(0, 0, 0, .5)").alpha()).toBe(0.5); 78 | expect(colord("rgba(50% 50% 50% / .999%)").alpha()).toBe(0.01); 79 | expect(colord("hsla(0, 0%, 0%, .25)").alpha()).toBe(0.25); 80 | }); 81 | 82 | it("Ignores invalid color formats", () => { 83 | // mixing prefix 84 | expect(colord("AbC").isValid()).toBe(false); 85 | expect(colord("111").isValid()).toBe(false); 86 | expect(colord("999999").isValid()).toBe(false); 87 | // no bracket 88 | expect(colord("rgb 10 10 10)").isValid()).toBe(false); 89 | expect(colord("rgb(10 10 10").isValid()).toBe(false); 90 | // missing commas 91 | expect(colord("rgb( 10 10 10 0.1 )").isValid()).toBe(false); 92 | expect(colord("hsl(10, 20 30)").isValid()).toBe(false); 93 | // mixing numbers and percentage 94 | expect(colord("rgb(100, 100%, 20)").isValid()).toBe(false); 95 | // mixing commas and slash 96 | expect(colord("rgba(10, 50, 30 / .5").isValid()).toBe(false); 97 | expect(colord("hsla(10, 20, 30/50%)").isValid()).toBe(false); 98 | // missing percent 99 | expect(colord("hsl(10deg, 50, 50)").isValid()).toBe(false); 100 | // wrong content 101 | expect(colord("rgb(10, 10, 10, var(--alpha))").isValid()).toBe(false); 102 | expect(colord("hsl(var(--h) 10% 10%)").isValid()).toBe(false); 103 | }); 104 | 105 | it("Clamps input numbers", () => { 106 | expect(colord("rgba(256, 999, -200, 2)").toRgb()).toMatchObject({ r: 255, g: 255, b: 0, a: 1 }); 107 | expect( 108 | colord({ 109 | r: NaN, 110 | g: -Infinity, 111 | b: +Infinity, 112 | a: 100500, 113 | }).toRgb() 114 | ).toMatchObject({ r: 0, g: 0, b: 255, a: 1 }); 115 | expect( 116 | colord({ 117 | h: NaN, 118 | s: -Infinity, 119 | l: +Infinity, 120 | a: 100500, 121 | }).toHsl() 122 | ).toMatchObject({ h: 0, s: 0, l: 100, a: 1 }); 123 | }); 124 | 125 | it("Clamps hue (angle) value properly", () => { 126 | expect(colord("hsl(361, 50%, 50%)").toHsl().h).toBe(1); 127 | expect(colord("hsl(-1, 50%, 50%)").toHsl().h).toBe(359); 128 | expect(colord({ h: 999, s: 50, l: 50 }).toHsl().h).toBe(279); 129 | expect(colord({ h: -999, s: 50, l: 50 }).toHsl().h).toBe(81); 130 | expect(colord({ h: 400, s: 50, v: 50 }).toHsv().h).toBe(40); 131 | expect(colord({ h: -400, s: 50, v: 50 }).toHsv().h).toBe(320); 132 | }); 133 | 134 | it("Supports all valid CSS angle units", () => { 135 | // https://developer.mozilla.org/en-US/docs/Web/CSS/angle#examples 136 | expect(colord("hsl(90deg, 50%, 50%)").toHsl().h).toBe(90); 137 | expect(colord("hsl(100grad, 50%, 50%)").toHsl().h).toBe(90); 138 | expect(colord("hsl(.25turn, 50%, 50%)").toHsl().h).toBe(90); 139 | expect(colord("hsl(1.5708rad, 50%, 50%)").toHsl().h).toBe(90); 140 | expect(colord("hsl(-180deg, 50%, 50%)").toHsl().h).toBe(180); 141 | expect(colord("hsl(-200grad, 50%, 50%)").toHsl().h).toBe(180); 142 | expect(colord("hsl(-.5turn, 50%, 50%)").toHsl().h).toBe(180); 143 | expect(colord("hsl(-3.1416rad, 50%, 50%)").toHsl().h).toBe(180); 144 | }); 145 | 146 | it("Accepts a colord instance as an input", () => { 147 | const instance = colord(lime.hex as string); 148 | expect(colord(instance).toRgb()).toMatchObject(lime.rgba); 149 | expect(colord(colord(instance)).toHsl()).toMatchObject(lime.hsla); 150 | }); 151 | 152 | it("Does not crash when input has an invalid type", () => { 153 | const fallbackRgba = { r: 0, g: 0, b: 0, a: 1 }; 154 | // @ts-ignore 155 | expect(colord().toRgb()).toMatchObject(fallbackRgba); 156 | // @ts-ignore 157 | expect(colord(null).toRgb()).toMatchObject(fallbackRgba); 158 | // @ts-ignore 159 | expect(colord(undefined).toRgb()).toMatchObject(fallbackRgba); 160 | // @ts-ignore 161 | expect(colord([1, 2, 3]).toRgb()).toMatchObject(fallbackRgba); 162 | }); 163 | 164 | it("Does not crash when input has an invalid format", () => { 165 | const fallbackRgba = { r: 0, g: 0, b: 0, a: 1 }; 166 | // @ts-ignore 167 | expect(colord({ w: 1, u: 2, t: 3 }).toRgb()).toMatchObject(fallbackRgba); 168 | expect(colord("WUT?").toRgb()).toMatchObject(fallbackRgba); 169 | }); 170 | 171 | it("Validates an input value", () => { 172 | expect(colord("#ffffff").isValid()).toBe(true); 173 | expect(colord("#0011gg").isValid()).toBe(false); 174 | expect(colord("#12345").isValid()).toBe(false); 175 | expect(colord("#1234567").isValid()).toBe(false); 176 | expect(colord("abracadabra").isValid()).toBe(false); 177 | expect(colord("rgba(0,0,0,1)").isValid()).toBe(true); 178 | expect(colord("hsla(100,50%,50%,1)").isValid()).toBe(true); 179 | expect(colord({ r: 255, g: 255, b: 255 }).isValid()).toBe(true); 180 | // @ts-ignore 181 | expect(colord({ r: 255, g: 255, v: 255 }).isValid()).toBe(false); 182 | // @ts-ignore 183 | expect(colord({ h: 0, w: 0, l: 0 }).isValid()).toBe(false); 184 | // @ts-ignore 185 | expect(colord({ w: 1, u: 2, t: 3 }).isValid()).toBe(false); 186 | }); 187 | 188 | it("Saturates and desaturates a color", () => { 189 | const instance = colord(saturationLevels[5]); 190 | expect(instance.saturate(0.2).toHex()).toBe(saturationLevels[7]); 191 | expect(instance.desaturate(0.2).toHex()).toBe(saturationLevels[3]); 192 | expect(instance.saturate(0.5).toHex()).toBe(saturationLevels[10]); 193 | expect(instance.desaturate(0.5).toHex()).toBe(saturationLevels[0]); 194 | expect(instance.saturate(1).toHex()).toBe(saturationLevels[10]); 195 | expect(instance.desaturate(1).toHex()).toBe(saturationLevels[0]); 196 | expect(instance.grayscale().toHex()).toBe(saturationLevels[0]); 197 | }); 198 | 199 | it("Makes a color lighter and darker", () => { 200 | expect(colord("hsl(100, 50%, 50%)").lighten().toHslString()).toBe("hsl(100, 50%, 60%)"); 201 | expect(colord("hsl(100, 50%, 50%)").lighten(0.25).toHsl().l).toBe(75); 202 | expect(colord("hsl(100, 50%, 50%)").darken().toHslString()).toBe("hsl(100, 50%, 40%)"); 203 | expect(colord("hsl(100, 50%, 50%)").darken(0.25).toHsl().l).toBe(25); 204 | 205 | expect(colord("#000").lighten(1).toHex()).toBe("#ffffff"); 206 | expect(colord("#000").lighten(0.5).toHex()).toBe("#808080"); 207 | expect(colord("#FFF").darken(1).toHex()).toBe("#000000"); 208 | expect(colord("#FFF").darken(0.5).toHex()).toBe("#808080"); 209 | }); 210 | 211 | it("Inverts a color", () => { 212 | expect(colord("#000").invert().toHex()).toBe("#ffffff"); 213 | expect(colord("#FFF").invert().toHex()).toBe("#000000"); 214 | expect(colord("#123").invert().toHex()).toBe("#eeddcc"); 215 | }); 216 | 217 | it("Gets color brightness", () => { 218 | expect(colord("#000").brightness()).toBe(0); 219 | expect(colord("#808080").brightness()).toBe(0.5); 220 | expect(colord("#FFF").brightness()).toBe(1); 221 | expect(colord("#000").isDark()).toBe(true); 222 | expect(colord("#665544").isDark()).toBe(true); 223 | expect(colord("#888").isDark()).toBe(false); 224 | expect(colord("#777").isLight()).toBe(false); 225 | expect(colord("#aabbcc").isLight()).toBe(true); 226 | expect(colord("#FFF").isLight()).toBe(true); 227 | }); 228 | 229 | it("Gets an alpha channel value", () => { 230 | expect(colord("#000").alpha()).toBe(1); 231 | expect(colord("rgba(50, 100, 150, 0.5)").alpha()).toBe(0.5); 232 | }); 233 | 234 | it("Changes an alpha channel value", () => { 235 | expect(colord("#000").alpha(0.25).alpha()).toBe(0.25); 236 | expect(colord("#FFF").alpha(0).toRgb().a).toBe(0); 237 | }); 238 | 239 | it("Produces alpha values with up to 3 digits after the decimal point", () => { 240 | expect(colord("#000").alpha(0.9).alpha()).toBe(0.9); 241 | expect(colord("#000").alpha(0.01).alpha()).toBe(0.01); 242 | expect(colord("#000").alpha(0.33333333).alpha()).toBe(0.333); 243 | expect(colord("rgba(0, 0, 0, 0.075)").toRgbString()).toBe("rgba(0, 0, 0, 0.075)"); 244 | expect(colord("hsla(0, 0%, 0%, 0.789)").toHslString()).toBe("hsla(0, 0%, 0%, 0.789)"); 245 | expect(colord("hsla(0, 0%, 0%, 0.999)").toRgbString()).toBe("rgba(0, 0, 0, 0.999)"); 246 | }); 247 | 248 | it("Gets a hue value", () => { 249 | expect(colord("#000").hue()).toBe(0); 250 | expect(colord("hsl(90, 50%, 50%)").hue()).toBe(90); 251 | expect(colord("hsl(-10, 50%, 50%)").hue()).toBe(350); 252 | }); 253 | 254 | it("Changes a hue value", () => { 255 | expect(colord("hsl(90, 50%, 50%)").hue(0).toHslString()).toBe("hsl(0, 50%, 50%)"); 256 | expect(colord("hsl(90, 50%, 50%)").hue(180).toHslString()).toBe("hsl(180, 50%, 50%)"); 257 | expect(colord("hsl(90, 50%, 50%)").hue(370).toHslString()).toBe("hsl(10, 50%, 50%)"); 258 | }); 259 | 260 | it("Rotates a hue circle", () => { 261 | expect(colord("hsl(90, 50%, 50%)").rotate(0).toHslString()).toBe("hsl(90, 50%, 50%)"); 262 | expect(colord("hsl(90, 50%, 50%)").rotate(360).toHslString()).toBe("hsl(90, 50%, 50%)"); 263 | expect(colord("hsl(90, 50%, 50%)").rotate(90).toHslString()).toBe("hsl(180, 50%, 50%)"); 264 | expect(colord("hsl(90, 50%, 50%)").rotate(-180).toHslString()).toBe("hsl(270, 50%, 50%)"); 265 | }); 266 | 267 | it("Checks colors for equality", () => { 268 | const otherColor = "#1ab2c3"; 269 | const otherInstance = colord(otherColor); 270 | for (const format in lime) { 271 | const instance = colord(lime[format] as AnyColor); 272 | expect(instance.isEqual(colord(lime["hex"] as AnyColor))).toBe(true); 273 | expect(instance.isEqual(colord(lime["rgb"] as AnyColor))).toBe(true); 274 | expect(instance.isEqual(colord(lime["rgbString"] as AnyColor))).toBe(true); 275 | expect(instance.isEqual(colord(lime["hsl"] as AnyColor))).toBe(true); 276 | expect(instance.isEqual(colord(lime["hslString"] as AnyColor))).toBe(true); 277 | expect(instance.isEqual(colord(lime["hsv"] as AnyColor))).toBe(true); 278 | expect(instance.isEqual(otherInstance)).toBe(false); 279 | expect(instance.isEqual(lime["hex"] as AnyColor)).toBe(true); 280 | expect(instance.isEqual(lime["rgb"] as AnyColor)).toBe(true); 281 | expect(instance.isEqual(lime["rgbString"] as AnyColor)).toBe(true); 282 | expect(instance.isEqual(lime["hsl"] as AnyColor)).toBe(true); 283 | expect(instance.isEqual(lime["hslString"] as AnyColor)).toBe(true); 284 | expect(instance.isEqual(lime["hsv"] as AnyColor)).toBe(true); 285 | expect(instance.isEqual(otherColor)).toBe(false); 286 | } 287 | }); 288 | 289 | it("Generates a random color", () => { 290 | expect(random()).toBeInstanceOf(Colord); 291 | expect(random().toHex()).not.toBe(random().toHex()); 292 | }); 293 | 294 | it("Gets an input color format", () => { 295 | expect(getFormat("#000")).toBe("hex"); 296 | expect(getFormat("rgb(128, 128, 128)")).toBe("rgb"); 297 | expect(getFormat("rgba(50% 50% 50% / 50%)")).toBe("rgb"); 298 | expect(getFormat("hsl(180, 50%, 50%)")).toBe("hsl"); 299 | expect(getFormat({ r: 128, g: 128, b: 128, a: 0.5 })).toBe("rgb"); 300 | expect(getFormat({ h: 180, s: 50, l: 50, a: 0.5 })).toBe("hsl"); 301 | expect(getFormat({ h: 180, s: 50, v: 50, a: 0.5 })).toBe("hsv"); 302 | expect(getFormat("disco-dancing")).toBeUndefined(); 303 | // @ts-ignore 304 | expect(getFormat({ w: 1, u: 2, t: 3 })).toBeUndefined(); 305 | }); 306 | -------------------------------------------------------------------------------- /tests/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { HslColor, HsvColor, Input, RgbColor } from "../src/types"; 2 | 3 | interface Fixture { 4 | hex: string; 5 | rgb: RgbColor; 6 | hsl: HslColor; 7 | hsv: HsvColor; 8 | } 9 | 10 | // https://www.w3schools.com/colors/colors_converter.asp 11 | // https://www.rapidtables.com/convert/color/rgb-to-hsv.html 12 | export const fixtures: Fixture[] = [ 13 | { 14 | hex: "#000000", 15 | rgb: { r: 0, g: 0, b: 0 }, 16 | hsl: { h: 0, s: 0, l: 0 }, 17 | hsv: { h: 0, s: 0, v: 0 }, 18 | }, 19 | { 20 | hex: "#ffffff", 21 | rgb: { r: 255, g: 255, b: 255 }, 22 | hsl: { h: 0, s: 0, l: 100 }, 23 | hsv: { h: 0, s: 0, v: 100 }, 24 | }, 25 | { 26 | hex: "#ff0000", 27 | rgb: { r: 255, g: 0, b: 0 }, 28 | hsl: { h: 0, s: 100, l: 50 }, 29 | hsv: { h: 0, s: 100, v: 100 }, 30 | }, 31 | { 32 | hex: "#ff00ff", 33 | rgb: { r: 255, g: 0, b: 255 }, 34 | hsl: { h: 300, s: 100, l: 50 }, 35 | hsv: { h: 300, s: 100, v: 100 }, 36 | }, 37 | { 38 | hex: "#808080", 39 | rgb: { r: 128, g: 128, b: 128 }, 40 | hsl: { h: 0, s: 0, l: 50 }, 41 | hsv: { h: 0, s: 0, v: 50 }, 42 | }, 43 | { 44 | hex: "#76a800", 45 | rgb: { r: 118, g: 168, b: 0 }, 46 | hsl: { h: 78, s: 100, l: 33 }, 47 | hsv: { h: 78, s: 100, v: 66 }, 48 | }, 49 | { 50 | hex: "#6699cc", 51 | rgb: { r: 102, g: 153, b: 204 }, 52 | hsl: { h: 210, s: 50, l: 60 }, 53 | hsv: { h: 210, s: 50, v: 80 }, 54 | }, 55 | ]; 56 | 57 | interface TestColor { 58 | [key: string]: Input; 59 | } 60 | 61 | export const lime: TestColor = { 62 | shorthandHex: "#0F0", 63 | hex: "#00ff00", 64 | hex4: "#0F0F", 65 | hex8: "#00ff00ff", 66 | rgb: { r: 0, g: 255, b: 0 }, 67 | rgbString: "rgb(0, 255, 0)", 68 | rgbStringNoSpaces: "rgb(0,255,0)", 69 | rgba: { r: 0, g: 255, b: 0, a: 1 }, 70 | rgbaString: "rgba(0, 255, 0, 1)", 71 | hsl: { h: 120, s: 100, l: 50 }, 72 | hslString: "hsl(120, 100%, 50%)", 73 | hsla: { h: 120, s: 100, l: 50, a: 1 }, 74 | hslaString: "hsla(120, 100%, 50%, 1)", 75 | hsv: { h: 120, s: 100, v: 100 }, 76 | hsva: { h: 120, s: 100, v: 100, a: 1 }, 77 | }; 78 | 79 | export const saturationLevels = [ 80 | "#808080", 81 | "#79738c", 82 | "#736699", 83 | "#6d5aa6", 84 | "#664db3", 85 | "#6040bf", 86 | "#5933cc", 87 | "#5327d9", 88 | "#4d19e6", 89 | "#460df2", 90 | "#4000ff", 91 | ]; 92 | -------------------------------------------------------------------------------- /tests/plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { colord, getFormat, extend, Colord } from "../src/"; 2 | import a11yPlugin from "../src/plugins/a11y"; 3 | import cmykPlugin from "../src/plugins/cmyk"; 4 | import harmoniesPlugin, { HarmonyType } from "../src/plugins/harmonies"; 5 | import hwbPlugin from "../src/plugins/hwb"; 6 | import labPlugin from "../src/plugins/lab"; 7 | import lchPlugin from "../src/plugins/lch"; 8 | import minifyPlugin from "../src/plugins/minify"; 9 | import mixPlugin from "../src/plugins/mix"; 10 | import namesPlugin from "../src/plugins/names"; 11 | import xyzPlugin from "../src/plugins/xyz"; 12 | 13 | describe("a11y", () => { 14 | extend([a11yPlugin]); 15 | 16 | it("Returns the perceived luminance of a color", () => { 17 | expect(colord("#000000").luminance()).toBe(0); 18 | expect(colord("#e42189").luminance()).toBe(0.19); 19 | expect(colord("#ff0000").luminance()).toBe(0.21); 20 | expect(colord("#808080").luminance()).toBe(0.22); 21 | expect(colord("#aabbcc").luminance()).toBe(0.48); 22 | expect(colord("#ccddee").luminance()).toBe(0.71); 23 | expect(colord("#ffffff").luminance()).toBe(1); 24 | }); 25 | 26 | it("Calculates a contrast ratio for a color pair", () => { 27 | // https://webaim.org/resources/contrastchecker/ 28 | expect(colord("#000000").contrast()).toBe(21); 29 | expect(colord("#ffffff").contrast("#000000")).toBe(21); 30 | expect(colord("#777777").contrast()).toBe(4.47); 31 | expect(colord("#ff0000").contrast()).toBe(3.99); 32 | expect(colord("#00ff00").contrast()).toBe(1.37); 33 | expect(colord("#2e2e2e").contrast()).toBe(13.57); 34 | expect(colord("#0079ad").contrast()).toBe(4.84); 35 | expect(colord("#0079ad").contrast("#2e2e2e")).toBe(2.8); 36 | expect(colord("#e42189").contrast("#0d0330")).toBe(4.54); 37 | expect(colord("#fff4cc").contrast("#3a1209")).toBe(15); 38 | expect(colord("#fff4cc").contrast(colord("#3a1209"))).toBe(15); 39 | }); 40 | 41 | it("Check readability", () => { 42 | // https://webaim.org/resources/contrastchecker/ 43 | expect(colord("#000").isReadable()).toBe(true); 44 | expect(colord("#777777").isReadable()).toBe(false); 45 | expect(colord("#e60000").isReadable("#ffff47")).toBe(true); 46 | expect(colord("#af085c").isReadable("#000000")).toBe(false); 47 | expect(colord("#af085c").isReadable("#000000", { size: "large" })).toBe(true); 48 | expect(colord("#d53987").isReadable("#000000")).toBe(true); 49 | expect(colord("#d53987").isReadable("#000000", { level: "AAA" })).toBe(false); 50 | expect(colord("#e9dddd").isReadable("#864b7c", { level: "AA" })).toBe(true); 51 | expect(colord("#e9dddd").isReadable("#864b7c", { level: "AAA" })).toBe(false); 52 | expect(colord("#e9dddd").isReadable("#864b7c", { level: "AAA", size: "large" })).toBe(true); 53 | expect(colord("#e9dddd").isReadable("#67325e", { level: "AAA" })).toBe(true); 54 | expect(colord("#e9dddd").isReadable(colord("#67325e"), { level: "AAA" })).toBe(true); 55 | }); 56 | }); 57 | 58 | describe("cmyk", () => { 59 | extend([cmykPlugin]); 60 | 61 | it("Parses CMYK color object", () => { 62 | expect(colord({ c: 0, m: 0, y: 0, k: 100 }).toHex()).toBe("#000000"); 63 | expect(colord({ c: 16, m: 8, y: 0, k: 20, a: 1 }).toHex()).toBe("#abbccc"); 64 | expect(colord({ c: 51, m: 47, y: 0, k: 33, a: 0.5 }).toHex()).toBe("#545bab80"); 65 | expect(colord({ c: 0, m: 0, y: 0, k: 0, a: 1 }).toHex()).toBe("#ffffff"); 66 | }); 67 | 68 | it("Parses CMYK color string", () => { 69 | expect(colord("device-cmyk(0% 0% 0% 100%)").toHex()).toBe("#000000"); 70 | expect(colord("device-cmyk(0% 61% 72% 0% / 50%)").toHex()).toBe("#ff634780"); 71 | expect(colord("device-cmyk(0 0.61 0.72 0 / 0.5)").toHex()).toBe("#ff634780"); 72 | }); 73 | 74 | it("Converts a color to CMYK object", () => { 75 | // https://htmlcolors.com/color-converter 76 | expect(colord("#000000").toCmyk()).toMatchObject({ c: 0, m: 0, y: 0, k: 100, a: 1 }); 77 | expect(colord("#ff0000").toCmyk()).toMatchObject({ c: 0, m: 100, y: 100, k: 0, a: 1 }); 78 | expect(colord("#00ffff").toCmyk()).toMatchObject({ c: 100, m: 0, y: 0, k: 0, a: 1 }); 79 | expect(colord("#665533").toCmyk()).toMatchObject({ c: 0, m: 17, y: 50, k: 60, a: 1 }); 80 | expect(colord("#feacfa").toCmyk()).toMatchObject({ c: 0, m: 32, y: 2, k: 0, a: 1 }); 81 | expect(colord("#ffffff").toCmyk()).toMatchObject({ c: 0, m: 0, y: 0, k: 0, a: 1 }); 82 | }); 83 | 84 | it("Converts a color to CMYK string", () => { 85 | // https://en.wikipedia.org/wiki/CMYK_color_model 86 | expect(colord("#999966").toCmykString()).toBe("device-cmyk(0% 0% 33% 40%)"); 87 | expect(colord("#99ffff").toCmykString()).toBe("device-cmyk(40% 0% 0% 0%)"); 88 | expect(colord("#00336680").toCmykString()).toBe("device-cmyk(100% 50% 0% 60% / 0.5)"); 89 | }); 90 | 91 | it("Supported by `getFormat`", () => { 92 | expect(getFormat({ c: 0, m: 0, y: 0, k: 100 })).toBe("cmyk"); 93 | }); 94 | }); 95 | 96 | describe("harmonies", () => { 97 | extend([harmoniesPlugin]); 98 | 99 | const check = (type: HarmonyType | undefined, input: string, expected: string[]) => { 100 | const harmonies = colord(input).harmonies(type); 101 | const hexes = harmonies.map((value) => value.toHex()); 102 | return expect(hexes).toEqual(expected); 103 | }; 104 | 105 | it("Generates harmony colors", () => { 106 | check(undefined, "#ff0000", ["#ff0000", "#00ffff"]); // "complementary" 107 | check("analogous", "#ff0000", ["#ff0080", "#ff0000", "#ff8000"]); 108 | check("complementary", "#ff0000", ["#ff0000", "#00ffff"]); 109 | check("double-split-complementary", "#ff0000", [ 110 | "#ff0080", 111 | "#ff0000", 112 | "#ff8000", 113 | "#00ff80", 114 | "#0080ff", 115 | ]); 116 | check("rectangle", "#ff0000", ["#ff0000", "#ffff00", "#00ffff", "#0000ff"]); 117 | check("tetradic", "#ff0000", ["#ff0000", "#80ff00", "#00ffff", "#8000ff"]); 118 | check("triadic", "#ff0000", ["#ff0000", "#00ff00", "#0000ff"]); 119 | check("split-complementary", "#ff0000", ["#ff0000", "#00ff80", "#0080ff"]); 120 | }); 121 | }); 122 | 123 | describe("hwb", () => { 124 | extend([hwbPlugin]); 125 | 126 | it("Parses HWB color object", () => { 127 | expect(colord({ h: 0, w: 0, b: 100 }).toHex()).toBe("#000000"); 128 | expect(colord({ h: 210, w: 67, b: 20, a: 1 }).toHex()).toBe("#abbbcc"); 129 | expect(colord({ h: 236, w: 33, b: 33, a: 0.5 }).toHex()).toBe("#545aab80"); 130 | expect(colord({ h: 0, w: 100, b: 0, a: 1 }).toHex()).toBe("#ffffff"); 131 | }); 132 | 133 | it("Converts a color to HWB object", () => { 134 | // https://htmlcolors.com/color-converter 135 | expect(colord("#000000").toHwb()).toMatchObject({ h: 0, w: 0, b: 100, a: 1 }); 136 | expect(colord("#ff0000").toHwb()).toMatchObject({ h: 0, w: 0, b: 0, a: 1 }); 137 | expect(colord("#00ffff").toHwb()).toMatchObject({ h: 180, w: 0, b: 0, a: 1 }); 138 | expect(colord("#665533").toHwb()).toMatchObject({ h: 40, w: 20, b: 60, a: 1 }); 139 | expect(colord("#feacfa").toHwb()).toMatchObject({ h: 303, w: 67, b: 0, a: 1 }); 140 | expect(colord("#ffffff").toHwb()).toMatchObject({ h: 0, w: 100, b: 0, a: 1 }); 141 | }); 142 | 143 | it("Parses HWB color string", () => { 144 | // https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb() 145 | // https://en.wikipedia.org/wiki/HWB_color_model 146 | expect(colord("hwb(194 0% 0%)").toHex()).toBe("#00c3ff"); 147 | expect(colord("hwb(194 0% 0% / .5)").toHex()).toBe("#00c3ff80"); 148 | expect(colord("hwb(-90deg 40% 40% / 50%)").toHex()).toBe("#7f669980"); 149 | }); 150 | 151 | it("Ignores invalid syntax", () => { 152 | // comma syntax is not documented 153 | expect(colord("hwb(194, 0%, 0%, .5)").isValid()).toBe(false); 154 | // missing percents 155 | expect(colord("hwb(-90deg 40 40)").isValid()).toBe(false); 156 | }); 157 | 158 | it("Converts a color to HWB string", () => { 159 | // https://en.wikipedia.org/wiki/HWB_color_model 160 | expect(colord("#999966").toHwbString()).toBe("hwb(60 40% 40%)"); 161 | expect(colord("#99ffff").toHwbString()).toBe("hwb(180 60% 0%)"); 162 | expect(colord("#00336680").toHwbString()).toBe("hwb(210 0% 60% / 0.5)"); 163 | }); 164 | 165 | it("Supports all valid CSS angle units", () => { 166 | // https://developer.mozilla.org/en-US/docs/Web/CSS/angle 167 | expect(colord("hwb(90deg 20% 20%)").toHwb().h).toBe(90); 168 | expect(colord("hwb(100grad 20% 20%)").toHwb().h).toBe(90); 169 | expect(colord("hwb(1.25turn 20% 20%)").toHwb().h).toBe(90); 170 | expect(colord("hwb(1.5708rad 20% 20%)").toHwb().h).toBe(90); 171 | }); 172 | 173 | it("Supported by `getFormat`", () => { 174 | expect(getFormat("hwb(180deg 50% 50%)")).toBe("hwb"); 175 | expect(getFormat({ h: 0, w: 0, b: 100 })).toBe("hwb"); 176 | }); 177 | }); 178 | 179 | describe("lab", () => { 180 | extend([labPlugin]); 181 | 182 | it("Parses CIE LAB color object", () => { 183 | // https://cielab.xyz/colorconv/ 184 | expect(colord({ l: 100, a: 0, b: 0 }).toHex()).toBe("#ffffff"); 185 | expect(colord({ l: 0, a: 0, b: 0 }).toHex()).toBe("#000000"); 186 | expect(colord({ l: 54.29, a: 80.81, b: 69.89 }).toHex()).toBe("#ff0000"); 187 | expect(colord({ l: 15.05, a: 6.68, b: 14.59, alpha: 0.5 }).toHex()).toBe("#33221180"); 188 | expect(colord({ l: 50.93, a: 64.96, b: -6.38, alpha: 1 }).toHex()).toBe("#d53987"); 189 | }); 190 | 191 | it("Converts a color to CIE LAB object", () => { 192 | // https://cielab.xyz/colorconv/ 193 | expect(colord("#ffffff").toLab()).toMatchObject({ l: 100, a: 0, b: 0, alpha: 1 }); 194 | expect(colord("#00000000").toLab()).toMatchObject({ l: 0, a: 0, b: 0, alpha: 0 }); 195 | expect(colord("#ff0000").toLab()).toMatchObject({ l: 54.29, a: 80.81, b: 69.89, alpha: 1 }); 196 | expect(colord("#00ff00").toLab()).toMatchObject({ l: 87.82, a: -79.29, b: 80.99, alpha: 1 }); 197 | expect(colord("#ffff00").toLab()).toMatchObject({ l: 97.61, a: -15.75, b: 93.39, alpha: 1 }); 198 | expect(colord("#aabbcc").toLab()).toMatchObject({ l: 74.97, a: -3.4, b: -10.7, alpha: 1 }); 199 | expect(colord("#33221180").toLab()).toMatchObject({ l: 15.05, a: 6.68, b: 14.59, alpha: 0.5 }); 200 | expect(colord("#d53987").toLab()).toMatchObject({ l: 50.93, a: 64.96, b: -6.38, alpha: 1 }); 201 | }); 202 | 203 | it("Calculates the the perceived color difference", () => { 204 | /** 205 | * Test results: https://cielab.xyz/colordiff.php 206 | * 207 | * All tests done using RGB. 208 | * Inner state is RGB, it is discrete thus all model transformations become discrete 209 | * and some accuracy is lost. 210 | * 211 | * After migrating the state to XYZ or handling the rounding problem, tests using other color models should be added. 212 | */ 213 | expect(colord("#3296fa").delta("#197dc8")).toBe(0.099); 214 | expect(colord("#faf0c8").delta("#fff")).toBe(0.145); 215 | expect(colord("#afafaf").delta("#b4b4b4")).toBe(0.014); 216 | expect(colord("#000").delta("#fff")).toBe(1); 217 | expect(colord("#000").delta("#c8cdd7")).toBe(0.737); 218 | expect(colord("#c8cdd7").delta("#000")).toBe(0.737); 219 | expect(colord("#f4f4f4").delta("#fafafa")).toBe(0.012); 220 | expect(colord("#f4f4f4").delta("#f4f4f4")).toBe(0); 221 | }); 222 | 223 | it("Supported by `getFormat`", () => { 224 | expect(getFormat({ l: 50, a: 0, b: 0, alpha: 1 })).toBe("lab"); 225 | }); 226 | }); 227 | 228 | describe("lch", () => { 229 | extend([lchPlugin]); 230 | 231 | it("Parses CIE LCH color object", () => { 232 | // https://www.w3.org/TR/css-color-4/#specifying-lab-lch 233 | expect(colord({ l: 0, c: 0, h: 0, a: 0 }).toHex()).toBe("#00000000"); 234 | expect(colord({ l: 100, c: 0, h: 0 }).toHex()).toBe("#ffffff"); 235 | expect(colord({ l: 29.2345, c: 44.2, h: 27 }).toHex()).toBe("#7d2329"); 236 | expect(colord({ l: 52.2345, c: 72.2, h: 56.2 }).toHex()).toBe("#c65d06"); 237 | expect(colord({ l: 60.2345, c: 59.2, h: 95.2 }).toHex()).toBe("#9d9318"); 238 | expect(colord({ l: 62.2345, c: 59.2, h: 126.2 }).toHex()).toBe("#68a639"); 239 | expect(colord({ l: 67.5345, c: 42.5, h: 258.2, a: 0.5 }).toHex()).toBe("#62acef80"); 240 | }); 241 | 242 | it("Parses CIE LCH color string", () => { 243 | // https://cielab.xyz/colorconv/ 244 | // https://www.w3.org/TR/css-color-4/ 245 | expect(colord("lch(0% 0 0 / 0)").toHex()).toBe("#00000000"); 246 | expect(colord("lch(100% 0 0)").toHex()).toBe("#ffffff"); 247 | expect(colord("lch(52.2345% 72.2 56.2 / 1)").toHex()).toBe("#c65d06"); 248 | expect(colord("lch(37% 105 305)").toHex()).toBe("#6a27e7"); 249 | expect(colord("lch(56.2% 83.6 357.4 / 93%)").toHex()).toBe("#fe1091ed"); 250 | }); 251 | 252 | it("Converts a color to CIE LCH object", () => { 253 | // https://cielab.xyz/colorconv/ 254 | expect(colord("#00000000").toLch()).toMatchObject({ l: 0, c: 0, h: 0, a: 0 }); 255 | expect(colord("#ffffff").toLch()).toMatchObject({ l: 100, c: 0, h: 0, a: 1 }); 256 | expect(colord("#7d2329").toLch()).toMatchObject({ l: 29.16, c: 44.14, h: 26.48, a: 1 }); 257 | expect(colord("#c65d06").toLch()).toMatchObject({ l: 52.31, c: 72.21, h: 56.33, a: 1 }); 258 | expect(colord("#9d9318").toLch()).toMatchObject({ l: 60.31, c: 59.2, h: 95.46, a: 1 }); 259 | expect(colord("#68a639").toLch()).toMatchObject({ l: 62.22, c: 59.15, h: 126.15, a: 1 }); 260 | expect(colord("#62acef80").toLch()).toMatchObject({ l: 67.67, c: 42.18, h: 257.79, a: 0.5 }); 261 | }); 262 | 263 | it("Converts a color to CIE LCH string (CSS functional notation)", () => { 264 | // https://cielab.xyz/colorconv/ 265 | expect(colord("#00000080").toLchString()).toBe("lch(0% 0 0 / 0.5)"); 266 | expect(colord("#ffffff").toLchString()).toBe("lch(100% 0 0)"); 267 | expect(colord("#c65d06ed").toLchString()).toBe("lch(52.31% 72.21 56.33 / 0.93)"); 268 | expect(colord("#aabbcc").toLchString()).toBe("lch(74.97% 11.22 252.37)"); 269 | }); 270 | 271 | it("Supports all valid CSS angle units", () => { 272 | // https://developer.mozilla.org/en-US/docs/Web/CSS/angle 273 | expect(colord("lch(50% 50 90deg)").toLch().h).toBe(90); 274 | expect(colord("lch(50% 50 100grad)").toLch().h).toBe(90); 275 | expect(colord("lch(50% 50 0.25turn)").toLch().h).toBe(90); 276 | expect(colord("lch(50% 50 1.5708rad)").toLch().h).toBe(90); 277 | }); 278 | 279 | it("Supported by `getFormat`", () => { 280 | expect(getFormat("lch(50% 50 180deg)")).toBe("lch"); 281 | expect(getFormat({ l: 50, c: 50, h: 180 })).toBe("lch"); 282 | }); 283 | }); 284 | 285 | describe("minify", () => { 286 | extend([minifyPlugin, namesPlugin]); 287 | 288 | it("Minifies a color", () => { 289 | expect(colord("#000000").minify()).toBe("#000"); 290 | expect(colord("black").minify()).toBe("#000"); 291 | expect(colord("#112233").minify()).toBe("#123"); 292 | expect(colord("darkgray").minify()).toBe("#a9a9a9"); 293 | expect(colord("rgba(200,200,200,0.55)").minify()).toBe("hsla(0,0%,78%,.55)"); 294 | expect(colord("rgba(200,200,200,0.55)").minify({ hsl: false })).toBe("rgba(200,200,200,.55)"); 295 | }); 296 | 297 | it("Supports alpha hexes", () => { 298 | expect(colord("hsla(0, 100%, 50%, .5)").minify()).toBe("rgba(255,0,0,.5)"); 299 | expect(colord("hsla(0, 100%, 50%, .5)").minify({ alphaHex: true })).toBe("#ff000080"); 300 | expect(colord("rgba(0, 0, 255, 0.4)").minify({ alphaHex: true })).toBe("#00f6"); 301 | }); 302 | 303 | it("Performs lossless minification (handles alpha hex issues)", () => { 304 | expect(colord("rgba(0,0,0,.4)").minify({ alphaHex: true })).toBe("#0006"); 305 | expect(colord("rgba(0,0,0,.075)").minify({ alphaHex: true })).toBe("rgba(0,0,0,.075)"); 306 | expect(colord("hsla(0,0%,50%,.515)").minify({ alphaHex: true })).toBe("hsla(0,0%,50%,.515)"); 307 | }); 308 | 309 | it("Supports names", () => { 310 | expect(colord("#f00").minify({ name: true })).toBe("red"); 311 | expect(colord("#000080").minify({ name: true })).toBe("navy"); 312 | expect(colord("rgb(255,0,0)").minify({ name: true })).toBe("red"); 313 | expect(colord("hsl(0, 100%, 50%)").minify({ name: true })).toBe("red"); 314 | }); 315 | 316 | it("Supports `transparent` keyword", () => { 317 | expect(colord("rgba(0,0,0,0)").minify()).toBe("rgba(0,0,0,0)"); 318 | expect(colord("rgba(0,0,0,0.0)").minify({ name: true })).toBe("rgba(0,0,0,0)"); 319 | expect(colord("hsla(0,0%,0%,0)").minify({ transparent: true })).toBe("transparent"); 320 | expect(colord("rgba(0,0,0,0)").minify({ transparent: true })).toBe("transparent"); 321 | expect(colord("rgba(0,0,0,0)").minify({ transparent: true, alphaHex: true })).toBe("#0000"); 322 | }); 323 | }); 324 | 325 | describe("mix", () => { 326 | extend([mixPlugin]); 327 | 328 | it("Mixes two colors", () => { 329 | expect(colord("#000000").mix("#ffffff").toHex()).toBe("#777777"); 330 | expect(colord("#dc143c").mix("#000000").toHex()).toBe("#6a1b21"); 331 | expect(colord("#800080").mix("#dda0dd").toHex()).toBe("#af5cae"); 332 | expect(colord("#228b22").mix("#87cefa").toHex()).toBe("#60ac8f"); 333 | expect(colord("#cd853f").mix("#eee8aa", 0.6).toHex()).toBe("#e3c07e"); 334 | expect(colord("#483d8b").mix("#00bfff", 0.35).toHex()).toBe("#4969b2"); 335 | }); 336 | 337 | it("Returns the same color if ratio is 0 or 1", () => { 338 | expect(colord("#cd853f").mix("#ffffff", 0).toHex()).toBe("#cd853f"); 339 | expect(colord("#ffffff").mix("#cd853f", 1).toHex()).toBe("#cd853f"); 340 | }); 341 | 342 | it("Return the color if both values are equal", () => { 343 | expect(colord("#ffffff").mix("#ffffff").toHex()).toBe("#ffffff"); 344 | expect(colord("#000000").mix("#000000").toHex()).toBe("#000000"); 345 | }); 346 | 347 | const check = (colors: Colord[], expected: string[]) => { 348 | const hexes = colors.map((value) => value.toHex()); 349 | return expect(hexes).toEqual(expected); 350 | }; 351 | 352 | it("Generates a tints palette", () => { 353 | check(colord("#ff0000").tints(2), ["#ff0000", "#ffffff"]); 354 | check(colord("#ff0000").tints(3), ["#ff0000", "#ff9f80", "#ffffff"]); 355 | check(colord("#ff0000").tints(), ["#ff0000", "#ff6945", "#ff9f80", "#ffd0be", "#ffffff"]); 356 | expect(colord("#aabbcc").tints(499)).toHaveLength(499); 357 | }); 358 | 359 | it("Generates a shades palette", () => { 360 | check(colord("#ff0000").shades(2), ["#ff0000", "#000000"]); 361 | check(colord("#ff0000").shades(3), ["#ff0000", "#7a1b0b", "#000000"]); 362 | check(colord("#ff0000").shades(), ["#ff0000", "#ba1908", "#7a1b0b", "#3f1508", "#000000"]); 363 | expect(colord("#aabbcc").shades(333)).toHaveLength(333); 364 | }); 365 | 366 | it("Generates a tones palette", () => { 367 | check(colord("#ff0000").tones(2), ["#ff0000", "#808080"]); 368 | check(colord("#ff0000").tones(3), ["#ff0000", "#c86147", "#808080"]); 369 | check(colord("#ff0000").tones(), ["#ff0000", "#e54729", "#c86147", "#a87363", "#808080"]); 370 | expect(colord("#aabbcc").tones(987)).toHaveLength(987); 371 | }); 372 | }); 373 | 374 | describe("names", () => { 375 | extend([namesPlugin]); 376 | 377 | it("Parses valid CSS color names", () => { 378 | expect(colord("white").toHex()).toBe("#ffffff"); 379 | expect(colord("red").toHex()).toBe("#ff0000"); 380 | expect(colord("rebeccapurple").toHex()).toBe("#663399"); 381 | }); 382 | 383 | it("Ignores the case and extra whitespaces", () => { 384 | expect(colord("White ").toHex()).toBe("#ffffff"); 385 | expect(colord(" YELLOW").toHex()).toBe("#ffff00"); 386 | expect(colord(" REbeccapurpLE ").toHex()).toBe("#663399"); 387 | }); 388 | 389 | it("Converts a color to CSS name", () => { 390 | expect(colord("#F00").toName()).toBe("red"); 391 | expect(colord("#663399").toName()).toBe("rebeccapurple"); 392 | }); 393 | 394 | it("Gets the closest CSS color keyword", () => { 395 | expect(colord("#AAA").toName({ closest: true })).toBe("darkgray"); 396 | expect(colord("#fd0202").toName({ closest: true })).toBe("red"); 397 | expect(colord("#00008d").toName({ closest: true })).toBe("darkblue"); 398 | expect(colord("#fe0000").toName({ closest: true })).toBe("red"); 399 | expect(colord("#FFF").toName({ closest: true })).toBe("white"); 400 | }); 401 | 402 | it("Does not crash when name is not found", () => { 403 | expect(colord("#123456").toName()).toBe(undefined); 404 | expect(colord("myownpurple").toHex()).toBe("#000000"); 405 | }); 406 | 407 | it("Processes 'transparent' color properly", () => { 408 | expect(colord("transparent").alpha()).toBe(0); 409 | expect(colord("transparent").toHex()).toBe("#00000000"); 410 | expect(colord("rgba(0, 0, 0, 0)").toName()).toBe("transparent"); 411 | expect(colord("rgba(255, 255, 255, 0)").toName()).toBeUndefined(); 412 | }); 413 | 414 | it("Works properly in pair with the built-in validation", () => { 415 | expect(colord("transparent").isValid()).toBe(true); 416 | expect(colord("red").isValid()).toBe(true); 417 | expect(colord("yellow").isValid()).toBe(true); 418 | expect(colord("sunyellow").isValid()).toBe(false); 419 | }); 420 | 421 | it("Supported by `getFormat`", () => { 422 | expect(getFormat("transparent")).toBe("name"); 423 | expect(getFormat("yellow")).toBe("name"); 424 | }); 425 | }); 426 | 427 | describe("xyz", () => { 428 | extend([xyzPlugin]); 429 | 430 | it("Parses XYZ color object", () => { 431 | // https://www.nixsensor.com/free-color-converter/ 432 | expect(colord({ x: 0, y: 0, z: 0 }).toHex()).toBe("#000000"); 433 | expect(colord({ x: 50, y: 50, z: 50 }).toHex()).toBe("#beb9cf"); 434 | expect(colord({ x: 96.42, y: 100, z: 82.52, a: 1 }).toHex()).toBe("#ffffff"); 435 | }); 436 | 437 | it("Converts a color to CIE XYZ object", () => { 438 | // https://www.easyrgb.com/en/convert.php 439 | // https://cielab.xyz/colorconv/ 440 | expect(colord("#ffffff").toXyz()).toMatchObject({ x: 96.42, y: 100, z: 82.52, a: 1 }); 441 | expect(colord("#5cbf54").toXyz()).toMatchObject({ x: 26, y: 40.27, z: 11.54, a: 1 }); 442 | expect(colord("#00000000").toXyz()).toMatchObject({ x: 0, y: 0, z: 0, a: 0 }); 443 | }); 444 | 445 | it("Supported by `getFormat`", () => { 446 | expect(getFormat({ x: 50, y: 50, z: 50 })).toBe("xyz"); 447 | }); 448 | }); 449 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "strict": true, 9 | "outDir": "dist" 10 | }, 11 | "include": ["./src/**/*", "./test/**/*"] 12 | } 13 | --------------------------------------------------------------------------------