├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── index.test-d.ts ├── package.json └── test └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.coffee] 11 | indent_style = space 12 | 13 | [{package.json,*.yml}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 20 15 | - 18 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | package-lock = false 3 | update-notifier = false 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for taking the time to contribute to color.js. Follow these guidelines to make the process smoother: 2 | 3 | 1. One feature per pull request. Each PR should have one focus, and all the code changes should be supporting that one feature or bug fix. Using a [separate branch](https://guides.github.com/introduction/flow/index.html) for each feature should help you manage developing multiple features at once. 4 | 5 | 2. Follow the style of the file when it comes to syntax like curly braces and indents. 6 | 7 | 3. Add a test for the feature or fix, if possible. See the `test` directory for existing tests and README describing how to run these tests. 8 | 9 | 4. Run tests with `npm test` 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Heather Arthur 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # color 2 | 3 | > JavaScript library for immutable color conversion and manipulation with support for CSS color strings. 4 | 5 | ```js 6 | const color = Color('#7743CE').alpha(0.5).lighten(0.5); 7 | console.log(color.hsl().string()); // 'hsla(262, 59%, 81%, 0.5)' 8 | 9 | console.log(color.cmyk().round().array()); // [ 16, 25, 0, 8, 0.5 ] 10 | 11 | console.log(color.ansi256().object()); // { ansi256: 183, alpha: 0.5 } 12 | ``` 13 | 14 | ## Install 15 | ```shell 16 | npm install color 17 | ``` 18 | 19 | ## Usage 20 | ```js 21 | import Color from 'color'; 22 | ``` 23 | 24 | ### Constructors 25 | ```js 26 | // string constructor 27 | const color = Color('rgb(255, 255, 255)') // { model: 'rgb', color: [ 255, 255, 255 ], valpha: 1 } 28 | const color = Color('hsl(194, 53%, 79%)') // { model: 'hsl', color: [ 195, 53, 79 ], valpha: 1 } 29 | const color = Color('hsl(194, 53%, 79%, 0.5)') // { model: 'hsl', color: [ 195, 53, 79 ], valpha: 0.5 } 30 | const color = Color('#FF0000') // { model: 'rgb', color: [ 255, 0, 0 ], valpha: 1 } 31 | const color = Color('#FF000033') // { model: 'rgb', color: [ 255, 0, 0 ], valpha: 0.2 } 32 | const color = Color('lightblue') // { model: 'rgb', color: [ 173, 216, 230 ], valpha: 1 } 33 | const color = Color('purple') // { model: 'rgb', color: [ 128, 0, 128 ], valpha: 1 } 34 | 35 | // rgb 36 | const color = Color({r: 255, g: 255, b: 255}) // { model: 'rgb', color: [ 255, 255, 255 ], valpha: 1 } 37 | const color = Color({r: 255, g: 255, b: 255, alpha: 0.5}) // { model: 'rgb', color: [ 255, 255, 255 ], valpha: 0.5 } 38 | const color = Color.rgb(255, 255, 255) // { model: 'rgb', color: [ 255, 255, 255 ], valpha: 1 } 39 | const color = Color.rgb(255, 255, 255, 0.5) // { model: 'rgb', color: [ 255, 255, 255 ], valpha: 0.5 } 40 | const color = Color.rgb(0xFF, 0x00, 0x00, 0.5) // { model: 'rgb', color: [ 255, 0, 0 ], valpha: 0.5 } 41 | const color = Color.rgb([255, 255, 255]) // { model: 'rgb', color: [ 255, 255, 255 ], valpha: 1 } 42 | const color = Color.rgb([0xFF, 0x00, 0x00, 0.5]) // { model: 'rgb', color: [ 255, 0, 0 ], valpha: 0.5 } 43 | 44 | // hsl 45 | const color = Color({h: 194, s: 53, l: 79}) // { model: 'hsl', color: [ 195, 53, 79 ], valpha: 1 } 46 | const color = Color({h: 194, s: 53, l: 79, alpha: 0.5}) // { model: 'hsl', color: [ 195, 53, 79 ], valpha: 0.5 } 47 | const color = Color.hsl(194, 53, 79) // { model: 'hsl', color: [ 195, 53, 79 ], valpha: 1 } 48 | 49 | // hsv 50 | const color = Color({h: 195, s: 25, v: 99}) // { model: 'hsv', color: [ 195, 25, 99 ], valpha: 1 } 51 | const color = Color({h: 195, s: 25, v: 99, alpha: 0.5}) // { model: 'hsv', color: [ 195, 25, 99 ], valpha: 0.5 } 52 | const color = Color.hsv(195, 25, 99) // { model: 'hsv', color: [ 195, 25, 99 ], valpha: 1 } 53 | const color = Color.hsv([195, 25, 99]) // { model: 'hsv', color: [ 195, 25, 99 ], valpha: 1 } 54 | 55 | // cmyk 56 | const color = Color({c: 0, m: 100, y: 100, k: 0}) // { model: 'cmyk', color: [ 0, 100, 100, 0 ], valpha: 1 } 57 | const color = Color({c: 0, m: 100, y: 100, k: 0, alpha: 0.5}) // { model: 'cmyk', color: [ 0, 100, 100, 0 ], valpha: 0.5 } 58 | const color = Color.cmyk(0, 100, 100, 0) // { model: 'cmyk', color: [ 0, 100, 100, 0 ], valpha: 1 } 59 | const color = Color.cmyk(0, 100, 100, 0, 0.5) // { model: 'cmyk', color: [ 0, 100, 100, 0 ], valpha: 0.5 } 60 | 61 | // hwb 62 | const color = Color({h: 180, w: 0, b: 0}) // { model: 'hwb', color: [ 180, 0, 0 ], valpha: 1 } 63 | const color = Color.hwb(180, 0, 0) // { model: 'hwb', color: [ 180, 0, 0 ], valpha: 1 } 64 | 65 | // lch 66 | const color = Color({l: 53, c: 105, h: 40}) // { model: 'lch', color: [ 53, 105, 40 ], valpha: 1 } 67 | const color = Color.lch(53, 105, 40) // { model: 'lch', color: [ 53, 105, 40 ], valpha: 1 } 68 | 69 | // lab 70 | const color = Color({l: 53, a: 80, b: 67}) // { model: 'lab', color: [ 53, 80, 67 ], valpha: 1 } 71 | const color = Color.lab(53, 80, 67) // { model: 'lab', color: [ 53, 80, 67 ], valpha: 1 } 72 | 73 | // hcg 74 | const color = Color({h: 0, c: 100, g: 0}) // { model: 'hcg', color: [ 0, 100, 0 ], valpha: 1 } 75 | const color = Color.hcg(0, 100, 0) // { model: 'hcg', color: [ 0, 100, 0 ], valpha: 1 } 76 | 77 | // ansi16 78 | const color = Color.ansi16(91) // { model: 'ansi16', color: [ 91 ], valpha: 1 } 79 | const color = Color.ansi16(91, 0.5) // { model: 'ansi16', color: [ 91 ], valpha: 0.5 } 80 | 81 | // ansi256 82 | const color = Color.ansi256(196) // { model: 'ansi256', color: [ 196 ], valpha: 1 } 83 | const color = Color.ansi256(196, 0.5) // { model: 'ansi256', color: [ 196 ], valpha: 0.5 } 84 | 85 | // apple 86 | const color = Color.apple(65535, 65535, 65535) // { model: 'apple', color: [ 65535, 65535, 65535 ], valpha: 1 } 87 | const color = Color.apple([65535, 65535, 65535]) // { model: 'apple', color: [ 65535, 65535, 65535 ], valpha: 1 } 88 | 89 | 90 | ``` 91 | 92 | Set the values for individual channels with `alpha`, `red`, `green`, `blue`, `hue`, `saturationl` (hsl), `saturationv` (hsv), `lightness`, `whiteness`, `blackness`, `cyan`, `magenta`, `yellow`, `black` 93 | 94 | String constructors are handled by [color-string](https://www.npmjs.com/package/color-string) 95 | 96 | ### Getters 97 | ```js 98 | color.hsl() 99 | ``` 100 | Convert a color to a different space (`hsl()`, `cmyk()`, etc.). 101 | 102 | ```js 103 | color.object() // {r: 255, g: 255, b: 255} 104 | ``` 105 | Get a hash of the color value. Reflects the color's current model (see above). 106 | 107 | ```js 108 | color.rgb().array() // [255, 255, 255] 109 | ``` 110 | Get an array of the values with `array()`. Reflects the color's current model (see above). 111 | 112 | ```js 113 | color.rgbNumber() // 16777215 (0xffffff) 114 | ``` 115 | Get the rgb number value. 116 | 117 | ```js 118 | color.hex() // #ffffff 119 | ``` 120 | Get the hex value. (**NOTE:** `.hex()` does not return alpha values; use `.hexa()` for an RGBA representation) 121 | 122 | ```js 123 | color.red() // 255 124 | ``` 125 | Get the value for an individual channel. 126 | 127 | ### CSS Strings 128 | ```js 129 | color.hsl().string() // 'hsl(320, 50%, 100%)' 130 | ``` 131 | 132 | Calling `.string()` with a number rounds the numbers to that decimal place. It defaults to 1. 133 | 134 | ### Luminosity 135 | ```js 136 | color.luminosity(); // 0.412 137 | ``` 138 | The [WCAG luminosity](http://www.w3.org/TR/WCAG20/#relativeluminancedef) of the color. 0 is black, 1 is white. 139 | 140 | ```js 141 | color.contrast(Color("blue")) // 12 142 | ``` 143 | The [WCAG contrast ratio](http://www.w3.org/TR/WCAG20/#contrast-ratiodef) to another color, from 1 (same color) to 21 (contrast b/w white and black). 144 | 145 | ```js 146 | color.isLight() // true 147 | color.isDark() // false 148 | ``` 149 | Get whether the color is "light" or "dark", useful for deciding text color. 150 | 151 | ### Manipulation 152 | ```js 153 | color.negate() // rgb(0, 100, 255) -> rgb(255, 155, 0) 154 | 155 | color.lighten(0.5) // hsl(100, 50%, 50%) -> hsl(100, 50%, 75%) 156 | color.lighten(0.5) // hsl(100, 50%, 0) -> hsl(100, 50%, 0) 157 | color.darken(0.5) // hsl(100, 50%, 50%) -> hsl(100, 50%, 25%) 158 | color.darken(0.5) // hsl(100, 50%, 0) -> hsl(100, 50%, 0) 159 | 160 | color.lightness(50) // hsl(100, 50%, 10%) -> hsl(100, 50%, 50%) 161 | 162 | color.saturate(0.5) // hsl(100, 50%, 50%) -> hsl(100, 75%, 50%) 163 | color.desaturate(0.5) // hsl(100, 50%, 50%) -> hsl(100, 25%, 50%) 164 | color.grayscale() // #5CBF54 -> #969696 165 | 166 | color.whiten(0.5) // hwb(100, 50%, 50%) -> hwb(100, 75%, 50%) 167 | color.blacken(0.5) // hwb(100, 50%, 50%) -> hwb(100, 50%, 75%) 168 | 169 | color.fade(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 0.4) 170 | color.opaquer(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 1.0) 171 | 172 | color.rotate(180) // hsl(60, 20%, 20%) -> hsl(240, 20%, 20%) 173 | color.rotate(-90) // hsl(60, 20%, 20%) -> hsl(330, 20%, 20%) 174 | 175 | color.mix(Color("yellow")) // cyan -> rgb(128, 255, 128) 176 | color.mix(Color("yellow"), 0.3) // cyan -> rgb(77, 255, 179) 177 | 178 | // chaining 179 | color.green(100).grayscale().lighten(0.6) 180 | ``` 181 | 182 | ## Propers 183 | The API was inspired by [color-js](https://github.com/brehaut/color-js). Manipulation functions by CSS tools like Sass, LESS, and Stylus. 184 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type convert from 'color-convert'; 2 | 3 | export type ColorLike = ColorInstance | string | ArrayLike | number | Record; 4 | export type ColorJson = {model: string; color: number[]; valpha: number}; 5 | export type ColorObject = {alpha?: number | undefined} & Record; 6 | 7 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 8 | export interface ColorInstance { 9 | toString(): string; 10 | // eslint-disable-next-line @typescript-eslint/naming-convention 11 | toJSON(): ColorJson; 12 | string(places?: number): string; 13 | percentString(places?: number): string; 14 | array(): number[]; 15 | object(): ColorObject; 16 | unitArray(): number[]; 17 | unitObject(): {r: number; g: number; b: number; alpha?: number | undefined}; 18 | round(places?: number): ColorInstance; 19 | alpha(): number; 20 | alpha(value: number): ColorInstance; 21 | red(): number; 22 | red(value: number): ColorInstance; 23 | green(): number; 24 | green(value: number): ColorInstance; 25 | blue(): number; 26 | blue(value: number): ColorInstance; 27 | hue(): number; 28 | hue(value: number): ColorInstance; 29 | saturationl(): number; 30 | saturationl(value: number): ColorInstance; 31 | lightness(): number; 32 | lightness(value: number): ColorInstance; 33 | saturationv(): number; 34 | saturationv(value: number): ColorInstance; 35 | value(): number; 36 | value(value: number): ColorInstance; 37 | chroma(): number; 38 | chroma(value: number): ColorInstance; 39 | gray(): number; 40 | gray(value: number): ColorInstance; 41 | white(): number; 42 | white(value: number): ColorInstance; 43 | wblack(): number; 44 | wblack(value: number): ColorInstance; 45 | cyan(): number; 46 | cyan(value: number): ColorInstance; 47 | magenta(): number; 48 | magenta(value: number): ColorInstance; 49 | yellow(): number; 50 | yellow(value: number): ColorInstance; 51 | black(): number; 52 | black(value: number): ColorInstance; 53 | x(): number; 54 | x(value: number): ColorInstance; 55 | y(): number; 56 | y(value: number): ColorInstance; 57 | z(): number; 58 | z(value: number): ColorInstance; 59 | l(): number; 60 | l(value: number): ColorInstance; 61 | a(): number; 62 | a(value: number): ColorInstance; 63 | b(): number; 64 | b(value: number): ColorInstance; 65 | keyword(): string; 66 | keyword(value: V): ColorInstance; 67 | hex(): string; 68 | hex(value: V): ColorInstance; 69 | hexa(): string; 70 | hexa(value: V): ColorInstance; 71 | rgbNumber(): number; 72 | luminosity(): number; 73 | contrast(color2: ColorInstance): number; 74 | level(color2: ColorInstance): 'AAA' | 'AA' | ''; 75 | isDark(): boolean; 76 | isLight(): boolean; 77 | negate(): ColorInstance; 78 | lighten(ratio: number): ColorInstance; 79 | darken(ratio: number): ColorInstance; 80 | saturate(ratio: number): ColorInstance; 81 | desaturate(ratio: number): ColorInstance; 82 | whiten(ratio: number): ColorInstance; 83 | blacken(ratio: number): ColorInstance; 84 | grayscale(): ColorInstance; 85 | fade(ratio: number): ColorInstance; 86 | opaquer(ratio: number): ColorInstance; 87 | rotate(degrees: number): ColorInstance; 88 | mix(mixinColor: ColorInstance, weight?: number): ColorInstance; 89 | rgb(...arguments_: number[]): ColorInstance; 90 | hsl(...arguments_: number[]): ColorInstance; 91 | hsv(...arguments_: number[]): ColorInstance; 92 | hwb(...arguments_: number[]): ColorInstance; 93 | cmyk(...arguments_: number[]): ColorInstance; 94 | xyz(...arguments_: number[]): ColorInstance; 95 | lab(...arguments_: number[]): ColorInstance; 96 | lch(...arguments_: number[]): ColorInstance; 97 | ansi16(...arguments_: number[]): ColorInstance; 98 | ansi256(...arguments_: number[]): ColorInstance; 99 | hcg(...arguments_: number[]): ColorInstance; 100 | apple(...arguments_: number[]): ColorInstance; 101 | } 102 | 103 | export type ColorConstructor = { 104 | (object?: ColorLike, model?: keyof (typeof convert)): ColorInstance; 105 | new(object?: ColorLike, model?: keyof (typeof convert)): ColorInstance; 106 | rgb(...value: number[]): ColorInstance; 107 | rgb(color: ColorLike): ColorInstance; 108 | hsl(...value: number[]): ColorInstance; 109 | hsl(color: ColorLike): ColorInstance; 110 | hsv(...value: number[]): ColorInstance; 111 | hsv(color: ColorLike): ColorInstance; 112 | hwb(...value: number[]): ColorInstance; 113 | hwb(color: ColorLike): ColorInstance; 114 | cmyk(...value: number[]): ColorInstance; 115 | cmyk(color: ColorLike): ColorInstance; 116 | xyz(...value: number[]): ColorInstance; 117 | xyz(color: ColorLike): ColorInstance; 118 | lab(...value: number[]): ColorInstance; 119 | lab(color: ColorLike): ColorInstance; 120 | lch(...value: number[]): ColorInstance; 121 | lch(color: ColorLike): ColorInstance; 122 | ansi16(...value: number[]): ColorInstance; 123 | ansi16(color: ColorLike): ColorInstance; 124 | ansi256(...value: number[]): ColorInstance; 125 | ansi256(color: ColorLike): ColorInstance; 126 | hcg(...value: number[]): ColorInstance; 127 | hcg(color: ColorLike): ColorInstance; 128 | apple(...value: number[]): ColorInstance; 129 | apple(color: ColorLike): ColorInstance; 130 | }; 131 | 132 | // eslint-disable-next-line @typescript-eslint/naming-convention 133 | declare const Color: ColorConstructor; 134 | 135 | export default Color; 136 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import colorString from 'color-string'; 2 | import convert from 'color-convert'; 3 | 4 | const skippedModels = [ 5 | // To be honest, I don't really feel like keyword belongs in color convert, but eh. 6 | 'keyword', 7 | 8 | // Gray conflicts with some method names, and has its own method defined. 9 | 'gray', 10 | 11 | // Shouldn't really be in color-convert either... 12 | 'hex', 13 | ]; 14 | 15 | const hashedModelKeys = {}; 16 | for (const model of Object.keys(convert)) { 17 | hashedModelKeys[[...convert[model].labels].sort().join('')] = model; 18 | } 19 | 20 | const limiters = {}; 21 | 22 | function Color(object, model) { 23 | if (!(this instanceof Color)) { 24 | return new Color(object, model); 25 | } 26 | 27 | if (model && model in skippedModels) { 28 | model = null; 29 | } 30 | 31 | if (model && !(model in convert)) { 32 | throw new Error('Unknown model: ' + model); 33 | } 34 | 35 | let i; 36 | let channels; 37 | 38 | if (object == null) { // eslint-disable-line no-eq-null,eqeqeq 39 | this.model = 'rgb'; 40 | this.color = [0, 0, 0]; 41 | this.valpha = 1; 42 | } else if (object instanceof Color) { 43 | this.model = object.model; 44 | this.color = [...object.color]; 45 | this.valpha = object.valpha; 46 | } else if (typeof object === 'string') { 47 | const result = colorString.get(object); 48 | if (result === null) { 49 | throw new Error('Unable to parse color from string: ' + object); 50 | } 51 | 52 | this.model = result.model; 53 | channels = convert[this.model].channels; 54 | this.color = result.value.slice(0, channels); 55 | this.valpha = typeof result.value[channels] === 'number' ? result.value[channels] : 1; 56 | } else if (object.length > 0) { 57 | this.model = model || 'rgb'; 58 | channels = convert[this.model].channels; 59 | const newArray = Array.prototype.slice.call(object, 0, channels); 60 | this.color = zeroArray(newArray, channels); 61 | this.valpha = typeof object[channels] === 'number' ? object[channels] : 1; 62 | } else if (typeof object === 'number') { 63 | // This is always RGB - can be converted later on. 64 | this.model = 'rgb'; 65 | this.color = [ 66 | (object >> 16) & 0xFF, 67 | (object >> 8) & 0xFF, 68 | object & 0xFF, 69 | ]; 70 | this.valpha = 1; 71 | } else { 72 | this.valpha = 1; 73 | 74 | const keys = Object.keys(object); 75 | if ('alpha' in object) { 76 | keys.splice(keys.indexOf('alpha'), 1); 77 | this.valpha = typeof object.alpha === 'number' ? object.alpha : 0; 78 | } 79 | 80 | const hashedKeys = keys.sort().join(''); 81 | if (!(hashedKeys in hashedModelKeys)) { 82 | throw new Error('Unable to parse color from object: ' + JSON.stringify(object)); 83 | } 84 | 85 | this.model = hashedModelKeys[hashedKeys]; 86 | 87 | const {labels} = convert[this.model]; 88 | const color = []; 89 | for (i = 0; i < labels.length; i++) { 90 | color.push(object[labels[i]]); 91 | } 92 | 93 | this.color = zeroArray(color); 94 | } 95 | 96 | // Perform limitations (clamping, etc.) 97 | if (limiters[this.model]) { 98 | channels = convert[this.model].channels; 99 | for (i = 0; i < channels; i++) { 100 | const limit = limiters[this.model][i]; 101 | if (limit) { 102 | this.color[i] = limit(this.color[i]); 103 | } 104 | } 105 | } 106 | 107 | this.valpha = Math.max(0, Math.min(1, this.valpha)); 108 | 109 | if (Object.freeze) { 110 | Object.freeze(this); 111 | } 112 | } 113 | 114 | Color.prototype = { 115 | toString() { 116 | return this.string(); 117 | }, 118 | 119 | toJSON() { 120 | return this[this.model](); 121 | }, 122 | 123 | string(places) { 124 | let self = this.model in colorString.to ? this : this.rgb(); 125 | self = self.round(typeof places === 'number' ? places : 1); 126 | const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha]; 127 | return colorString.to[self.model](...arguments_); 128 | }, 129 | 130 | percentString(places) { 131 | const self = this.rgb().round(typeof places === 'number' ? places : 1); 132 | const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha]; 133 | return colorString.to.rgb.percent(...arguments_); 134 | }, 135 | 136 | array() { 137 | return this.valpha === 1 ? [...this.color] : [...this.color, this.valpha]; 138 | }, 139 | 140 | object() { 141 | const result = {}; 142 | const {channels} = convert[this.model]; 143 | const {labels} = convert[this.model]; 144 | 145 | for (let i = 0; i < channels; i++) { 146 | result[labels[i]] = this.color[i]; 147 | } 148 | 149 | if (this.valpha !== 1) { 150 | result.alpha = this.valpha; 151 | } 152 | 153 | return result; 154 | }, 155 | 156 | unitArray() { 157 | const rgb = this.rgb().color; 158 | rgb[0] /= 255; 159 | rgb[1] /= 255; 160 | rgb[2] /= 255; 161 | 162 | if (this.valpha !== 1) { 163 | rgb.push(this.valpha); 164 | } 165 | 166 | return rgb; 167 | }, 168 | 169 | unitObject() { 170 | const rgb = this.rgb().object(); 171 | rgb.r /= 255; 172 | rgb.g /= 255; 173 | rgb.b /= 255; 174 | 175 | if (this.valpha !== 1) { 176 | rgb.alpha = this.valpha; 177 | } 178 | 179 | return rgb; 180 | }, 181 | 182 | round(places) { 183 | places = Math.max(places || 0, 0); 184 | return new Color([...this.color.map(roundToPlace(places)), this.valpha], this.model); 185 | }, 186 | 187 | alpha(value) { 188 | if (value !== undefined) { 189 | return new Color([...this.color, Math.max(0, Math.min(1, value))], this.model); 190 | } 191 | 192 | return this.valpha; 193 | }, 194 | 195 | // Rgb 196 | red: getset('rgb', 0, maxfn(255)), 197 | green: getset('rgb', 1, maxfn(255)), 198 | blue: getset('rgb', 2, maxfn(255)), 199 | 200 | hue: getset(['hsl', 'hsv', 'hsl', 'hwb', 'hcg'], 0, value => ((value % 360) + 360) % 360), 201 | 202 | saturationl: getset('hsl', 1, maxfn(100)), 203 | lightness: getset('hsl', 2, maxfn(100)), 204 | 205 | saturationv: getset('hsv', 1, maxfn(100)), 206 | value: getset('hsv', 2, maxfn(100)), 207 | 208 | chroma: getset('hcg', 1, maxfn(100)), 209 | gray: getset('hcg', 2, maxfn(100)), 210 | 211 | white: getset('hwb', 1, maxfn(100)), 212 | wblack: getset('hwb', 2, maxfn(100)), 213 | 214 | cyan: getset('cmyk', 0, maxfn(100)), 215 | magenta: getset('cmyk', 1, maxfn(100)), 216 | yellow: getset('cmyk', 2, maxfn(100)), 217 | black: getset('cmyk', 3, maxfn(100)), 218 | 219 | x: getset('xyz', 0, maxfn(95.047)), 220 | y: getset('xyz', 1, maxfn(100)), 221 | z: getset('xyz', 2, maxfn(108.833)), 222 | 223 | l: getset('lab', 0, maxfn(100)), 224 | a: getset('lab', 1), 225 | b: getset('lab', 2), 226 | 227 | keyword(value) { 228 | if (value !== undefined) { 229 | return new Color(value); 230 | } 231 | 232 | return convert[this.model].keyword(this.color); 233 | }, 234 | 235 | hex(value) { 236 | if (value !== undefined) { 237 | return new Color(value); 238 | } 239 | 240 | return colorString.to.hex(...this.rgb().round().color); 241 | }, 242 | 243 | hexa(value) { 244 | if (value !== undefined) { 245 | return new Color(value); 246 | } 247 | 248 | const rgbArray = this.rgb().round().color; 249 | 250 | let alphaHex = Math.round(this.valpha * 255).toString(16).toUpperCase(); 251 | if (alphaHex.length === 1) { 252 | alphaHex = '0' + alphaHex; 253 | } 254 | 255 | return colorString.to.hex(...rgbArray) + alphaHex; 256 | }, 257 | 258 | rgbNumber() { 259 | const rgb = this.rgb().color; 260 | return ((rgb[0] & 0xFF) << 16) | ((rgb[1] & 0xFF) << 8) | (rgb[2] & 0xFF); 261 | }, 262 | 263 | luminosity() { 264 | // http://www.w3.org/TR/WCAG20/#relativeluminancedef 265 | const rgb = this.rgb().color; 266 | 267 | const lum = []; 268 | for (const [i, element] of rgb.entries()) { 269 | const chan = element / 255; 270 | lum[i] = (chan <= 0.04045) ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4; 271 | } 272 | 273 | return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; 274 | }, 275 | 276 | contrast(color2) { 277 | // http://www.w3.org/TR/WCAG20/#contrast-ratiodef 278 | const lum1 = this.luminosity(); 279 | const lum2 = color2.luminosity(); 280 | 281 | if (lum1 > lum2) { 282 | return (lum1 + 0.05) / (lum2 + 0.05); 283 | } 284 | 285 | return (lum2 + 0.05) / (lum1 + 0.05); 286 | }, 287 | 288 | level(color2) { 289 | // https://www.w3.org/TR/WCAG/#contrast-enhanced 290 | const contrastRatio = this.contrast(color2); 291 | if (contrastRatio >= 7) { 292 | return 'AAA'; 293 | } 294 | 295 | return (contrastRatio >= 4.5) ? 'AA' : ''; 296 | }, 297 | 298 | isDark() { 299 | // YIQ equation from http://24ways.org/2010/calculating-color-contrast 300 | const rgb = this.rgb().color; 301 | const yiq = (rgb[0] * 2126 + rgb[1] * 7152 + rgb[2] * 722) / 10000; 302 | return yiq < 128; 303 | }, 304 | 305 | isLight() { 306 | return !this.isDark(); 307 | }, 308 | 309 | negate() { 310 | const rgb = this.rgb(); 311 | for (let i = 0; i < 3; i++) { 312 | rgb.color[i] = 255 - rgb.color[i]; 313 | } 314 | 315 | return rgb; 316 | }, 317 | 318 | lighten(ratio) { 319 | const hsl = this.hsl(); 320 | hsl.color[2] += hsl.color[2] * ratio; 321 | return hsl; 322 | }, 323 | 324 | darken(ratio) { 325 | const hsl = this.hsl(); 326 | hsl.color[2] -= hsl.color[2] * ratio; 327 | return hsl; 328 | }, 329 | 330 | saturate(ratio) { 331 | const hsl = this.hsl(); 332 | hsl.color[1] += hsl.color[1] * ratio; 333 | return hsl; 334 | }, 335 | 336 | desaturate(ratio) { 337 | const hsl = this.hsl(); 338 | hsl.color[1] -= hsl.color[1] * ratio; 339 | return hsl; 340 | }, 341 | 342 | whiten(ratio) { 343 | const hwb = this.hwb(); 344 | hwb.color[1] += hwb.color[1] * ratio; 345 | return hwb; 346 | }, 347 | 348 | blacken(ratio) { 349 | const hwb = this.hwb(); 350 | hwb.color[2] += hwb.color[2] * ratio; 351 | return hwb; 352 | }, 353 | 354 | grayscale() { 355 | // http://en.wikipedia.org/wiki/Grayscale#Converting_colour_to_grayscale 356 | const rgb = this.rgb().color; 357 | const value = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; 358 | return Color.rgb(value, value, value); 359 | }, 360 | 361 | fade(ratio) { 362 | return this.alpha(this.valpha - (this.valpha * ratio)); 363 | }, 364 | 365 | opaquer(ratio) { 366 | return this.alpha(this.valpha + (this.valpha * ratio)); 367 | }, 368 | 369 | rotate(degrees) { 370 | const hsl = this.hsl(); 371 | let hue = hsl.color[0]; 372 | hue = (hue + degrees) % 360; 373 | hue = hue < 0 ? 360 + hue : hue; 374 | hsl.color[0] = hue; 375 | return hsl; 376 | }, 377 | 378 | mix(mixinColor, weight) { 379 | // Ported from sass implementation in C 380 | // https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209 381 | if (!mixinColor || !mixinColor.rgb) { 382 | throw new Error('Argument to "mix" was not a Color instance, but rather an instance of ' + typeof mixinColor); 383 | } 384 | 385 | const color1 = mixinColor.rgb(); 386 | const color2 = this.rgb(); 387 | const p = weight === undefined ? 0.5 : weight; 388 | 389 | const w = 2 * p - 1; 390 | const a = color1.alpha() - color2.alpha(); 391 | 392 | const w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2; 393 | const w2 = 1 - w1; 394 | 395 | return Color.rgb( 396 | w1 * color1.red() + w2 * color2.red(), 397 | w1 * color1.green() + w2 * color2.green(), 398 | w1 * color1.blue() + w2 * color2.blue(), 399 | color1.alpha() * p + color2.alpha() * (1 - p)); 400 | }, 401 | }; 402 | 403 | // Model conversion methods and static constructors 404 | for (const model of Object.keys(convert)) { 405 | if (skippedModels.includes(model)) { 406 | continue; 407 | } 408 | 409 | const {channels} = convert[model]; 410 | 411 | // Conversion methods 412 | Color.prototype[model] = function (...arguments_) { 413 | if (this.model === model) { 414 | return new Color(this); 415 | } 416 | 417 | if (arguments_.length > 0) { 418 | return new Color(arguments_, model); 419 | } 420 | 421 | return new Color([...assertArray(convert[this.model][model].raw(this.color)), this.valpha], model); 422 | }; 423 | 424 | // 'static' construction methods 425 | Color[model] = function (...arguments_) { 426 | let color = arguments_[0]; 427 | if (typeof color === 'number') { 428 | color = zeroArray(arguments_, channels); 429 | } 430 | 431 | return new Color(color, model); 432 | }; 433 | } 434 | 435 | function roundTo(number, places) { 436 | return Number(number.toFixed(places)); 437 | } 438 | 439 | function roundToPlace(places) { 440 | return function (number) { 441 | return roundTo(number, places); 442 | }; 443 | } 444 | 445 | function getset(model, channel, modifier) { 446 | model = Array.isArray(model) ? model : [model]; 447 | 448 | for (const m of model) { 449 | (limiters[m] ||= [])[channel] = modifier; 450 | } 451 | 452 | model = model[0]; 453 | 454 | return function (value) { 455 | let result; 456 | 457 | if (value !== undefined) { 458 | if (modifier) { 459 | value = modifier(value); 460 | } 461 | 462 | result = this[model](); 463 | result.color[channel] = value; 464 | return result; 465 | } 466 | 467 | result = this[model]().color[channel]; 468 | if (modifier) { 469 | result = modifier(result); 470 | } 471 | 472 | return result; 473 | }; 474 | } 475 | 476 | function maxfn(max) { 477 | return function (v) { 478 | return Math.max(0, Math.min(max, v)); 479 | }; 480 | } 481 | 482 | function assertArray(value) { 483 | return Array.isArray(value) ? value : [value]; 484 | } 485 | 486 | function zeroArray(array, length) { 487 | for (let i = 0; i < length; i++) { 488 | if (typeof array[i] !== 'number') { 489 | array[i] = 0; 490 | } 491 | } 492 | 493 | return array; 494 | } 495 | 496 | export default Color; 497 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import Color, { 3 | type ColorInstance, type ColorJson, type ColorObject, 4 | } from './index.js'; 5 | 6 | // String constructor 7 | expectType(Color('rgb(255, 255, 255)')); 8 | expectType(Color('hsl(194, 53%, 79%)')); 9 | expectType(Color('hsl(194, 53%, 79%, 0.5)')); 10 | expectType(Color('#FF0000')); 11 | expectType(Color('#FF000033')); 12 | expectType(Color('lightblue')); 13 | expectType(Color('purple')); 14 | // RGB 15 | expectType(Color({r: 255, g: 255, b: 255})); 16 | expectType(Color({ 17 | r: 255, g: 255, b: 255, alpha: 0.5, 18 | })); 19 | expectType(Color.rgb(255, 255, 255)); 20 | expectType(Color.rgb(255, 255, 255, 0.5)); 21 | expectType(Color.rgb(0xFF, 0x00, 0x00, 0.5)); 22 | expectType(Color.rgb([255, 255, 255])); 23 | expectType(Color.rgb([0xFF, 0x00, 0x00, 0.5])); 24 | // HSL 25 | expectType(Color({h: 194, s: 53, l: 79})); 26 | expectType(Color({ 27 | h: 194, s: 53, l: 79, alpha: 0.5, 28 | })); 29 | expectType(Color.hsl(194, 53, 79)); 30 | // HSV 31 | expectType(Color({h: 195, s: 25, v: 99})); 32 | expectType(Color({ 33 | h: 195, s: 25, v: 99, alpha: 0.5, 34 | })); 35 | expectType(Color.hsv(195, 25, 99)); 36 | expectType(Color.hsv([195, 25, 99])); 37 | // CMYK 38 | expectType(Color({ 39 | c: 0, m: 100, y: 100, k: 0, 40 | })); 41 | expectType(Color({ 42 | c: 0, m: 100, y: 100, k: 0, alpha: 0.5, 43 | })); 44 | expectType(Color.cmyk(0, 100, 100, 0)); 45 | expectType(Color.cmyk(0, 100, 100, 0, 0.5)); 46 | // Hwb 47 | expectType(Color({h: 180, w: 0, b: 0})); 48 | expectType(Color.hwb(180, 0, 0)); 49 | // Lch 50 | expectType(Color({l: 53, c: 105, h: 40})); 51 | expectType(Color.lch(53, 105, 40)); 52 | // Lab 53 | expectType(Color({l: 53, a: 80, b: 67})); 54 | expectType(Color.lab(53, 80, 67)); 55 | // Hcg 56 | expectType(Color({h: 0, c: 100, g: 0})); 57 | expectType(Color.hcg(0, 100, 0)); 58 | // Ansi16 59 | expectType(Color.ansi16(91)); 60 | expectType(Color.ansi16(91, 0.5)); 61 | // Ansi256 62 | expectType(Color.ansi256(196)); 63 | expectType(Color.ansi256(196, 0.5)); 64 | // Apple 65 | expectType(Color.apple(65535, 65535, 65535)); 66 | expectType(Color.apple([65535, 65535, 65535])); 67 | 68 | // Getters 69 | const color = Color('#00ccff'); 70 | expectType(color.hsl()); 71 | expectType(color.toJSON()); 72 | expectType(color.object()); 73 | expectType(color.rgb().array()); 74 | expectType(color.rgbNumber()); 75 | expectType(color.hex()); 76 | expectType(color.hex('#00ccff')); 77 | expectType(color.red()); 78 | expectType(color.red(255)); 79 | 80 | // CSS strings 81 | expectType(color.hsl().string()); 82 | 83 | // Luminosity 84 | expectType(color.luminosity()); 85 | expectType(color.contrast(Color('blue'))); 86 | expectType(color.isLight()); 87 | expectType(color.isDark()); 88 | 89 | // Manipulation 90 | expectType(color.negate()); 91 | expectType(color.lighten(0.5)); 92 | expectType(color.lighten(0.5)); 93 | expectType(color.darken(0.5)); 94 | expectType(color.darken(0.5)); 95 | expectType(color.lightness(50)); 96 | expectType(color.saturate(0.5)); 97 | expectType(color.desaturate(0.5)); 98 | expectType(color.grayscale()); 99 | expectType(color.whiten(0.5)); 100 | expectType(color.blacken(0.5)); 101 | expectType(color.fade(0.5)); 102 | expectType(color.opaquer(0.5)); 103 | expectType(color.rotate(180)); 104 | expectType(color.rotate(-90)); 105 | expectType(color.mix(Color('yellow'))); 106 | expectType(color.mix(Color('yellow'), 0.3)); 107 | 108 | // Chaining 109 | expectType(color.green(100).grayscale().lighten(0.6)); 110 | expectType(color.hsl().rgb().hex()); 111 | expectType(color.hsl().rgb().gray()); 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color", 3 | "version": "5.0.0", 4 | "description": "Color conversion and manipulation with CSS string support", 5 | "type": "module", 6 | "exports": "./index.js", 7 | "types": "./index.d.ts", 8 | "sideEffects": false, 9 | "keywords": [ 10 | "color", 11 | "colour", 12 | "css" 13 | ], 14 | "authors": [ 15 | "Josh Junon ", 16 | "Heather Arthur ", 17 | "Maxime Thirouin" 18 | ], 19 | "license": "MIT", 20 | "repository": "Qix-/color", 21 | "xo": { 22 | "rules": { 23 | "no-bitwise": 0, 24 | "no-cond-assign": 0, 25 | "new-cap": 0, 26 | "unicorn/prefer-module": 0, 27 | "no-mixed-operators": 0, 28 | "complexity": 0, 29 | "unicorn/numeric-separators-style": 0 30 | } 31 | }, 32 | "files": [ 33 | "LICENSE", 34 | "index.js", 35 | "index.d.ts" 36 | ], 37 | "scripts": { 38 | "test": "xo && tsd && mocha" 39 | }, 40 | "engines": { 41 | "node": ">=18" 42 | }, 43 | "dependencies": { 44 | "color-convert": "^3.0.1", 45 | "color-string": "^2.0.0" 46 | }, 47 | "devDependencies": { 48 | "mocha": "11.1.0", 49 | "tsd": "0.31.2", 50 | "xo": "0.60.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 3 | import assert from 'node:assert'; 4 | import Color from '../index.js'; 5 | 6 | const {deepEqual} = assert; 7 | const {equal} = assert; 8 | const {ok} = assert; 9 | const {notStrictEqual} = assert; 10 | const {throws} = assert; 11 | 12 | it('Color() instance', () => { 13 | equal(new Color('red').red(), 255); 14 | ok((new Color()) instanceof Color); 15 | const c = Color(); 16 | notStrictEqual(c.rgb(), c.rgb()); 17 | }); 18 | 19 | it('Color() instance (null)', () => { 20 | ok((new Color(null)) instanceof Color); 21 | }); 22 | 23 | it('Color() instance (undefined)', () => { 24 | ok((new Color(undefined)) instanceof Color); 25 | }); 26 | 27 | it('Immutability', () => { 28 | const c = Color(0xFF0000); 29 | ok(c !== c.rgb()); 30 | ok(c != c.rgb()); // eslint-disable-line eqeqeq 31 | }); 32 | 33 | it('Colors to JSON', () => { 34 | deepEqual(Color('#0A1E19').rgb().toJSON(), { 35 | color: [10, 30, 25], 36 | model: 'rgb', 37 | valpha: 1, 38 | }); 39 | deepEqual(Color('rgb(10, 30, 25)').rgb().toJSON(), { 40 | color: [10, 30, 25], 41 | model: 'rgb', 42 | valpha: 1, 43 | }); 44 | deepEqual(Color('rgba(10, 30, 25, 0.4)').rgb().toJSON(), { 45 | color: [10, 30, 25], 46 | model: 'rgb', 47 | valpha: 0.4, 48 | }); 49 | deepEqual(Color('rgb(4%, 12%, 10%)').rgb().toJSON(), { 50 | color: [10, 31, 26], 51 | model: 'rgb', 52 | valpha: 1, 53 | }); 54 | deepEqual(Color('rgba(4%, 12%, 10%, 0.4)').rgb().toJSON(), { 55 | color: [10, 31, 26], 56 | model: 'rgb', 57 | valpha: 0.4, 58 | }); 59 | deepEqual(Color('blue').rgb().toJSON(), { 60 | color: [0, 0, 255], 61 | model: 'rgb', 62 | valpha: 1, 63 | }); 64 | deepEqual(Color('hsl(120, 50%, 60%)').hsl().toJSON(), { 65 | color: [120, 50, 60], 66 | model: 'hsl', 67 | valpha: 1, 68 | }); 69 | deepEqual(Color('hsla(120, 50%, 60%, 0.4)').hsl().toJSON(), { 70 | color: [120, 50, 60], 71 | model: 'hsl', 72 | valpha: 0.4, 73 | }); 74 | deepEqual(Color('hwb(120, 50%, 60%)').hwb().toJSON(), { 75 | color: [120, 50, 60], 76 | model: 'hwb', 77 | valpha: 1, 78 | }); 79 | deepEqual(Color('hwb(120, 50%, 60%, 0.4)').hwb().toJSON(), { 80 | color: [120, 50, 60], 81 | model: 'hwb', 82 | valpha: 0.4, 83 | }); 84 | 85 | deepEqual(Color({ 86 | r: 10, 87 | g: 30, 88 | b: 25, 89 | }).rgb().toJSON(), { 90 | color: [10, 30, 25], 91 | model: 'rgb', 92 | valpha: 1, 93 | }); 94 | deepEqual(Color({ 95 | h: 10, 96 | s: 30, 97 | l: 25, 98 | }).hsl().toJSON(), { 99 | color: [10, 30, 25], 100 | model: 'hsl', 101 | valpha: 1, 102 | }); 103 | deepEqual(Color({ 104 | h: 10, 105 | s: 30, 106 | v: 25, 107 | }).hsv().toJSON(), { 108 | color: [10, 30, 25], 109 | model: 'hsv', 110 | valpha: 1, 111 | }); 112 | deepEqual(Color({ 113 | h: 10, 114 | w: 30, 115 | b: 25, 116 | }).hwb().toJSON(), { 117 | color: [10, 30, 25], 118 | model: 'hwb', 119 | valpha: 1, 120 | }); 121 | deepEqual(Color({ 122 | c: 10, 123 | m: 30, 124 | y: 25, 125 | k: 10, 126 | }).cmyk().toJSON(), { 127 | color: [10, 30, 25, 10], 128 | model: 'cmyk', 129 | valpha: 1, 130 | }); 131 | }); 132 | 133 | it('Color() argument', () => { 134 | deepEqual(Color('#0A1E19').rgb().object(), { 135 | r: 10, 136 | g: 30, 137 | b: 25, 138 | }); 139 | deepEqual(Color('rgb(10, 30, 25)').rgb().object(), { 140 | r: 10, 141 | g: 30, 142 | b: 25, 143 | }); 144 | deepEqual(Color('rgba(10, 30, 25, 0.4)').rgb().object(), { 145 | r: 10, 146 | g: 30, 147 | b: 25, 148 | alpha: 0.4, 149 | }); 150 | deepEqual(Color('rgb(4%, 12%, 10%)').rgb().object(), { 151 | r: 10, 152 | g: 31, 153 | b: 26, 154 | }); 155 | deepEqual(Color('rgba(4%, 12%, 10%, 0.4)').rgb().object(), { 156 | r: 10, 157 | g: 31, 158 | b: 26, 159 | alpha: 0.4, 160 | }); 161 | deepEqual(Color('blue').rgb().object(), { 162 | r: 0, 163 | g: 0, 164 | b: 255, 165 | }); 166 | deepEqual(Color('hsl(120, 50%, 60%)').hsl().object(), { 167 | h: 120, 168 | s: 50, 169 | l: 60, 170 | }); 171 | deepEqual(Color('hsla(120, 50%, 60%, 0.4)').hsl().object(), { 172 | h: 120, 173 | s: 50, 174 | l: 60, 175 | alpha: 0.4, 176 | }); 177 | deepEqual(Color('hwb(120, 50%, 60%)').hwb().object(), { 178 | h: 120, 179 | w: 50, 180 | b: 60, 181 | }); 182 | deepEqual(Color('hwb(120, 50%, 60%, 0.4)').hwb().object(), { 183 | h: 120, 184 | w: 50, 185 | b: 60, 186 | alpha: 0.4, 187 | }); 188 | 189 | deepEqual(Color({ 190 | r: 10, 191 | g: 30, 192 | b: 25, 193 | }).rgb().object(), { 194 | r: 10, 195 | g: 30, 196 | b: 25, 197 | }); 198 | deepEqual(Color({ 199 | h: 10, 200 | s: 30, 201 | l: 25, 202 | }).hsl().object(), { 203 | h: 10, 204 | s: 30, 205 | l: 25, 206 | }); 207 | deepEqual(Color({ 208 | h: 10, 209 | s: 30, 210 | v: 25, 211 | }).hsv().object(), { 212 | h: 10, 213 | s: 30, 214 | v: 25, 215 | }); 216 | deepEqual(Color({ 217 | h: 10, 218 | w: 30, 219 | b: 25, 220 | }).hwb().object(), { 221 | h: 10, 222 | w: 30, 223 | b: 25, 224 | }); 225 | deepEqual(Color({ 226 | c: 10, 227 | m: 30, 228 | y: 25, 229 | k: 10, 230 | }).cmyk().object(), { 231 | c: 10, 232 | m: 30, 233 | y: 25, 234 | k: 10, 235 | }); 236 | }); 237 | 238 | it('Setters', () => { 239 | deepEqual(Color.rgb(10, 30, 25).rgb().object(), { 240 | r: 10, 241 | g: 30, 242 | b: 25, 243 | }); 244 | deepEqual(Color.rgb(10, 30, 25, 0.4).rgb().object(), { 245 | r: 10, 246 | g: 30, 247 | b: 25, 248 | alpha: 0.4, 249 | }); 250 | deepEqual(Color.rgb([10, 30, 25]).rgb().object(), { 251 | r: 10, 252 | g: 30, 253 | b: 25, 254 | }); 255 | deepEqual(Color.rgb([10, 30, 25, 0.4]).rgb().object(), { 256 | r: 10, 257 | g: 30, 258 | b: 25, 259 | alpha: 0.4, 260 | }); 261 | deepEqual(Color.rgb({ 262 | r: 10, 263 | g: 30, 264 | b: 25, 265 | }).rgb().object(), { 266 | r: 10, 267 | g: 30, 268 | b: 25, 269 | }); 270 | deepEqual(Color.rgb({ 271 | r: 10, 272 | g: 30, 273 | b: 25, 274 | alpha: 0.4, 275 | }).rgb().object(), { 276 | r: 10, 277 | g: 30, 278 | b: 25, 279 | alpha: 0.4, 280 | }); 281 | 282 | deepEqual(Color.hsl([260, 10, 10]).hsl().object(), { 283 | h: 260, 284 | s: 10, 285 | l: 10, 286 | }); 287 | deepEqual(Color.hsv([260, 10, 10]).hsv().object(), { 288 | h: 260, 289 | s: 10, 290 | v: 10, 291 | }); 292 | deepEqual(Color.hwb([260, 10, 10]).hwb().object(), { 293 | h: 260, 294 | w: 10, 295 | b: 10, 296 | }); 297 | deepEqual(Color.cmyk([10, 10, 10, 10]).cmyk().object(), { 298 | c: 10, 299 | m: 10, 300 | y: 10, 301 | k: 10, 302 | }); 303 | }); 304 | 305 | it('Retain Alpha', () => { 306 | equal(Color.rgb(1, 2, 3, 0.4).ansi256().rgb().alpha(), 0.4); 307 | }); 308 | 309 | it('Translations', () => { 310 | deepEqual(Color.rgb(10, 30, 25).rgb().round().object(), { 311 | r: 10, 312 | g: 30, 313 | b: 25, 314 | }); 315 | deepEqual(Color.rgb(10, 30, 25).hsl().round().object(), { 316 | h: 165, 317 | s: 50, 318 | l: 8, 319 | }); 320 | deepEqual(Color.rgb(10, 30, 25).hsv().round().object(), { 321 | h: 165, 322 | s: 67, 323 | v: 12, 324 | }); 325 | deepEqual(Color.rgb(10, 30, 25).hwb().round().object(), { 326 | h: 165, 327 | w: 4, 328 | b: 88, 329 | }); 330 | deepEqual(Color.rgb(10, 30, 25).cmyk().round().object(), { 331 | c: 67, 332 | m: 0, 333 | y: 17, 334 | k: 88, 335 | }); 336 | }); 337 | 338 | it('Array getters', () => { 339 | deepEqual(Color({ 340 | r: 10, 341 | g: 20, 342 | b: 30, 343 | }).rgb().array(), [10, 20, 30]); 344 | deepEqual(Color({ 345 | r: 10, 346 | g: 20, 347 | b: 30, 348 | }).unitArray(), [10 / 255, 20 / 255, 30 / 255]); 349 | deepEqual(Color({ 350 | r: 10, 351 | g: 20, 352 | b: 30, 353 | alpha: 0.5, 354 | }).unitArray(), [10 / 255, 20 / 255, 30 / 255, 0.5]); 355 | deepEqual(Color({ 356 | h: 10, 357 | s: 20, 358 | l: 30, 359 | }).hsl().array(), [10, 20, 30]); 360 | deepEqual(Color({ 361 | h: 10, 362 | s: 20, 363 | v: 30, 364 | }).hsv().array(), [10, 20, 30]); 365 | deepEqual(Color({ 366 | h: 10, 367 | w: 20, 368 | b: 30, 369 | }).hwb().array(), [10, 20, 30]); 370 | deepEqual(Color({ 371 | c: 10, 372 | m: 20, 373 | y: 30, 374 | k: 40, 375 | }).cmyk().array(), [10, 20, 30, 40]); 376 | }); 377 | 378 | it('Multiple times', () => { 379 | const color = Color({ 380 | r: 10, 381 | g: 20, 382 | b: 30, 383 | }); 384 | deepEqual(color.rgb().array(), [10, 20, 30]); 385 | deepEqual(color.rgb().array(), [10, 20, 30]); 386 | }); 387 | 388 | it('Channel getters/setters', () => { 389 | equal(Color({ 390 | r: 10, 391 | g: 20, 392 | b: 30, 393 | alpha: 0.4, 394 | }).alpha(), 0.4); 395 | equal(Color({ 396 | r: 10, 397 | g: 20, 398 | b: 30, 399 | alpha: 0.4, 400 | }).alpha(0.7).alpha(), 0.7); 401 | equal(Color({ 402 | r: 10, 403 | g: 20, 404 | b: 30, 405 | }).red(), 10); 406 | equal(Color({ 407 | r: 10, 408 | g: 20, 409 | b: 30, 410 | }).red(100).red(), 100); 411 | equal(Color({ 412 | r: 10, 413 | g: 20, 414 | b: 30, 415 | }).green(), 20); 416 | equal(Color({ 417 | r: 10, 418 | g: 20, 419 | b: 30, 420 | }).green(200).green(), 200); 421 | equal(Color({ 422 | r: 10, 423 | g: 20, 424 | b: 30, 425 | }).blue(), 30); 426 | equal(Color({ 427 | r: 10, 428 | g: 20, 429 | b: 30, 430 | }).blue(60).blue(), 60); 431 | equal(Color({ 432 | h: 10, 433 | s: 20, 434 | l: 30, 435 | }).hue(), 10); 436 | equal(Color({ 437 | h: 10, 438 | s: 20, 439 | l: 30, 440 | }).hue(100).hue(), 100); 441 | equal(Color({ 442 | h: 10, 443 | w: 20, 444 | b: 30, 445 | }).hue(), 10); 446 | equal(Color({ 447 | h: 10, 448 | w: 20, 449 | b: 30, 450 | }).hue(100).hue(), 100); 451 | equal(Color({ 452 | h: 10, 453 | s: 20, 454 | l: 30, 455 | }).hue(), 10); 456 | equal(Color({ 457 | h: 10, 458 | s: 20, 459 | l: 30, 460 | }).hue(460).hue(), 100); 461 | equal(Color({ 462 | h: 10, 463 | w: 20, 464 | b: 30, 465 | }).hue(), 10); 466 | equal(Color({ 467 | h: 10, 468 | w: 20, 469 | b: 30, 470 | }).hue(-260).hue(), 100); 471 | }); 472 | 473 | it('Setting the same value', () => { 474 | const colorString = '#BADA55'; 475 | const color = Color(colorString); 476 | const alpha = color.alpha(); 477 | const red = color.red(); 478 | const green = color.green(); 479 | const blue = color.blue(); 480 | const hue = color.hue(); 481 | const saturation = color.saturationl(); 482 | const saturationv = color.saturationv(); 483 | const lightness = color.lightness(); 484 | const whiteness = color.white(); 485 | const blackness = color.wblack(); 486 | const cyan = color.cyan(); 487 | const magenta = color.magenta(); 488 | const yellow = color.yellow(); 489 | const black = color.black(); 490 | 491 | equal(color.hex(), colorString); 492 | 493 | color.alpha(alpha); 494 | equal(color.alpha(), alpha); 495 | equal(color.hex(), colorString); 496 | 497 | color.red(red); 498 | equal(color.red(), red); 499 | equal(color.hex(), colorString); 500 | 501 | color.green(green); 502 | equal(color.green(), green); 503 | equal(color.hex(), colorString); 504 | 505 | color.blue(blue); 506 | equal(color.blue(), blue); 507 | equal(color.hex(), colorString); 508 | 509 | color.hue(hue); 510 | equal(color.hue(), hue); 511 | equal(color.hex(), colorString); 512 | 513 | color.saturationl(saturation); 514 | equal(color.saturationl(), saturation); 515 | equal(color.hex(), colorString); 516 | 517 | color.saturationv(saturationv); 518 | equal(color.saturationv(), saturationv); 519 | equal(color.hex(), colorString); 520 | 521 | color.lightness(lightness); 522 | equal(color.lightness(), lightness); 523 | equal(color.hex(), colorString); 524 | 525 | color.white(whiteness); 526 | equal(color.white(), whiteness); 527 | equal(color.hex(), colorString); 528 | 529 | color.wblack(blackness); 530 | equal(color.wblack(), blackness); 531 | equal(color.hex(), colorString); 532 | 533 | color.cyan(cyan); 534 | equal(color.cyan(), cyan); 535 | equal(color.hex(), colorString); 536 | 537 | color.magenta(magenta); 538 | equal(color.magenta(), magenta); 539 | equal(color.hex(), colorString); 540 | 541 | color.yellow(yellow); 542 | equal(color.yellow(), yellow); 543 | equal(color.hex(), colorString); 544 | 545 | color.black(black); 546 | equal(color.black(), black); 547 | equal(color.hex(), colorString); 548 | }); 549 | 550 | it('Capping values', () => { 551 | equal(Color({ 552 | h: 400, 553 | s: 50, 554 | l: 10, 555 | }).hue(), 40); 556 | equal(Color({ 557 | h: 100, 558 | s: 50, 559 | l: 80, 560 | }).lighten(0.5).lightness(), 100); 561 | equal(Color({ 562 | h: -400, 563 | s: 50, 564 | l: 10, 565 | }).hue(), 320); 566 | 567 | // 0 == 360 568 | equal(Color({ 569 | h: 400, 570 | w: 50, 571 | b: 10, 572 | }).hue(), 40); 573 | equal(Color({ 574 | h: 100, 575 | w: 50, 576 | b: 80, 577 | }).blacken(0.5).wblack(), 100); 578 | equal(Color({ 579 | h: -400, 580 | w: 50, 581 | b: 10, 582 | }).hue(), 320); 583 | 584 | equal(Color().red(400).red(), 255); 585 | equal(Color().red(-400).red(), 0); 586 | equal(Color.rgb(10, 10, 10, 12).alpha(), 1); 587 | equal(Color.rgb(10, 10, 10, -200).alpha(), 0); 588 | equal(Color().alpha(-12).alpha(), 0); 589 | equal(Color().alpha(3).alpha(), 1); 590 | }); 591 | 592 | it('Translate with channel setters', () => { 593 | deepEqual(Color({ 594 | r: 0, 595 | g: 0, 596 | b: 0, 597 | }).lightness(50).hsl().object(), { 598 | h: 0, 599 | s: 0, 600 | l: 50, 601 | }); 602 | deepEqual(Color({ 603 | r: 0, 604 | g: 0, 605 | b: 0, 606 | }).red(50).green(50).hsv().round().object(), { 607 | h: 60, 608 | s: 100, 609 | v: 20, 610 | }); 611 | }); 612 | 613 | it('CSS String getters', () => { 614 | equal(Color('rgb(10, 30, 25)').hex(), '#0A1E19'); 615 | equal(Color('rgb(10, 30, 25, .5)').hex(), '#0A1E19'); 616 | equal(Color('rgb(10, 30, 25, 1)').hexa(), '#0A1E19FF'); 617 | equal(Color('rgb(10, 30, 25, 0.4)').hexa(), '#0A1E1966'); 618 | equal(Color('rgb(10, 30, 25, 0)').hexa(), '#0A1E1900'); 619 | equal(Color('rgb(10, 30, 25, 0.01)').hexa(), '#0A1E1903'); 620 | equal(Color('rgb(10, 30, 25)').rgb().string(), 'rgb(10, 30, 25)'); 621 | equal(Color('rgb(10, 30, 25, 0.4)').rgb().string(), 'rgba(10, 30, 25, 0.4)'); 622 | equal(Color('rgb(10, 30, 25)').percentString(), 'rgb(4%, 12%, 10%)'); 623 | equal(Color('rgb(10, 30, 25, 0.3)').percentString(), 'rgba(4%, 12%, 10%, 0.3)'); 624 | equal(Color('rgb(10, 30, 25)').hsl().string(), 'hsl(165, 50%, 7.8%)'); 625 | equal(Color('rgb(10, 30, 25, 0.3)').hsl().string(), 'hsla(165, 50%, 7.8%, 0.3)'); 626 | equal(Color({ 627 | h: 0, 628 | s: 0, 629 | v: 100, 630 | }).hsl().string(), 'hsl(0, 0%, 100%)'); 631 | equal(Color('rgb(10, 30, 25)').hwb().string(0), 'hwb(165, 4%, 88%)'); 632 | equal(Color('rgb(10, 30, 25, 0.3)').hwb().string(0), 'hwb(165, 4%, 88%, 0.3)'); 633 | equal(Color('rgb(0, 0, 255)').keyword(), 'blue'); 634 | equal(Color.rgb(155.5, 243.1555, 88.1999).string(), 'rgb(156, 243, 88)'); 635 | }); 636 | 637 | it('Number getters', () => { 638 | equal(Color('rgb(10, 30, 25)').rgbNumber(), 0xA_1E_19); 639 | }); 640 | 641 | it('luminosity, etc.', () => { 642 | equal(Color('white').luminosity(), 1); 643 | equal(Color('black').luminosity(), 0); 644 | equal(Color('red').luminosity(), 0.2126); 645 | equal(Color('white').contrast(Color('black')), 21); 646 | equal(Math.round(Color('white').contrast(Color('red'))), 4); 647 | equal(Math.round(Color('red').contrast(Color('white'))), 4); 648 | equal(Color('blue').contrast(Color('blue')), 1); 649 | ok(Color('black').isDark()); 650 | ok(!Color('black').isLight()); 651 | ok(Color('white').isLight()); 652 | ok(!Color('white').isDark()); 653 | ok(Color('blue').isDark()); 654 | ok(Color('darkgreen').isDark()); 655 | ok(Color('pink').isLight()); 656 | ok(Color('goldenrod').isLight()); 657 | ok(Color('red').isDark()); 658 | }); 659 | 660 | it('Manipulators wo/ mix', () => { 661 | deepEqual(Color({ 662 | r: 67, 663 | g: 122, 664 | b: 134, 665 | }).grayscale().rgb().round().object(), { 666 | r: 107, 667 | g: 107, 668 | b: 107, 669 | }); 670 | deepEqual(Color({ 671 | r: 67, 672 | g: 122, 673 | b: 134, 674 | }).negate().rgb().round().object(), { 675 | r: 188, 676 | g: 133, 677 | b: 121, 678 | }); 679 | equal(Color({ 680 | h: 100, 681 | s: 50, 682 | l: 60, 683 | }).lighten(0.5).lightness(), 90); 684 | equal(Color({ 685 | h: 100, 686 | s: 50, 687 | l: 60, 688 | }).darken(0.5).lightness(), 30); 689 | equal(Color({ 690 | h: 100, 691 | w: 50, 692 | b: 60, 693 | }).whiten(0.5).white(), 75); 694 | equal(Color({ 695 | h: 100, 696 | w: 50, 697 | b: 60, 698 | }).blacken(0.5).wblack(), 90); 699 | equal(Color({ 700 | h: 100, 701 | s: 40, 702 | l: 50, 703 | }).saturate(0.5).saturationl(), 60); 704 | equal(Color({ 705 | h: 100, 706 | s: 80, 707 | l: 60, 708 | }).desaturate(0.5).saturationl(), 40); 709 | equal(Color({ 710 | r: 10, 711 | g: 10, 712 | b: 10, 713 | alpha: 0.8, 714 | }).fade(0.5).alpha(), 0.4); 715 | equal(Color({ 716 | r: 10, 717 | g: 10, 718 | b: 10, 719 | alpha: 0.5, 720 | }).opaquer(0.5).alpha(), 0.75); 721 | equal(Color({ 722 | h: 60, 723 | s: 0, 724 | l: 0, 725 | }).rotate(180).hue(), 240); 726 | equal(Color({ 727 | h: 60, 728 | s: 0, 729 | l: 0, 730 | }).rotate(-180).hue(), 240); 731 | }); 732 | 733 | it('Mix: basic', () => { 734 | equal(Color('#f00').mix(Color('#00f')).hex(), '#800080'); 735 | }); 736 | 737 | it('Mix: weight', () => { 738 | equal(Color('#f00').mix(Color('#00f'), 0.25).hex(), '#BF0040'); 739 | }); 740 | 741 | it('Mix: alpha', () => { 742 | equal(Color('rgba(255, 0, 0, 0.5)').mix(Color('#00f')).rgb().string(0), 'rgba(64, 0, 191, 0.75)'); 743 | }); 744 | 745 | it('Mix: 0%', () => { 746 | equal(Color('#f00').mix(Color('#00f'), 0).hex(), '#FF0000'); 747 | }); 748 | 749 | it('Mix: 25%', () => { 750 | equal(Color('#f00').mix(Color('#00f'), 0.25).hex(), '#BF0040'); 751 | }); 752 | 753 | it('Mix: 50%', () => { 754 | equal(Color('#f00').mix(Color('#00f'), 0.5).hex(), '#800080'); 755 | }); 756 | 757 | it('Mix: 75%', () => { 758 | equal(Color('#f00').mix(Color('#00f'), 0.75).hex(), '#4000BF'); 759 | }); 760 | 761 | it('Mix: 100%', () => { 762 | equal(Color('#f00').mix(Color('#00f'), 1).hex(), '#0000FF'); 763 | }); 764 | 765 | it('Level', () => { 766 | equal(Color('white').level(Color('black')), 'AAA'); 767 | equal(Color('grey').level(Color('black')), 'AA'); 768 | }); 769 | 770 | it('Exceptions', () => { 771 | throws(() => { 772 | Color('unknow'); 773 | }, /Unable to parse color from string/); 774 | 775 | throws(() => { 776 | Color({}); 777 | }, /Unable to parse color from object/); 778 | 779 | throws(() => { 780 | Color(''); 781 | }, /Unable to parse color from string/); 782 | }); 783 | 784 | it('Should parse alphas in RGBA hex notation correctly', () => { 785 | // Tests for regression of #174 786 | notStrictEqual( 787 | Color('#000000ab').alpha(), 788 | Color('#000000aa').alpha(), 789 | ); 790 | }); 791 | --------------------------------------------------------------------------------