├── .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 |
6 |
7 |
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://bundlephobia.com/result?p=colord) | [](https://bundlephobia.com/result?p=colord) | [](https://bundlephobia.com/result?p=colord) | [](https://bundlephobia.com/result?p=colord) |
49 | | color | 744,263 | [](https://bundlephobia.com/result?p=color) | [](https://bundlephobia.com/result?p=color) | [](https://bundlephobia.com/result?p=color) | [](https://bundlephobia.com/result?p=color) |
50 | | tinycolor2 | 971,312 | [](https://bundlephobia.com/result?p=tinycolor2) | [](https://bundlephobia.com/result?p=tinycolor2) | [](https://bundlephobia.com/result?p=tinycolor2) | [](https://bundlephobia.com/result?p=tinycolor2) |
51 | | ac-colors | 660,722 | [](https://bundlephobia.com/result?p=ac-colors) | [](https://bundlephobia.com/result?p=ac-colors) | [](https://bundlephobia.com/result?p=ac-colors) | [](https://bundlephobia.com/result?p=ac-colors) |
52 | | chroma-js | 962,967 | [](https://bundlephobia.com/result?p=chroma-js) | [](https://bundlephobia.com/result?p=chroma-js) | [](https://bundlephobia.com/result?p=chroma-js) | [](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 |

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