├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── build.ts ├── demo ├── index.html ├── package.json ├── src │ └── main.ts └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── conversion.ts ├── css-color-names.ts ├── format-input.ts ├── from-ratio.ts ├── index.ts ├── interfaces.ts ├── public_api.ts ├── random.ts ├── readability.ts ├── to-ms-filter.ts ├── umd_api.ts └── util.ts ├── test ├── conversions.spec.ts ├── conversions.ts ├── index.spec.ts ├── modifications.ts └── random.spec.ts ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.module.json ├── vercel.json └── vitest.config.ts /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "50..100" 3 | status: 4 | project: no 5 | patch: no 6 | comment: 7 | require_changes: yes 8 | behavior: once 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | demo 3 | docs 4 | build.ts 5 | rollup.demo.js 6 | jest.config.js 7 | coverage 8 | .eslintrc.js 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": ["@ctrl/eslint-config"], 8 | "rules": { 9 | "@typescript-eslint/prefer-includes": "off", 10 | "no-bitwise": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 20 17 | cache: 'npm' 18 | - run: npm ci 19 | - name: lint 20 | run: npm run lint 21 | - name: test 22 | run: npm run test:ci 23 | - name: coverage 24 | uses: codecov/codecov-action@v3 25 | with: 26 | token: ${{ secrets.CODECOV_TOKEN }} 27 | 28 | publish: 29 | needs: build 30 | runs-on: ubuntu-latest 31 | if: github.ref_name == 'master' 32 | permissions: 33 | contents: write # to be able to publish a GitHub release 34 | issues: write # to be able to comment on released issues 35 | pull-requests: write # to be able to comment on released pull requests 36 | id-token: write # to enable use of OIDC for npm provenance 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | - uses: actions/setup-node@v4 42 | with: 43 | node-version: 20 44 | cache: 'npm' 45 | - run: npm ci 46 | - name: Delete Workspaces 47 | run: npm pkg delete workspaces 48 | - name: release 49 | run: npx semantic-release 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directory 12 | coverage 13 | 14 | # node-waf configuration 15 | .lock-wscript 16 | 17 | # Compiled binary addons (http://nodejs.org/api/addons.html) 18 | build/Release 19 | 20 | # Dependency directories 21 | node_modules 22 | jspm_packages 23 | 24 | # Optional npm cache directory 25 | .npm 26 | 27 | # Optional REPL history 28 | .node_repl_history 29 | .vscode 30 | 31 | # build 32 | build 33 | docs 34 | dist 35 | .rpt2_cache 36 | demo/**/*.js 37 | demo/**/*.js.map 38 | junit.xml 39 | yarn.lock 40 | tsconfig-build* 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "printWidth": 100, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Scott Cooper 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tinycolor 2 | 3 | [![npm](https://badgen.net/npm/v/@ctrl/tinycolor)](https://www.npmjs.com/package/@ctrl/tinycolor) 4 | [![coverage](https://badgen.net/codecov/c/github/scttcper/tinycolor)](https://codecov.io/gh/scttcper/tinycolor) 5 | [![bundlesize](https://badgen.net/bundlephobia/min/@ctrl/tinycolor)](https://bundlephobia.com/result?p=@ctrl/tinycolor) 6 | 7 | > TinyColor is a small library for color manipulation and conversion 8 | 9 | A fork of [tinycolor2](https://github.com/bgrins/TinyColor) by [Brian Grinstead](https://github.com/bgrins) 10 | 11 | __DEMO__: https://tinycolor.vercel.app 12 | 13 | ### Changes from tinycolor2 14 | 15 | * reformatted into TypeScript / es2015 and requires node >= 8 16 | * tree shakeable "module" export and no package `sideEffects` 17 | * `tinycolor` is now exported as a class called `TinyColor` 18 | * default export removed, use `import { TinyColor } from '@ctrl/tinycolor'` 19 | * new `random`, an implementation of [randomColor](https://github.com/davidmerfield/randomColor/) by David Merfield that returns a TinyColor object 20 | * several functions moved out of the tinycolor class and are no longer `TinyColor.` 21 | * `readability`, `fromRatio` moved out 22 | * `random` moved out and renamed to `legacyRandom` 23 | * `toFilter` has been moved out and renamed to `toMsFilter` 24 | * `mix`, `equals` use the current TinyColor object as the first parameter 25 | * added polyad colors tinycolor PR [126](https://github.com/bgrins/TinyColor/pull/126) 26 | * color wheel values (360) are allowed to over or under-spin and still return valid colors tinycolor PR [108](https://github.com/bgrins/TinyColor/pull/108) 27 | * added `tint()` and `shade()` tinycolor PR [159](https://github.com/bgrins/TinyColor/pull/159) 28 | * `isValid`, `format` are now propertys instead of a function 29 | 30 | ## Install 31 | 32 | ```sh 33 | npm install @ctrl/tinycolor 34 | ``` 35 | 36 | ## Use 37 | 38 | ```ts 39 | import { TinyColor } from '@ctrl/tinycolor'; 40 | const color = new TinyColor('red').toHexString(); // '#ff0000' 41 | ``` 42 | 43 | ## Accepted String Input 44 | 45 | The string parsing is very permissive. It is meant to make typing a color as input as easy as possible. All commas, percentages, parenthesis are optional, and most input allow either 0-1, 0%-100%, or 0-n (where n is either 100, 255, or 360 depending on the value). 46 | 47 | HSL and HSV both require either 0%-100% or 0-1 for the `S`/`L`/`V` properties. The `H` (hue) can have values between 0%-100% or 0-360. 48 | 49 | RGB input requires either 0-255 or 0%-100%. 50 | 51 | If you call `tinycolor.fromRatio`, RGB and Hue input can also accept 0-1. 52 | 53 | Here are some examples of string input: 54 | 55 | ### Hex, 8-digit (RGBA) Hex 56 | 57 | ```ts 58 | new TinyColor('#000'); 59 | new TinyColor('000'); 60 | new TinyColor('#369C'); 61 | new TinyColor('369C'); 62 | new TinyColor('#f0f0f6'); 63 | new TinyColor('f0f0f6'); 64 | new TinyColor('#f0f0f688'); 65 | new TinyColor('f0f0f688'); 66 | ``` 67 | 68 | ### RGB, RGBA 69 | 70 | ```ts 71 | new TinyColor('rgb (255, 0, 0)'); 72 | new TinyColor('rgb 255 0 0'); 73 | new TinyColor('rgba (255, 0, 0, .5)'); 74 | new TinyColor({ r: 255, g: 0, b: 0 }); 75 | 76 | import { fromRatio } from '@ctrl/tinycolor'; 77 | fromRatio({ r: 1, g: 0, b: 0 }); 78 | fromRatio({ r: 0.5, g: 0.5, b: 0.5 }); 79 | ``` 80 | 81 | ### HSL, HSLA 82 | 83 | ```ts 84 | new TinyColor('hsl(0, 100%, 50%)'); 85 | new TinyColor('hsla(0, 100%, 50%, .5)'); 86 | new TinyColor('hsl(0, 100%, 50%)'); 87 | new TinyColor('hsl 0 1.0 0.5'); 88 | new TinyColor({ h: 0, s: 1, l: 0.5 }); 89 | ``` 90 | 91 | ### HSV, HSVA 92 | 93 | ```ts 94 | new TinyColor('hsv(0, 100%, 100%)'); 95 | new TinyColor('hsva(0, 100%, 100%, .5)'); 96 | new TinyColor('hsv (0 100% 100%)'); 97 | new TinyColor('hsv 0 1 1'); 98 | new TinyColor({ h: 0, s: 100, v: 100 }); 99 | ``` 100 | 101 | ### CMYK 102 | 103 | ```ts 104 | new TinyColor('cmyk(0, 25, 20, 0)'); 105 | new TinyColor('cmyk(0, 100, 100, 0)'); 106 | new TinyColor('cmyk 100 0 100 0)'); 107 | new TinyColor({c: 0, m: 25, y: 25, k: 0}); 108 | ``` 109 | 110 | ### Named 111 | 112 | ```ts 113 | new TinyColor('RED'); 114 | new TinyColor('blanchedalmond'); 115 | new TinyColor('darkblue'); 116 | ``` 117 | 118 | ### Number 119 | ```ts 120 | new TinyColor(0x0); 121 | new TinyColor(0xaabbcc); 122 | ``` 123 | 124 | ### Accepted Object Input 125 | 126 | If you are calling this from code, you may want to use object input. Here are some examples of the different types of accepted object inputs: 127 | 128 | ```ts 129 | { r: 255, g: 0, b: 0 } 130 | { r: 255, g: 0, b: 0, a: .5 } 131 | { h: 0, s: 100, l: 50 } 132 | { h: 0, s: 100, v: 100 } 133 | ``` 134 | 135 | ## Properties 136 | 137 | ### originalInput 138 | 139 | The original input passed into the constructer used to create the tinycolor instance 140 | 141 | ```ts 142 | const color = new TinyColor('red'); 143 | color.originalInput; // "red" 144 | color = new TinyColor({ r: 255, g: 255, b: 255 }); 145 | color.originalInput; // "{r: 255, g: 255, b: 255}" 146 | ``` 147 | 148 | ### format 149 | 150 | Returns the format used to create the tinycolor instance 151 | 152 | ```ts 153 | const color = new TinyColor('red'); 154 | color.format; // "name" 155 | color = new TinyColor({ r: 255, g: 255, b: 255 }); 156 | color.format; // "rgb" 157 | ``` 158 | 159 | ### isValid 160 | 161 | A boolean indicating whether the color was successfully parsed. Note: if the color is not valid then it will act like `black` when being used with other methods. 162 | 163 | ```ts 164 | const color1 = new TinyColor('red'); 165 | color1.isValid; // true 166 | color1.toHexString(); // "#ff0000" 167 | 168 | const color2 = new TinyColor('not a color'); 169 | color2.isValid; // false 170 | color2.toString(); // "#000000" 171 | ``` 172 | 173 | ## Methods 174 | 175 | ### getBrightness 176 | 177 | Returns the perceived brightness of a color, from `0-255`, as defined by [Web Content Accessibility Guidelines (Version 1.0)](http://www.w3.org/TR/AERT#color-contrast). 178 | 179 | ```ts 180 | const color1 = new TinyColor('#fff'); 181 | color1.getBrightness(); // 255 182 | 183 | const color2 = new TinyColor('#000'); 184 | color2.getBrightness(); // 0 185 | ``` 186 | 187 | ### isLight 188 | 189 | Return a boolean indicating whether the color's perceived brightness is light. 190 | 191 | ```ts 192 | const color1 = new TinyColor('#fff'); 193 | color1.isLight(); // true 194 | 195 | const color2 = new TinyColor('#000'); 196 | color2.isLight(); // false 197 | ``` 198 | 199 | ### isDark 200 | 201 | Return a boolean indicating whether the color's perceived brightness is dark. 202 | 203 | ```ts 204 | const color1 = new TinyColor('#fff'); 205 | color1.isDark(); // false 206 | 207 | const color2 = new TinyColor('#000'); 208 | color2.isDark(); // true 209 | ``` 210 | 211 | ### getLuminance 212 | 213 | Returns the perceived luminance of a color, from `0-1` as defined by [Web Content Accessibility Guidelines (Version 2.0).](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef) 214 | 215 | ```ts 216 | const color1 = new TinyColor('#fff'); 217 | color1.getLuminance(); // 1 218 | 219 | const color2 = new TinyColor('#000'); 220 | color2.getLuminance(); // 0 221 | ``` 222 | 223 | ### getAlpha 224 | 225 | Returns the alpha value of a color, from `0-1`. 226 | 227 | ```ts 228 | const color1 = new TinyColor('rgba(255, 0, 0, .5)'); 229 | color1.getAlpha(); // 0.5 230 | 231 | const color2 = new TinyColor('rgb(255, 0, 0)'); 232 | color2.getAlpha(); // 1 233 | 234 | const color3 = new TinyColor('transparent'); 235 | color3.getAlpha(); // 0 236 | ``` 237 | 238 | ### setAlpha 239 | 240 | Sets the alpha value on a current color. Accepted range is in between `0-1`. 241 | 242 | ```ts 243 | const color = new TinyColor('red'); 244 | color.getAlpha(); // 1 245 | color.setAlpha(0.5); 246 | color.getAlpha(); // .5 247 | color.toRgbString(); // "rgba(255, 0, 0, .5)" 248 | ``` 249 | 250 | ### onBackground 251 | 252 | Compute how the color would appear on a background. When the color is fully transparent (i.e. `getAlpha() == 0`), the result will be the background color. When the color is not transparent at all (i.e. `getAlpha() == 1`), the result will be the color itself. Otherwise you will get a computed result. 253 | 254 | ```ts 255 | const color = new TinyColor('rgba(255, 0, 0, .5)'); 256 | const computedColor = color.onBackground('rgb(0, 0, 255)'); 257 | computedColor.toRgbString(); // "rgb(128, 0, 128)" 258 | ``` 259 | 260 | ### String Representations 261 | 262 | The following methods will return a property for the `alpha` value, which can be ignored: `toHsv`, `toHsl`, `toRgb` 263 | 264 | ### toHsv 265 | 266 | ```ts 267 | const color = new TinyColor('red'); 268 | color.toHsv(); // { h: 0, s: 1, v: 1, a: 1 } 269 | ``` 270 | 271 | ### toHsvString 272 | 273 | ```ts 274 | const color = new TinyColor('red'); 275 | color.toHsvString(); // "hsv(0, 100%, 100%)" 276 | color.setAlpha(0.5); 277 | color.toHsvString(); // "hsva(0, 100%, 100%, 0.5)" 278 | ``` 279 | 280 | ### toHsl 281 | 282 | ```ts 283 | const color = new TinyColor('red'); 284 | color.toHsl(); // { h: 0, s: 1, l: 0.5, a: 1 } 285 | ``` 286 | 287 | ### toHslString 288 | 289 | ```ts 290 | const color = new TinyColor('red'); 291 | color.toHslString(); // "hsl(0, 100%, 50%)" 292 | color.setAlpha(0.5); 293 | color.toHslString(); // "hsla(0, 100%, 50%, 0.5)" 294 | ``` 295 | 296 | ### toCmykString 297 | 298 | ```ts 299 | const color = new TinyColor('red'); 300 | color.toCmykString(); // "cmyk(0, 100, 100, 0)" 301 | ``` 302 | 303 | ### toNumber 304 | ```ts 305 | new TinyColor('#aabbcc').toNumber() === 0xaabbcc // true 306 | new TinyColor('rgb(1, 1, 1)').toNumber() === (1 << 16) + (1 << 8) + 1 // true 307 | ``` 308 | 309 | ### toHex 310 | 311 | ```ts 312 | const color = new TinyColor('red'); 313 | color.toHex(); // "ff0000" 314 | ``` 315 | 316 | ### toHexString 317 | 318 | ```ts 319 | const color = new TinyColor('red'); 320 | color.toHexString(); // "#ff0000" 321 | ``` 322 | 323 | ### toHex8 324 | 325 | ```ts 326 | const color = new TinyColor('red'); 327 | color.toHex8(); // "ff0000ff" 328 | ``` 329 | 330 | ### toHex8String 331 | 332 | ```ts 333 | const color = new TinyColor('red'); 334 | color.toHex8String(); // "#ff0000ff" 335 | ``` 336 | 337 | ### toHexShortString 338 | 339 | ```ts 340 | const color1 = new TinyColor('#ff000000'); 341 | color1.toHexShortString(); // "#ff000000" 342 | color1.toHexShortString(true); // "#f000" 343 | 344 | const color2 = new TinyColor('#ff0000ff'); 345 | color2.toHexShortString(); // "#ff0000" 346 | color2.toHexShortString(true); // "#f00" 347 | ``` 348 | 349 | ### toRgb 350 | 351 | ```ts 352 | const color = new TinyColor('red'); 353 | color.toRgb(); // { r: 255, g: 0, b: 0, a: 1 } 354 | ``` 355 | 356 | ### toRgbString 357 | 358 | ```ts 359 | const color = new TinyColor('red'); 360 | color.toRgbString(); // "rgb(255, 0, 0)" 361 | color.setAlpha(0.5); 362 | color.toRgbString(); // "rgba(255, 0, 0, 0.5)" 363 | ``` 364 | 365 | ### toPercentageRgb 366 | 367 | ```ts 368 | const color = new TinyColor('red'); 369 | color.toPercentageRgb(); // { r: "100%", g: "0%", b: "0%", a: 1 } 370 | ``` 371 | 372 | ### toPercentageRgbString 373 | 374 | ```ts 375 | const color = new TinyColor('red'); 376 | color.toPercentageRgbString(); // "rgb(100%, 0%, 0%)" 377 | color.setAlpha(0.5); 378 | color.toPercentageRgbString(); // "rgba(100%, 0%, 0%, 0.5)" 379 | ``` 380 | 381 | ### toName 382 | 383 | ```ts 384 | const color = new TinyColor('red'); 385 | color.toName(); // "red" 386 | ``` 387 | 388 | ### toFilter 389 | 390 | ```ts 391 | import { toMsFilter } from '@ctrl/tinycolor'; 392 | toMsFilter('red', 'blue'); // 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffff0000,endColorstr=#ff0000ff)' 393 | ``` 394 | 395 | ### toString 396 | 397 | Print to a string, depending on the input format. You can also override this by passing one of `"rgb", "prgb", "hex6", "hex3", "hex8", "name", "hsl", "hsv"` into the function. 398 | 399 | ```ts 400 | const color1 = new TinyColor('red'); 401 | color1.toString(); // "red" 402 | color1.toString('hsv'); // "hsv(0, 100%, 100%)" 403 | 404 | const color2 = new TinyColor('rgb(255, 0, 0)'); 405 | color2.toString(); // "rgb(255, 0, 0)" 406 | color2.setAlpha(0.5); 407 | color2.toString(); // "rgba(255, 0, 0, 0.5)" 408 | ``` 409 | 410 | ### Color Modification 411 | 412 | These methods manipulate the current color, and return it for chaining. For instance: 413 | 414 | ```ts 415 | new TinyColor('red') 416 | .lighten() 417 | .desaturate() 418 | .toHexString(); // '#f53d3d' 419 | ``` 420 | 421 | ### lighten 422 | 423 | `lighten: function(amount = 10) -> TinyColor`. Lighten the color a given amount, from 0 to 100. Providing 100 will always return white. 424 | 425 | ```ts 426 | new TinyColor('#f00').lighten().toString(); // '#ff3333' 427 | new TinyColor('#f00').lighten(100).toString(); // '#ffffff' 428 | ``` 429 | 430 | ### brighten 431 | 432 | `brighten: function(amount = 10) -> TinyColor`. Brighten the color a given amount, from 0 to 100. 433 | 434 | ```ts 435 | new TinyColor('#f00').brighten().toString(); // '#ff1919' 436 | ``` 437 | 438 | ### darken 439 | 440 | `darken: function(amount = 10) -> TinyColor`. Darken the color a given amount, from 0 to 100. Providing 100 will always return black. 441 | 442 | ```ts 443 | new TinyColor('#f00').darken().toString(); // '#cc0000' 444 | new TinyColor('#f00').darken(100).toString(); // '#000000' 445 | ``` 446 | 447 | ### tint 448 | 449 | Mix the color with pure white, from 0 to 100. Providing 0 will do nothing, providing 100 will always return white. 450 | 451 | ```ts 452 | new TinyColor('#f00').tint().toString(); // "#ff1a1a" 453 | new TinyColor('#f00').tint(100).toString(); // "#ffffff" 454 | ``` 455 | 456 | ### shade 457 | 458 | Mix the color with pure black, from 0 to 100. Providing 0 will do nothing, providing 100 will always return black. 459 | 460 | ```ts 461 | new TinyColor('#f00').shade().toString(); // "#e60000" 462 | new TinyColor('#f00').shade(100).toString(); // "#000000" 463 | ``` 464 | 465 | ### desaturate 466 | 467 | `desaturate: function(amount = 10) -> TinyColor`. Desaturate the color a given amount, from 0 to 100. Providing 100 will is the same as calling `greyscale`. 468 | 469 | ```ts 470 | new TinyColor('#f00').desaturate().toString(); // "#f20d0d" 471 | new TinyColor('#f00').desaturate(100).toString(); // "#808080" 472 | ``` 473 | 474 | ### saturate 475 | 476 | `saturate: function(amount = 10) -> TinyColor`. Saturate the color a given amount, from 0 to 100. 477 | 478 | ```ts 479 | new TinyColor('hsl(0, 10%, 50%)').saturate().toString(); // "hsl(0, 20%, 50%)" 480 | ``` 481 | 482 | ### greyscale 483 | 484 | `greyscale: function() -> TinyColor`. Completely desaturates a color into greyscale. Same as calling `desaturate(100)`. 485 | 486 | ```ts 487 | new TinyColor('#f00').greyscale().toString(); // "#808080" 488 | ``` 489 | 490 | ### spin 491 | 492 | `spin: function(amount = 0) -> TinyColor`. Spin the hue a given amount, from -360 to 360. Calling with 0, 360, or -360 will do nothing (since it sets the hue back to what it was before). 493 | 494 | ```ts 495 | new TinyColor('#f00').spin(180).toString(); // "#00ffff" 496 | new TinyColor('#f00').spin(-90).toString(); // "#7f00ff" 497 | new TinyColor('#f00').spin(90).toString(); // "#80ff00" 498 | 499 | // spin(0) and spin(360) do nothing 500 | new TinyColor('#f00').spin(0).toString(); // "#ff0000" 501 | new TinyColor('#f00').spin(360).toString(); // "#ff0000" 502 | ``` 503 | 504 | ### mix 505 | 506 | `mix: function(amount = 50) => TinyColor`. Mix the current color a given amount with another color, from 0 to 100. 0 means no mixing (return current color). 507 | 508 | ```ts 509 | let color1 = new TinyColor('#f0f'); 510 | let color2 = new TinyColor('#0f0'); 511 | 512 | color1.mix(color2).toHexString(); // #808080 513 | ``` 514 | 515 | ### Color Combinations 516 | 517 | Combination functions return an array of TinyColor objects unless otherwise noted. 518 | 519 | ### analogous 520 | 521 | `analogous: function(results = 6, slices = 30) -> array`. 522 | 523 | ```ts 524 | const colors = new TinyColor('#f00').analogous(); 525 | colors.map(t => t.toHexString()); // [ "#ff0000", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600" ] 526 | ``` 527 | 528 | ### monochromatic 529 | 530 | `monochromatic: function(, results = 6) -> array`. 531 | 532 | ```ts 533 | const colors = new TinyColor('#f00').monochromatic(); 534 | colors.map(t => t.toHexString()); // [ "#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000" ] 535 | ``` 536 | 537 | ### splitcomplement 538 | 539 | `splitcomplement: function() -> array`. 540 | 541 | ```ts 542 | const colors = new TinyColor('#f00').splitcomplement(); 543 | colors.map(t => t.toHexString()); // [ "#ff0000", "#ccff00", "#0066ff" ] 544 | ``` 545 | 546 | ### triad 547 | 548 | `triad: function() -> array`. Alias for `polyad(3)`. 549 | 550 | ```ts 551 | const colors = new TinyColor('#f00').triad(); 552 | colors.map(t => t.toHexString()); // [ "#ff0000", "#00ff00", "#0000ff" ] 553 | ``` 554 | 555 | ### tetrad 556 | 557 | `tetrad: function() -> array`. Alias for `polyad(4)`. 558 | 559 | ```ts 560 | const colors = new TinyColor('#f00').tetrad(); 561 | colors.map(t => t.toHexString()); // [ "#ff0000", "#80ff00", "#00ffff", "#7f00ff" ] 562 | ``` 563 | 564 | ### polyad 565 | 566 | `polyad: function(number) -> array`. 567 | 568 | ```ts 569 | const colors = new TinyColor('#f00').polyad(4); 570 | colors.map(t => t.toHexString()); // [ "#ff0000", "#80ff00", "#00ffff", "#7f00ff" ] 571 | ``` 572 | 573 | ### complement 574 | 575 | `complement: function() -> TinyColor`. 576 | 577 | ```ts 578 | new TinyColor('#f00').complement().toHexString(); // "#00ffff" 579 | ``` 580 | 581 | ## Color Utilities 582 | 583 | ### equals 584 | 585 | ```ts 586 | let color1 = new TinyColor('red'); 587 | let color2 = new TinyColor('#f00'); 588 | 589 | color1.equals(color2); // true 590 | ``` 591 | 592 | ### random 593 | 594 | Returns a random TinyColor object. This is an implementation of [randomColor](https://github.com/davidmerfield/randomColor/) by David Merfield. 595 | The difference input parsing and output formatting are handled by TinyColor. 596 | 597 | You can pass an options object to influence the type of color it produces. The options object accepts the following properties: 598 | 599 | * `hue` – Controls the hue of the generated color. You can pass a string representing a color name: `red`, `orange`, `yellow`, `green`, `blue`, `purple`, `pink` and `monochrome` are currently supported. If you pass a hexidecimal color string such as #00FFFF, its hue value will be extracted and used to generate colors. 600 | * `luminosity` – Controls the luminosity of the generated color. You can specify a string containing bright, light or dark. 601 | * `count` – An integer which specifies the number of colors to generate. 602 | * `seed` – An integer which when passed will cause randomColor to return the same color each time. 603 | * `alpha` – A decimal between 0 and 1. Only relevant when using a format with an alpha channel (rgba and hsla). Defaults to a random value. 604 | 605 | ```ts 606 | import { random } from '@ctrl/tinycolor'; 607 | // Returns a TinyColor for an attractive color 608 | random(); 609 | 610 | // Returns an array of ten green colors 611 | random({ 612 | count: 10, 613 | hue: 'green', 614 | }); 615 | 616 | // Returns a TinyColor object in a light blue 617 | random({ 618 | luminosity: 'light', 619 | hue: 'blue', 620 | }); 621 | 622 | // Returns a TinyColor object in a 'truly random' color 623 | random({ 624 | luminosity: 'random', 625 | hue: 'random', 626 | }); 627 | 628 | // Returns a dark RGB color with specified alpha 629 | random({ 630 | luminosity: 'dark', 631 | alpha: 0.5, 632 | }); 633 | ``` 634 | 635 | ### Readability 636 | 637 | TinyColor assesses readability based on the [Web Content Accessibility Guidelines (Version 2.0)](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef). 638 | 639 | #### readability 640 | 641 | `readability: function(TinyColor, TinyColor) -> number`. 642 | Returns the contrast ratio between two colors. 643 | 644 | ```ts 645 | import { readability } from '@ctrl/tinycolor'; 646 | readability('#000', '#000'); // 1 647 | readability('#000', '#111'); // 1.1121078324840545 648 | readability('#000', '#fff'); // 21 649 | ``` 650 | 651 | Use the values in your own calculations, or use one of the convenience functions below. 652 | 653 | #### isReadable 654 | 655 | `isReadable: function(TinyColor, TinyColor, Object) -> Boolean`. Ensure that foreground and background color combinations meet WCAG guidelines. `Object` is optional, defaulting to `{level: "AA",size: "small"}`. `level` can be `"AA"` or "AAA" and `size` can be `"small"` or `"large"`. 656 | 657 | Here are links to read more about the [AA](http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) and [AAA](http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html) requirements. 658 | 659 | ```ts 660 | import { isReadable } from '@ctrl/tinycolor'; 661 | isReadable("#000", "#111"); // false 662 | isReadable("#ff0088", "#5c1a72", { level: "AA", size: "small" }); // false 663 | isReadable("#ff0088", "#5c1a72", { level: "AA", size: "large" }), // true 664 | ``` 665 | 666 | #### mostReadable 667 | 668 | `mostReadable: function(TinyColor, [TinyColor, TinyColor ...], Object) -> Boolean`. 669 | Given a base color and a list of possible foreground or background colors for that base, returns the most readable color. 670 | If none of the colors in the list is readable, `mostReadable` will return the better of black or white if `includeFallbackColors:true`. 671 | 672 | ```ts 673 | import { mostReadable } from '@ctrl/tinycolor'; 674 | mostReadable('#000', ['#f00', '#0f0', '#00f']).toHexString(); // "#00ff00" 675 | mostReadable('#123', ['#124', '#125'], { includeFallbackColors: false }).toHexString(); // "#112255" 676 | mostReadable('#123', ['#124', '#125'], { includeFallbackColors: true }).toHexString(); // "#ffffff" 677 | mostReadable('#ff0088', ['#2e0c3a'], { 678 | includeFallbackColors: true, 679 | level: 'AAA', 680 | size: 'large', 681 | }).toHexString(); // "#2e0c3a", 682 | mostReadable('#ff0088', ['#2e0c3a'], { 683 | includeFallbackColors: true, 684 | level: 'AAA', 685 | size: 'small', 686 | }).toHexString(); // "#000000", 687 | ``` 688 | 689 | See [index.html](https://github.com/bgrins/TinyColor/blob/master/index.html) in the project for a demo. 690 | 691 | ## Common operations 692 | 693 | ### clone 694 | 695 | `clone: function() -> TinyColor`. 696 | Instantiate a new TinyColor object with the same color. Any changes to the new one won't affect the old one. 697 | 698 | ```ts 699 | const color1 = new TinyColor('#F00'); 700 | const color2 = color1.clone(); 701 | color2.setAlpha(0.5); 702 | 703 | color1.toString(); // "#ff0000" 704 | color2.toString(); // "rgba(255, 0, 0, 0.5)" 705 | ``` 706 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import { rollup, OutputOptions, RollupOptions } from 'rollup'; 2 | import { default as terser } from '@rollup/plugin-terser'; 3 | 4 | // umd min 5 | const umdMinInputOptions: RollupOptions = { 6 | input: 'dist/module/umd_api.js', 7 | plugins: [terser({sourceMap: true})], 8 | }; 9 | const umdMinOutputOptions: OutputOptions = { 10 | file: './dist/bundles/tinycolor.umd.min.js', 11 | format: 'umd', 12 | sourcemap: true, 13 | name: 'tinycolor', 14 | exports: 'default', 15 | }; 16 | 17 | async function build() { 18 | // create browser bundle 19 | const umdMin = await rollup(umdMinInputOptions); 20 | await umdMin.write(umdMinOutputOptions); 21 | } 22 | 23 | build() 24 | .then(() => console.log('build success')) 25 | .catch(e => { 26 | console.error(e); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TinyColor - Fast, small color manipulation in JavaScript 7 | 8 | 9 | 10 | 11 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |

104 |
105 | TinyColor 106 |

107 |

Fast, small color manipulation and conversion for JavaScript

108 | 109 |

About

110 |

111 | TinyColor is a library for parsing color input and outputting colors as different formats. Input is meant to be as permissive as possible. 112 |

113 | 114 | 115 |

Usage

116 |

117 | API Docs - Readme 118 |

119 | 120 |

Demo

121 | 122 | 123 |

124 | 125 | red - 126 | 0f0 - 127 | rgb 255 128 128 - 128 | hsl(0, 100%, 50%) - 129 | hsv 0, 100%, 50% - 130 | cmyk(0, 25, 20, 0) 131 |

132 | 133 |
134 |
135 |
136 |
137 |
138 |
139 |

140 | 
141 |         
142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | 154 | 155 | 156 | 157 | 160 | 161 | 162 | 163 | 166 | 167 | 168 | 169 | 172 | 173 | 174 | 175 | 178 | 179 | 180 | 181 | 184 | 185 | 186 | 187 | 190 | 191 | 192 | 193 | 196 | 197 | 198 | 199 | 202 | 203 | 204 | 205 | 208 | 209 | 210 | 211 | 214 | 215 |
Lighten 146 |
147 |
Darken 152 |
153 |
Saturate 158 |
159 |
Desaturate 164 |
165 |
Greyscale 170 |
171 |
Brighten 176 |
177 |
Most Readable 182 |
183 |
Triad 188 |
189 |
Tetrad 194 |
195 |
Monochromatic 200 |
201 |
Analogous 206 |
207 |
Split Complements 212 |
213 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |

Credit

224 |

225 | Developed by 226 | Brian Grinstead. Big thanks to the following places: 227 |

228 |
    229 |
  • 230 | less.js for some of the modification functions 231 |
  • 232 |
  • 233 | jQuery xColor for some of the combination functions 234 |
  • 235 |
  • 236 | w3.org for the color list and parsing rules
  • 237 |
  • 238 | mjijackson.com for the first stab at RGB / HSL / HSV converters 239 |
  • 240 |
241 |
242 |
243 |
244 | 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "serve": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "typescript": "5.3.3", 12 | "vite": "5.0.11" 13 | }, 14 | "engines": { 15 | "node": ">=14" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import tinycolor from '../../src/umd_api.js'; 2 | 3 | // make tinycolor available in the console 4 | (window as any).tinycolor = tinycolor; 5 | (window as any).TinyColor = tinycolor.TinyColor; 6 | console.log('try "new TinyColor(\'blue\')" or "tinycolor.random()"'); 7 | 8 | const input = document.querySelector('#color'); 9 | 10 | input.addEventListener('keyup', event => colorChange((event.target as HTMLInputElement).value)); 11 | 12 | const codeOutputEl = document.querySelector('#code-output'); 13 | const filtersEl = document.querySelector('#filter-output'); 14 | const mostReadableEl = document.querySelector('#mostReadable'); 15 | const colorBoxEl = document.querySelector('#colorBox'); 16 | 17 | function colorChange(color) { 18 | const tiny = new tinycolor.TinyColor(color); 19 | 20 | const output = [ 21 | 'hex:\t' + tiny.toHexString(), 22 | 'hex8:\t' + tiny.toHex8String(), 23 | 'rgb:\t' + tiny.toRgbString(), 24 | 'hsl:\t' + tiny.toHslString(), 25 | 'hsv:\t' + tiny.toHsvString(), 26 | 'cmyk:\t' + tiny.toCmykString(), 27 | 'name:\t' + (tiny.toName() || 'none'), 28 | 'format:\t' + tiny.format, 29 | 'string:\t' + tiny.toString(), 30 | ].join('\n'); 31 | 32 | codeOutputEl.textContent = output; 33 | codeOutputEl.style['border-color'] = tiny.toHexString(); 34 | 35 | filtersEl.classList.toggle('invisible', !tiny.isValid); 36 | 37 | const lighten = new tinycolor.TinyColor(color).lighten(20).toHexString(); 38 | filtersEl.querySelector('.lighten').style['background-color'] = lighten; 39 | const darken = new tinycolor.TinyColor(color).darken(20).toHexString(); 40 | filtersEl.querySelector('.darken').style['background-color'] = darken; 41 | const saturate = new tinycolor.TinyColor(color).darken(20).toHexString(); 42 | filtersEl.querySelector('.saturate').style['background-color'] = saturate; 43 | const desaturate = new tinycolor.TinyColor(color).desaturate(20).toHexString(); 44 | filtersEl.querySelector('.desaturate').style['background-color'] = desaturate; 45 | const greyscale = new tinycolor.TinyColor(color).greyscale().toHexString(); 46 | filtersEl.querySelector('.greyscale').style['background-color'] = greyscale; 47 | const brighten = new tinycolor.TinyColor(color).brighten(20).toHexString(); 48 | filtersEl.querySelector('.brighten').style['background-color'] = brighten; 49 | 50 | const allColors = Object.keys(tinycolor.names); 51 | 52 | const readable = tinycolor.mostReadable(color, allColors); 53 | mostReadableEl.style['background-color'] = readable.toHexString(); 54 | 55 | const colorArrayToHTML = arr => 56 | arr.map(e => `
`).join(''); 57 | 58 | filtersEl.querySelector('.triad').innerHTML = colorArrayToHTML(tiny.triad()); 59 | filtersEl.querySelector('.tetrad').innerHTML = colorArrayToHTML(tiny.tetrad()); 60 | filtersEl.querySelector('.mono').innerHTML = colorArrayToHTML(tiny.monochromatic()); 61 | filtersEl.querySelector('.analogous').innerHTML = colorArrayToHTML(tiny.analogous()); 62 | filtersEl.querySelector('.sc').innerHTML = colorArrayToHTML(tiny.splitcomplement()); 63 | } 64 | 65 | (window as any).handleChange = function (color) { 66 | input.value = color; 67 | colorChange(color); 68 | }; 69 | 70 | const starterColor = tinycolor.random({ luminosity: 'bright' }).toHexString(); 71 | colorChange(starterColor); 72 | 73 | // Set that box next to the title to a random color 74 | colorBoxEl.style['background-color'] = starterColor; 75 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "lib": ["es2017", "dom"], 6 | "module": "CommonJS", 7 | "moduleResolution": "node", 8 | "strict": false, 9 | "target": "es5", 10 | }, 11 | "files": ["./src/main.ts"], 12 | "include": ["./**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ctrl/tinycolor", 3 | "version": "0.0.0", 4 | "description": "Fast, small color manipulation and conversion for JavaScript", 5 | "author": "Scott Cooper ", 6 | "publishConfig": { 7 | "access": "public" 8 | }, 9 | "license": "MIT", 10 | "homepage": "https://tinycolor.vercel.app", 11 | "repository": "scttcper/tinycolor", 12 | "keywords": [ 13 | "typescript", 14 | "color", 15 | "manipulation", 16 | "tinycolor", 17 | "hsa", 18 | "rgb" 19 | ], 20 | "main": "dist/public_api.js", 21 | "module": "dist/module/public_api.js", 22 | "typings": "dist/public_api.d.ts", 23 | "files": [ 24 | "dist" 25 | ], 26 | "sideEffects": false, 27 | "scripts": { 28 | "demo:build": "npm run build --workspace=demo", 29 | "demo:watch": "npm run dev --workspace=demo", 30 | "lint": "eslint --ext .js,.ts, .", 31 | "lint:fix": "eslint --fix --ext .js,.ts, .", 32 | "prepare": "npm run build", 33 | "build": "del-cli dist && tsc -p tsconfig.build.json && tsc -p tsconfig.module.json && ts-node build", 34 | "docs:build": "typedoc --out demo/dist/docs --hideGenerator --tsconfig tsconfig.build.json src/public_api.ts", 35 | "test": "vitest run", 36 | "test:watch": "vitest", 37 | "test:ci": "vitest run --coverage --reporter=default --reporter=junit --outputFile=./junit.xml" 38 | }, 39 | "devDependencies": { 40 | "@ctrl/eslint-config": "4.0.14", 41 | "@rollup/plugin-terser": "0.4.4", 42 | "@types/node": "20.11.5", 43 | "@vitest/coverage-v8": "1.2.1", 44 | "del-cli": "5.1.0", 45 | "rollup": "4.9.5", 46 | "ts-node": "10.9.2", 47 | "typedoc": "0.25.7", 48 | "typescript": "5.3.3", 49 | "vitest": "1.2.1" 50 | }, 51 | "workspaces": [ 52 | "demo" 53 | ], 54 | "release": { 55 | "branches": [ 56 | "master" 57 | ] 58 | }, 59 | "engines": { 60 | "node": ">=14" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/conversion.ts: -------------------------------------------------------------------------------- 1 | import { HSL, HSV, Numberify, RGB } from './interfaces.js'; 2 | import { bound01, pad2 } from './util.js'; 3 | 4 | // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from: 5 | // 6 | 7 | /** 8 | * Handle bounds / percentage checking to conform to CSS color spec 9 | * 10 | * *Assumes:* r, g, b in [0, 255] or [0, 1] 11 | * *Returns:* { r, g, b } in [0, 255] 12 | */ 13 | export function rgbToRgb( 14 | r: number | string, 15 | g: number | string, 16 | b: number | string, 17 | ): Numberify { 18 | return { 19 | r: bound01(r, 255) * 255, 20 | g: bound01(g, 255) * 255, 21 | b: bound01(b, 255) * 255, 22 | }; 23 | } 24 | 25 | /** 26 | * Converts an RGB color value to HSL. 27 | * *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] 28 | * *Returns:* { h, s, l } in [0,1] 29 | */ 30 | export function rgbToHsl(r: number, g: number, b: number): Numberify { 31 | r = bound01(r, 255); 32 | g = bound01(g, 255); 33 | b = bound01(b, 255); 34 | 35 | const max = Math.max(r, g, b); 36 | const min = Math.min(r, g, b); 37 | let h = 0; 38 | let s = 0; 39 | const l = (max + min) / 2; 40 | 41 | if (max === min) { 42 | s = 0; 43 | h = 0; // achromatic 44 | } else { 45 | const d = max - min; 46 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 47 | switch (max) { 48 | case r: 49 | h = (g - b) / d + (g < b ? 6 : 0); 50 | break; 51 | case g: 52 | h = (b - r) / d + 2; 53 | break; 54 | case b: 55 | h = (r - g) / d + 4; 56 | break; 57 | default: 58 | break; 59 | } 60 | 61 | h /= 6; 62 | } 63 | 64 | return { h, s, l }; 65 | } 66 | 67 | function hue2rgb(p: number, q: number, t: number): number { 68 | if (t < 0) { 69 | t += 1; 70 | } 71 | 72 | if (t > 1) { 73 | t -= 1; 74 | } 75 | 76 | if (t < 1 / 6) { 77 | return p + (q - p) * (6 * t); 78 | } 79 | 80 | if (t < 1 / 2) { 81 | return q; 82 | } 83 | 84 | if (t < 2 / 3) { 85 | return p + (q - p) * (2 / 3 - t) * 6; 86 | } 87 | 88 | return p; 89 | } 90 | 91 | /** 92 | * Converts an HSL color value to RGB. 93 | * 94 | * *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] 95 | * *Returns:* { r, g, b } in the set [0, 255] 96 | */ 97 | export function hslToRgb( 98 | h: number | string, 99 | s: number | string, 100 | l: number | string, 101 | ): Numberify { 102 | let r: number; 103 | let g: number; 104 | let b: number; 105 | 106 | h = bound01(h, 360); 107 | s = bound01(s, 100); 108 | l = bound01(l, 100); 109 | 110 | if (s === 0) { 111 | // achromatic 112 | g = l; 113 | b = l; 114 | r = l; 115 | } else { 116 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 117 | const p = 2 * l - q; 118 | r = hue2rgb(p, q, h + 1 / 3); 119 | g = hue2rgb(p, q, h); 120 | b = hue2rgb(p, q, h - 1 / 3); 121 | } 122 | 123 | return { r: r * 255, g: g * 255, b: b * 255 }; 124 | } 125 | 126 | /** 127 | * Converts an RGB color value to HSV 128 | * 129 | * *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] 130 | * *Returns:* { h, s, v } in [0,1] 131 | */ 132 | export function rgbToHsv(r: number, g: number, b: number): Numberify { 133 | r = bound01(r, 255); 134 | g = bound01(g, 255); 135 | b = bound01(b, 255); 136 | 137 | const max = Math.max(r, g, b); 138 | const min = Math.min(r, g, b); 139 | let h = 0; 140 | const v = max; 141 | const d = max - min; 142 | const s = max === 0 ? 0 : d / max; 143 | 144 | if (max === min) { 145 | h = 0; // achromatic 146 | } else { 147 | switch (max) { 148 | case r: 149 | h = (g - b) / d + (g < b ? 6 : 0); 150 | break; 151 | case g: 152 | h = (b - r) / d + 2; 153 | break; 154 | case b: 155 | h = (r - g) / d + 4; 156 | break; 157 | default: 158 | break; 159 | } 160 | 161 | h /= 6; 162 | } 163 | 164 | return { h, s, v }; 165 | } 166 | 167 | /** 168 | * Converts an HSV color value to RGB. 169 | * 170 | * *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] 171 | * *Returns:* { r, g, b } in the set [0, 255] 172 | */ 173 | export function hsvToRgb( 174 | h: number | string, 175 | s: number | string, 176 | v: number | string, 177 | ): Numberify { 178 | h = bound01(h, 360) * 6; 179 | s = bound01(s, 100); 180 | v = bound01(v, 100); 181 | 182 | const i = Math.floor(h); 183 | const f = h - i; 184 | const p = v * (1 - s); 185 | const q = v * (1 - f * s); 186 | const t = v * (1 - (1 - f) * s); 187 | const mod = i % 6; 188 | const r = [v, q, p, p, t, v][mod]; 189 | const g = [t, v, v, q, p, p][mod]; 190 | const b = [p, p, t, v, v, q][mod]; 191 | 192 | return { r: r * 255, g: g * 255, b: b * 255 }; 193 | } 194 | 195 | /** 196 | * Converts an RGB color to hex 197 | * 198 | * *Assumes:* r, g, and b are contained in the set [0, 255] 199 | * *Returns:* a 3 or 6 character hex 200 | */ 201 | export function rgbToHex(r: number, g: number, b: number, allow3Char: boolean): string { 202 | const hex = [ 203 | pad2(Math.round(r).toString(16)), 204 | pad2(Math.round(g).toString(16)), 205 | pad2(Math.round(b).toString(16)), 206 | ]; 207 | 208 | // Return a 3 character hex if possible 209 | if ( 210 | allow3Char && 211 | hex[0].startsWith(hex[0].charAt(1)) && 212 | hex[1].startsWith(hex[1].charAt(1)) && 213 | hex[2].startsWith(hex[2].charAt(1)) 214 | ) { 215 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0); 216 | } 217 | 218 | return hex.join(''); 219 | } 220 | 221 | /** 222 | * Converts an RGBA color plus alpha transparency to hex 223 | * 224 | * *Assumes:* r, g, b are contained in the set [0, 255] and a in [0, 1] 225 | * *Returns:* a 4 or 8 character rgba hex 226 | */ 227 | // eslint-disable-next-line max-params 228 | export function rgbaToHex(r: number, g: number, b: number, a: number, allow4Char: boolean): string { 229 | const hex = [ 230 | pad2(Math.round(r).toString(16)), 231 | pad2(Math.round(g).toString(16)), 232 | pad2(Math.round(b).toString(16)), 233 | pad2(convertDecimalToHex(a)), 234 | ]; 235 | 236 | // Return a 4 character hex if possible 237 | if ( 238 | allow4Char && 239 | hex[0].startsWith(hex[0].charAt(1)) && 240 | hex[1].startsWith(hex[1].charAt(1)) && 241 | hex[2].startsWith(hex[2].charAt(1)) && 242 | hex[3].startsWith(hex[3].charAt(1)) 243 | ) { 244 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0); 245 | } 246 | 247 | return hex.join(''); 248 | } 249 | 250 | /** 251 | * Converts an RGBA color to an ARGB Hex8 string 252 | * Rarely used, but required for "toFilter()" 253 | * 254 | * *Assumes:* r, g, b are contained in the set [0, 255] and a in [0, 1] 255 | * *Returns:* a 8 character argb hex 256 | */ 257 | export function rgbaToArgbHex(r: number, g: number, b: number, a: number): string { 258 | const hex = [ 259 | pad2(convertDecimalToHex(a)), 260 | pad2(Math.round(r).toString(16)), 261 | pad2(Math.round(g).toString(16)), 262 | pad2(Math.round(b).toString(16)), 263 | ]; 264 | 265 | return hex.join(''); 266 | } 267 | 268 | /** 269 | * Converts CMYK to RBG 270 | * Assumes c, m, y, k are in the set [0, 100] 271 | */ 272 | export function cmykToRgb(c: number, m: number, y: number, k: number) { 273 | const cConv = c / 100; 274 | const mConv = m / 100; 275 | const yConv = y / 100; 276 | const kConv = k / 100; 277 | 278 | const r = 255 * (1 - cConv) * (1 - kConv); 279 | const g = 255 * (1 - mConv) * (1 - kConv); 280 | const b = 255 * (1 - yConv) * (1 - kConv); 281 | 282 | return { r, g, b }; 283 | } 284 | 285 | export function rgbToCmyk(r: number, g: number, b: number) { 286 | let c = 1 - r / 255; 287 | let m = 1 - g / 255; 288 | let y = 1 - b / 255; 289 | let k = Math.min(c, m, y); 290 | 291 | if (k === 1) { 292 | c = 0; 293 | m = 0; 294 | y = 0; 295 | } else { 296 | c = ((c - k) / (1 - k)) * 100; 297 | m = ((m - k) / (1 - k)) * 100; 298 | y = ((y - k) / (1 - k)) * 100; 299 | } 300 | 301 | k *= 100; 302 | 303 | return { 304 | c: Math.round(c), 305 | m: Math.round(m), 306 | y: Math.round(y), 307 | k: Math.round(k), 308 | }; 309 | } 310 | 311 | /** Converts a decimal to a hex value */ 312 | export function convertDecimalToHex(d: string | number): string { 313 | return Math.round(parseFloat(d as string) * 255).toString(16); 314 | } 315 | 316 | /** Converts a hex value to a decimal */ 317 | export function convertHexToDecimal(h: string): number { 318 | return parseIntFromHex(h) / 255; 319 | } 320 | 321 | /** Parse a base-16 hex value into a base-10 integer */ 322 | export function parseIntFromHex(val: string): number { 323 | return parseInt(val, 16); 324 | } 325 | 326 | export function numberInputToObject(color: number): RGB { 327 | return { 328 | r: color >> 16, 329 | g: (color & 0xff00) >> 8, 330 | b: color & 0xff, 331 | }; 332 | } 333 | -------------------------------------------------------------------------------- /src/css-color-names.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json 2 | /** 3 | * @hidden 4 | */ 5 | export const names: Record = { 6 | aliceblue: '#f0f8ff', 7 | antiquewhite: '#faebd7', 8 | aqua: '#00ffff', 9 | aquamarine: '#7fffd4', 10 | azure: '#f0ffff', 11 | beige: '#f5f5dc', 12 | bisque: '#ffe4c4', 13 | black: '#000000', 14 | blanchedalmond: '#ffebcd', 15 | blue: '#0000ff', 16 | blueviolet: '#8a2be2', 17 | brown: '#a52a2a', 18 | burlywood: '#deb887', 19 | cadetblue: '#5f9ea0', 20 | chartreuse: '#7fff00', 21 | chocolate: '#d2691e', 22 | coral: '#ff7f50', 23 | cornflowerblue: '#6495ed', 24 | cornsilk: '#fff8dc', 25 | crimson: '#dc143c', 26 | cyan: '#00ffff', 27 | darkblue: '#00008b', 28 | darkcyan: '#008b8b', 29 | darkgoldenrod: '#b8860b', 30 | darkgray: '#a9a9a9', 31 | darkgreen: '#006400', 32 | darkgrey: '#a9a9a9', 33 | darkkhaki: '#bdb76b', 34 | darkmagenta: '#8b008b', 35 | darkolivegreen: '#556b2f', 36 | darkorange: '#ff8c00', 37 | darkorchid: '#9932cc', 38 | darkred: '#8b0000', 39 | darksalmon: '#e9967a', 40 | darkseagreen: '#8fbc8f', 41 | darkslateblue: '#483d8b', 42 | darkslategray: '#2f4f4f', 43 | darkslategrey: '#2f4f4f', 44 | darkturquoise: '#00ced1', 45 | darkviolet: '#9400d3', 46 | deeppink: '#ff1493', 47 | deepskyblue: '#00bfff', 48 | dimgray: '#696969', 49 | dimgrey: '#696969', 50 | dodgerblue: '#1e90ff', 51 | firebrick: '#b22222', 52 | floralwhite: '#fffaf0', 53 | forestgreen: '#228b22', 54 | fuchsia: '#ff00ff', 55 | gainsboro: '#dcdcdc', 56 | ghostwhite: '#f8f8ff', 57 | goldenrod: '#daa520', 58 | gold: '#ffd700', 59 | gray: '#808080', 60 | green: '#008000', 61 | greenyellow: '#adff2f', 62 | grey: '#808080', 63 | honeydew: '#f0fff0', 64 | hotpink: '#ff69b4', 65 | indianred: '#cd5c5c', 66 | indigo: '#4b0082', 67 | ivory: '#fffff0', 68 | khaki: '#f0e68c', 69 | lavenderblush: '#fff0f5', 70 | lavender: '#e6e6fa', 71 | lawngreen: '#7cfc00', 72 | lemonchiffon: '#fffacd', 73 | lightblue: '#add8e6', 74 | lightcoral: '#f08080', 75 | lightcyan: '#e0ffff', 76 | lightgoldenrodyellow: '#fafad2', 77 | lightgray: '#d3d3d3', 78 | lightgreen: '#90ee90', 79 | lightgrey: '#d3d3d3', 80 | lightpink: '#ffb6c1', 81 | lightsalmon: '#ffa07a', 82 | lightseagreen: '#20b2aa', 83 | lightskyblue: '#87cefa', 84 | lightslategray: '#778899', 85 | lightslategrey: '#778899', 86 | lightsteelblue: '#b0c4de', 87 | lightyellow: '#ffffe0', 88 | lime: '#00ff00', 89 | limegreen: '#32cd32', 90 | linen: '#faf0e6', 91 | magenta: '#ff00ff', 92 | maroon: '#800000', 93 | mediumaquamarine: '#66cdaa', 94 | mediumblue: '#0000cd', 95 | mediumorchid: '#ba55d3', 96 | mediumpurple: '#9370db', 97 | mediumseagreen: '#3cb371', 98 | mediumslateblue: '#7b68ee', 99 | mediumspringgreen: '#00fa9a', 100 | mediumturquoise: '#48d1cc', 101 | mediumvioletred: '#c71585', 102 | midnightblue: '#191970', 103 | mintcream: '#f5fffa', 104 | mistyrose: '#ffe4e1', 105 | moccasin: '#ffe4b5', 106 | navajowhite: '#ffdead', 107 | navy: '#000080', 108 | oldlace: '#fdf5e6', 109 | olive: '#808000', 110 | olivedrab: '#6b8e23', 111 | orange: '#ffa500', 112 | orangered: '#ff4500', 113 | orchid: '#da70d6', 114 | palegoldenrod: '#eee8aa', 115 | palegreen: '#98fb98', 116 | paleturquoise: '#afeeee', 117 | palevioletred: '#db7093', 118 | papayawhip: '#ffefd5', 119 | peachpuff: '#ffdab9', 120 | peru: '#cd853f', 121 | pink: '#ffc0cb', 122 | plum: '#dda0dd', 123 | powderblue: '#b0e0e6', 124 | purple: '#800080', 125 | rebeccapurple: '#663399', 126 | red: '#ff0000', 127 | rosybrown: '#bc8f8f', 128 | royalblue: '#4169e1', 129 | saddlebrown: '#8b4513', 130 | salmon: '#fa8072', 131 | sandybrown: '#f4a460', 132 | seagreen: '#2e8b57', 133 | seashell: '#fff5ee', 134 | sienna: '#a0522d', 135 | silver: '#c0c0c0', 136 | skyblue: '#87ceeb', 137 | slateblue: '#6a5acd', 138 | slategray: '#708090', 139 | slategrey: '#708090', 140 | snow: '#fffafa', 141 | springgreen: '#00ff7f', 142 | steelblue: '#4682b4', 143 | tan: '#d2b48c', 144 | teal: '#008080', 145 | thistle: '#d8bfd8', 146 | tomato: '#ff6347', 147 | turquoise: '#40e0d0', 148 | violet: '#ee82ee', 149 | wheat: '#f5deb3', 150 | white: '#ffffff', 151 | whitesmoke: '#f5f5f5', 152 | yellow: '#ffff00', 153 | yellowgreen: '#9acd32', 154 | }; 155 | -------------------------------------------------------------------------------- /src/format-input.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cmykToRgb, 3 | convertHexToDecimal, 4 | hslToRgb, 5 | hsvToRgb, 6 | parseIntFromHex, 7 | rgbToRgb, 8 | } from './conversion.js'; 9 | import { names } from './css-color-names.js'; 10 | import { CMYK, HSL, HSLA, HSV, HSVA, RGB, RGBA } from './interfaces.js'; 11 | import { boundAlpha, convertToPercentage } from './util.js'; 12 | 13 | /** 14 | * Given a string or object, convert that input to RGB 15 | * 16 | * Possible string inputs: 17 | * ``` 18 | * "red" 19 | * "#f00" or "f00" 20 | * "#ff0000" or "ff0000" 21 | * "#ff000000" or "ff000000" 22 | * "rgb 255 0 0" or "rgb (255, 0, 0)" 23 | * "rgb 1.0 0 0" or "rgb (1, 0, 0)" 24 | * "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" 25 | * "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" 26 | * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" 27 | * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" 28 | * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" 29 | * "cmyk(0, 20, 0, 0)" or "cmyk 0 20 0 0" 30 | * ``` 31 | */ 32 | export function inputToRGB(color: string | RGB | RGBA | HSL | HSLA | HSV | HSVA | CMYK | any): { 33 | ok: boolean; 34 | format: any; 35 | r: number; 36 | g: number; 37 | b: number; 38 | a: number; 39 | } { 40 | let rgb = { r: 0, g: 0, b: 0 }; 41 | let a = 1; 42 | let s: string | number | null = null; 43 | let v: string | number | null = null; 44 | let l: string | number | null = null; 45 | let ok = false; 46 | let format: string | false = false; 47 | 48 | if (typeof color === 'string') { 49 | color = stringInputToObject(color); 50 | } 51 | 52 | if (typeof color === 'object') { 53 | if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) { 54 | rgb = rgbToRgb(color.r, color.g, color.b); 55 | ok = true; 56 | format = String(color.r).substr(-1) === '%' ? 'prgb' : 'rgb'; 57 | } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) { 58 | s = convertToPercentage(color.s); 59 | v = convertToPercentage(color.v); 60 | rgb = hsvToRgb(color.h, s as number, v as number); 61 | ok = true; 62 | format = 'hsv'; 63 | } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) { 64 | s = convertToPercentage(color.s); 65 | l = convertToPercentage(color.l); 66 | rgb = hslToRgb(color.h, s as number, l as number); 67 | ok = true; 68 | format = 'hsl'; 69 | } else if ( 70 | isValidCSSUnit(color.c) && 71 | isValidCSSUnit(color.m) && 72 | isValidCSSUnit(color.y) && 73 | isValidCSSUnit(color.k) 74 | ) { 75 | rgb = cmykToRgb(color.c, color.m, color.y, color.k); 76 | ok = true; 77 | format = 'cmyk'; 78 | } 79 | 80 | if (Object.prototype.hasOwnProperty.call(color, 'a')) { 81 | a = color.a; 82 | } 83 | } 84 | 85 | a = boundAlpha(a); 86 | 87 | return { 88 | ok, 89 | format: color.format || format, 90 | r: Math.min(255, Math.max(rgb.r, 0)), 91 | g: Math.min(255, Math.max(rgb.g, 0)), 92 | b: Math.min(255, Math.max(rgb.b, 0)), 93 | a, 94 | }; 95 | } 96 | 97 | // 98 | const CSS_INTEGER = '[-\\+]?\\d+%?'; 99 | 100 | // 101 | const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?'; 102 | 103 | // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. 104 | const CSS_UNIT = '(?:' + CSS_NUMBER + ')|(?:' + CSS_INTEGER + ')'; 105 | 106 | // Actual matching. 107 | // Parentheses and commas are optional, but not required. 108 | // Whitespace can take the place of commas or opening paren 109 | // eslint-disable-next-line prettier/prettier 110 | const PERMISSIVE_MATCH3 = '[\\s|\\(]+(' + CSS_UNIT + ')[,|\\s]+(' + CSS_UNIT + ')[,|\\s]+(' + CSS_UNIT + ')\\s*\\)?'; 111 | const PERMISSIVE_MATCH4 = 112 | // eslint-disable-next-line prettier/prettier 113 | '[\\s|\\(]+(' + CSS_UNIT + ')[,|\\s]+(' + CSS_UNIT + ')[,|\\s]+(' + CSS_UNIT + ')[,|\\s]+(' + CSS_UNIT + ')\\s*\\)?'; 114 | 115 | const matchers = { 116 | CSS_UNIT: new RegExp(CSS_UNIT), 117 | rgb: new RegExp('rgb' + PERMISSIVE_MATCH3), 118 | rgba: new RegExp('rgba' + PERMISSIVE_MATCH4), 119 | hsl: new RegExp('hsl' + PERMISSIVE_MATCH3), 120 | hsla: new RegExp('hsla' + PERMISSIVE_MATCH4), 121 | hsv: new RegExp('hsv' + PERMISSIVE_MATCH3), 122 | hsva: new RegExp('hsva' + PERMISSIVE_MATCH4), 123 | cmyk: new RegExp('cmyk' + PERMISSIVE_MATCH4), 124 | hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, 125 | hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, 126 | hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, 127 | hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, 128 | }; 129 | 130 | /** 131 | * Permissive string parsing. Take in a number of formats, and output an object 132 | * based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` or `{c, m, y, k}` or `{c, m, y, k, a}` 133 | */ 134 | export function stringInputToObject(color: string): any { 135 | color = color.trim().toLowerCase(); 136 | if (color.length === 0) { 137 | return false; 138 | } 139 | 140 | let named = false; 141 | if (names[color]) { 142 | color = names[color]; 143 | named = true; 144 | } else if (color === 'transparent') { 145 | return { r: 0, g: 0, b: 0, a: 0, format: 'name' }; 146 | } 147 | 148 | // Try to match string input using regular expressions. 149 | // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360] 150 | // Just return an object and let the conversion functions handle that. 151 | // This way the result will be the same whether the tinycolor is initialized with string or object. 152 | let match = matchers.rgb.exec(color); 153 | if (match) { 154 | return { r: match[1], g: match[2], b: match[3] }; 155 | } 156 | 157 | match = matchers.rgba.exec(color); 158 | if (match) { 159 | return { r: match[1], g: match[2], b: match[3], a: match[4] }; 160 | } 161 | 162 | match = matchers.hsl.exec(color); 163 | if (match) { 164 | return { h: match[1], s: match[2], l: match[3] }; 165 | } 166 | 167 | match = matchers.hsla.exec(color); 168 | if (match) { 169 | return { h: match[1], s: match[2], l: match[3], a: match[4] }; 170 | } 171 | 172 | match = matchers.hsv.exec(color); 173 | if (match) { 174 | return { h: match[1], s: match[2], v: match[3] }; 175 | } 176 | 177 | match = matchers.hsva.exec(color); 178 | if (match) { 179 | return { h: match[1], s: match[2], v: match[3], a: match[4] }; 180 | } 181 | 182 | match = matchers.cmyk.exec(color); 183 | if (match) { 184 | return { 185 | c: match[1], 186 | m: match[2], 187 | y: match[3], 188 | k: match[4], 189 | }; 190 | } 191 | 192 | match = matchers.hex8.exec(color); 193 | if (match) { 194 | return { 195 | r: parseIntFromHex(match[1]), 196 | g: parseIntFromHex(match[2]), 197 | b: parseIntFromHex(match[3]), 198 | a: convertHexToDecimal(match[4]), 199 | format: named ? 'name' : 'hex8', 200 | }; 201 | } 202 | 203 | match = matchers.hex6.exec(color); 204 | if (match) { 205 | return { 206 | r: parseIntFromHex(match[1]), 207 | g: parseIntFromHex(match[2]), 208 | b: parseIntFromHex(match[3]), 209 | format: named ? 'name' : 'hex', 210 | }; 211 | } 212 | 213 | match = matchers.hex4.exec(color); 214 | if (match) { 215 | return { 216 | r: parseIntFromHex(match[1] + match[1]), 217 | g: parseIntFromHex(match[2] + match[2]), 218 | b: parseIntFromHex(match[3] + match[3]), 219 | a: convertHexToDecimal(match[4] + match[4]), 220 | format: named ? 'name' : 'hex8', 221 | }; 222 | } 223 | 224 | match = matchers.hex3.exec(color); 225 | if (match) { 226 | return { 227 | r: parseIntFromHex(match[1] + match[1]), 228 | g: parseIntFromHex(match[2] + match[2]), 229 | b: parseIntFromHex(match[3] + match[3]), 230 | format: named ? 'name' : 'hex', 231 | }; 232 | } 233 | 234 | return false; 235 | } 236 | 237 | /** 238 | * Check to see if it looks like a CSS unit 239 | * (see `matchers` above for definition). 240 | */ 241 | export function isValidCSSUnit(color: string | number): boolean { 242 | if (typeof color === 'number') { 243 | return !Number.isNaN(color); 244 | } 245 | 246 | return matchers.CSS_UNIT.test(color); 247 | } 248 | -------------------------------------------------------------------------------- /src/from-ratio.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from './index.js'; 2 | import { RGBA } from './interfaces.js'; 3 | import { convertToPercentage } from './util.js'; 4 | 5 | export interface RatioInput { 6 | r: number | string; 7 | g: number | string; 8 | b: number | string; 9 | a?: number | string; 10 | } 11 | 12 | /** 13 | * If input is an object, force 1 into "1.0" to handle ratios properly 14 | * String input requires "1.0" as input, so 1 will be treated as 1 15 | */ 16 | export function fromRatio(ratio: RatioInput, opts?: any): TinyColor { 17 | const newColor: Partial = { 18 | r: convertToPercentage(ratio.r), 19 | g: convertToPercentage(ratio.g), 20 | b: convertToPercentage(ratio.b), 21 | }; 22 | if (ratio.a !== undefined) { 23 | newColor.a = Number(ratio.a); 24 | } 25 | 26 | return new TinyColor(newColor as RGBA, opts); 27 | } 28 | 29 | /** old random function */ 30 | export function legacyRandom(): TinyColor { 31 | return new TinyColor({ 32 | r: Math.random(), 33 | g: Math.random(), 34 | b: Math.random(), 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | numberInputToObject, 3 | rgbaToHex, 4 | rgbToCmyk, 5 | rgbToHex, 6 | rgbToHsl, 7 | rgbToHsv, 8 | } from './conversion.js'; 9 | import { names } from './css-color-names.js'; 10 | import { inputToRGB } from './format-input.js'; 11 | import { CMYK, HSL, HSLA, HSV, HSVA, Numberify, RGB, RGBA } from './interfaces.js'; 12 | import { bound01, boundAlpha, clamp01 } from './util.js'; 13 | 14 | export interface TinyColorOptions { 15 | format: string; 16 | gradientType: string; 17 | } 18 | 19 | export type ColorInput = string | number | RGB | RGBA | HSL | HSLA | HSV | HSVA | CMYK | TinyColor; 20 | 21 | export type ColorFormats = 22 | | 'rgb' 23 | | 'prgb' 24 | | 'hex' 25 | | 'hex3' 26 | | 'hex4' 27 | | 'hex6' 28 | | 'hex8' 29 | | 'name' 30 | | 'hsl' 31 | | 'hsv' 32 | | 'cmyk'; 33 | 34 | export class TinyColor { 35 | /** red */ 36 | r!: number; 37 | 38 | /** green */ 39 | g!: number; 40 | 41 | /** blue */ 42 | b!: number; 43 | 44 | /** alpha */ 45 | a!: number; 46 | 47 | /** the format used to create the tinycolor instance */ 48 | format!: ColorFormats; 49 | 50 | /** input passed into the constructer used to create the tinycolor instance */ 51 | originalInput!: ColorInput; 52 | 53 | /** the color was successfully parsed */ 54 | isValid!: boolean; 55 | 56 | gradientType?: string; 57 | 58 | /** rounded alpha */ 59 | roundA!: number; 60 | 61 | constructor(color: ColorInput = '', opts: Partial = {}) { 62 | // If input is already a tinycolor, return itself 63 | if (color instanceof TinyColor) { 64 | // eslint-disable-next-line no-constructor-return 65 | return color; 66 | } 67 | 68 | if (typeof color === 'number') { 69 | color = numberInputToObject(color); 70 | } 71 | 72 | this.originalInput = color; 73 | const rgb = inputToRGB(color); 74 | this.originalInput = color; 75 | this.r = rgb.r; 76 | this.g = rgb.g; 77 | this.b = rgb.b; 78 | this.a = rgb.a; 79 | this.roundA = Math.round(100 * this.a) / 100; 80 | this.format = opts.format ?? rgb.format; 81 | this.gradientType = opts.gradientType; 82 | 83 | // Don't let the range of [0,255] come back in [0,1]. 84 | // Potentially lose a little bit of precision here, but will fix issues where 85 | // .5 gets interpreted as half of the total, instead of half of 1 86 | // If it was supposed to be 128, this was already taken care of by `inputToRgb` 87 | if (this.r < 1) { 88 | this.r = Math.round(this.r); 89 | } 90 | 91 | if (this.g < 1) { 92 | this.g = Math.round(this.g); 93 | } 94 | 95 | if (this.b < 1) { 96 | this.b = Math.round(this.b); 97 | } 98 | 99 | this.isValid = rgb.ok; 100 | } 101 | 102 | isDark(): boolean { 103 | return this.getBrightness() < 128; 104 | } 105 | 106 | isLight(): boolean { 107 | return !this.isDark(); 108 | } 109 | 110 | /** 111 | * Returns the perceived brightness of the color, from 0-255. 112 | */ 113 | getBrightness(): number { 114 | // http://www.w3.org/TR/AERT#color-contrast 115 | const rgb = this.toRgb(); 116 | return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; 117 | } 118 | 119 | /** 120 | * Returns the perceived luminance of a color, from 0-1. 121 | */ 122 | getLuminance(): number { 123 | // http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 124 | const rgb = this.toRgb(); 125 | let R; 126 | let G; 127 | let B; 128 | const RsRGB = rgb.r / 255; 129 | const GsRGB = rgb.g / 255; 130 | const BsRGB = rgb.b / 255; 131 | 132 | if (RsRGB <= 0.03928) { 133 | R = RsRGB / 12.92; 134 | } else { 135 | // eslint-disable-next-line prefer-exponentiation-operator 136 | R = Math.pow((RsRGB + 0.055) / 1.055, 2.4); 137 | } 138 | 139 | if (GsRGB <= 0.03928) { 140 | G = GsRGB / 12.92; 141 | } else { 142 | // eslint-disable-next-line prefer-exponentiation-operator 143 | G = Math.pow((GsRGB + 0.055) / 1.055, 2.4); 144 | } 145 | 146 | if (BsRGB <= 0.03928) { 147 | B = BsRGB / 12.92; 148 | } else { 149 | // eslint-disable-next-line prefer-exponentiation-operator 150 | B = Math.pow((BsRGB + 0.055) / 1.055, 2.4); 151 | } 152 | 153 | return 0.2126 * R + 0.7152 * G + 0.0722 * B; 154 | } 155 | 156 | /** 157 | * Returns the alpha value of a color, from 0-1. 158 | */ 159 | getAlpha(): number { 160 | return this.a; 161 | } 162 | 163 | /** 164 | * Sets the alpha value on the current color. 165 | * 166 | * @param alpha - The new alpha value. The accepted range is 0-1. 167 | */ 168 | setAlpha(alpha?: string | number): this { 169 | this.a = boundAlpha(alpha); 170 | this.roundA = Math.round(100 * this.a) / 100; 171 | return this; 172 | } 173 | 174 | /** 175 | * Returns whether the color is monochrome. 176 | */ 177 | isMonochrome(): boolean { 178 | const { s } = this.toHsl(); 179 | return s === 0; 180 | } 181 | 182 | /** 183 | * Returns the object as a HSVA object. 184 | */ 185 | toHsv(): Numberify { 186 | const hsv = rgbToHsv(this.r, this.g, this.b); 187 | return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this.a }; 188 | } 189 | 190 | /** 191 | * Returns the hsva values interpolated into a string with the following format: 192 | * "hsva(xxx, xxx, xxx, xx)". 193 | */ 194 | toHsvString(): string { 195 | const hsv = rgbToHsv(this.r, this.g, this.b); 196 | const h = Math.round(hsv.h * 360); 197 | const s = Math.round(hsv.s * 100); 198 | const v = Math.round(hsv.v * 100); 199 | return this.a === 1 ? `hsv(${h}, ${s}%, ${v}%)` : `hsva(${h}, ${s}%, ${v}%, ${this.roundA})`; 200 | } 201 | 202 | /** 203 | * Returns the object as a HSLA object. 204 | */ 205 | toHsl(): Numberify { 206 | const hsl = rgbToHsl(this.r, this.g, this.b); 207 | return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this.a }; 208 | } 209 | 210 | /** 211 | * Returns the hsla values interpolated into a string with the following format: 212 | * "hsla(xxx, xxx, xxx, xx)". 213 | */ 214 | toHslString(): string { 215 | const hsl = rgbToHsl(this.r, this.g, this.b); 216 | const h = Math.round(hsl.h * 360); 217 | const s = Math.round(hsl.s * 100); 218 | const l = Math.round(hsl.l * 100); 219 | return this.a === 1 ? `hsl(${h}, ${s}%, ${l}%)` : `hsla(${h}, ${s}%, ${l}%, ${this.roundA})`; 220 | } 221 | 222 | /** 223 | * Returns the hex value of the color. 224 | * @param allow3Char will shorten hex value to 3 char if possible 225 | */ 226 | toHex(allow3Char = false): string { 227 | return rgbToHex(this.r, this.g, this.b, allow3Char); 228 | } 229 | 230 | /** 231 | * Returns the hex value of the color -with a # prefixed. 232 | * @param allow3Char will shorten hex value to 3 char if possible 233 | */ 234 | toHexString(allow3Char = false): string { 235 | return '#' + this.toHex(allow3Char); 236 | } 237 | 238 | /** 239 | * Returns the hex 8 value of the color. 240 | * @param allow4Char will shorten hex value to 4 char if possible 241 | */ 242 | toHex8(allow4Char = false): string { 243 | return rgbaToHex(this.r, this.g, this.b, this.a, allow4Char); 244 | } 245 | 246 | /** 247 | * Returns the hex 8 value of the color -with a # prefixed. 248 | * @param allow4Char will shorten hex value to 4 char if possible 249 | */ 250 | toHex8String(allow4Char = false): string { 251 | return '#' + this.toHex8(allow4Char); 252 | } 253 | 254 | /** 255 | * Returns the shorter hex value of the color depends on its alpha -with a # prefixed. 256 | * @param allowShortChar will shorten hex value to 3 or 4 char if possible 257 | */ 258 | toHexShortString(allowShortChar = false): string { 259 | return this.a === 1 ? this.toHexString(allowShortChar) : this.toHex8String(allowShortChar); 260 | } 261 | 262 | /** 263 | * Returns the object as a RGBA object. 264 | */ 265 | toRgb(): Numberify { 266 | return { 267 | r: Math.round(this.r), 268 | g: Math.round(this.g), 269 | b: Math.round(this.b), 270 | a: this.a, 271 | }; 272 | } 273 | 274 | /** 275 | * Returns the RGBA values interpolated into a string with the following format: 276 | * "RGBA(xxx, xxx, xxx, xx)". 277 | */ 278 | toRgbString(): string { 279 | const r = Math.round(this.r); 280 | const g = Math.round(this.g); 281 | const b = Math.round(this.b); 282 | return this.a === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.roundA})`; 283 | } 284 | 285 | /** 286 | * Returns the object as a RGBA object. 287 | */ 288 | toPercentageRgb(): RGBA { 289 | const fmt = (x: number): string => `${Math.round(bound01(x, 255) * 100)}%`; 290 | return { 291 | r: fmt(this.r), 292 | g: fmt(this.g), 293 | b: fmt(this.b), 294 | a: this.a, 295 | }; 296 | } 297 | 298 | /** 299 | * Returns the RGBA relative values interpolated into a string 300 | */ 301 | toPercentageRgbString(): string { 302 | const rnd = (x: number): number => Math.round(bound01(x, 255) * 100); 303 | return this.a === 1 304 | ? `rgb(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%)` 305 | : `rgba(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%, ${this.roundA})`; 306 | } 307 | 308 | toCmyk(): Numberify { 309 | return { 310 | ...rgbToCmyk(this.r, this.g, this.b), 311 | }; 312 | } 313 | 314 | toCmykString(): string { 315 | const { c, m, y, k } = rgbToCmyk(this.r, this.g, this.b); 316 | return `cmyk(${c}, ${m}, ${y}, ${k})`; 317 | } 318 | 319 | /** 320 | * The 'real' name of the color -if there is one. 321 | */ 322 | toName(): string | false { 323 | if (this.a === 0) { 324 | return 'transparent'; 325 | } 326 | 327 | if (this.a < 1) { 328 | return false; 329 | } 330 | 331 | const hex = '#' + rgbToHex(this.r, this.g, this.b, false); 332 | for (const [key, value] of Object.entries(names)) { 333 | if (hex === value) { 334 | return key; 335 | } 336 | } 337 | 338 | return false; 339 | } 340 | 341 | /** 342 | * String representation of the color. 343 | * 344 | * @param format - The format to be used when displaying the string representation. 345 | */ 346 | toString(format: T): boolean | string; 347 | toString(format?: T): string; 348 | toString(format?: ColorFormats): string | false { 349 | const formatSet = Boolean(format); 350 | format = format ?? this.format; 351 | 352 | let formattedString: string | false = false; 353 | const hasAlpha = this.a < 1 && this.a >= 0; 354 | const needsAlphaFormat = 355 | !formatSet && hasAlpha && (format.startsWith('hex') || format === 'name'); 356 | 357 | if (needsAlphaFormat) { 358 | // Special case for "transparent", all other non-alpha formats 359 | // will return rgba when there is transparency. 360 | if (format === 'name' && this.a === 0) { 361 | return this.toName(); 362 | } 363 | 364 | return this.toRgbString(); 365 | } 366 | 367 | if (format === 'rgb') { 368 | formattedString = this.toRgbString(); 369 | } 370 | 371 | if (format === 'prgb') { 372 | formattedString = this.toPercentageRgbString(); 373 | } 374 | 375 | if (format === 'hex' || format === 'hex6') { 376 | formattedString = this.toHexString(); 377 | } 378 | 379 | if (format === 'hex3') { 380 | formattedString = this.toHexString(true); 381 | } 382 | 383 | if (format === 'hex4') { 384 | formattedString = this.toHex8String(true); 385 | } 386 | 387 | if (format === 'hex8') { 388 | formattedString = this.toHex8String(); 389 | } 390 | 391 | if (format === 'name') { 392 | formattedString = this.toName(); 393 | } 394 | 395 | if (format === 'hsl') { 396 | formattedString = this.toHslString(); 397 | } 398 | 399 | if (format === 'hsv') { 400 | formattedString = this.toHsvString(); 401 | } 402 | 403 | if (format === 'cmyk') { 404 | formattedString = this.toCmykString(); 405 | } 406 | 407 | return formattedString || this.toHexString(); 408 | } 409 | 410 | toNumber(): number { 411 | return (Math.round(this.r) << 16) + (Math.round(this.g) << 8) + Math.round(this.b); 412 | } 413 | 414 | clone(): TinyColor { 415 | return new TinyColor(this.toString()); 416 | } 417 | 418 | /** 419 | * Lighten the color a given amount. Providing 100 will always return white. 420 | * @param amount - valid between 1-100 421 | */ 422 | lighten(amount = 10): TinyColor { 423 | const hsl = this.toHsl(); 424 | hsl.l += amount / 100; 425 | hsl.l = clamp01(hsl.l); 426 | return new TinyColor(hsl); 427 | } 428 | 429 | /** 430 | * Brighten the color a given amount, from 0 to 100. 431 | * @param amount - valid between 1-100 432 | */ 433 | brighten(amount = 10): TinyColor { 434 | const rgb = this.toRgb(); 435 | rgb.r = Math.max(0, Math.min(255, rgb.r - Math.round(255 * -(amount / 100)))); 436 | rgb.g = Math.max(0, Math.min(255, rgb.g - Math.round(255 * -(amount / 100)))); 437 | rgb.b = Math.max(0, Math.min(255, rgb.b - Math.round(255 * -(amount / 100)))); 438 | return new TinyColor(rgb); 439 | } 440 | 441 | /** 442 | * Darken the color a given amount, from 0 to 100. 443 | * Providing 100 will always return black. 444 | * @param amount - valid between 1-100 445 | */ 446 | darken(amount = 10): TinyColor { 447 | const hsl = this.toHsl(); 448 | hsl.l -= amount / 100; 449 | hsl.l = clamp01(hsl.l); 450 | return new TinyColor(hsl); 451 | } 452 | 453 | /** 454 | * Mix the color with pure white, from 0 to 100. 455 | * Providing 0 will do nothing, providing 100 will always return white. 456 | * @param amount - valid between 1-100 457 | */ 458 | tint(amount = 10): TinyColor { 459 | return this.mix('white', amount); 460 | } 461 | 462 | /** 463 | * Mix the color with pure black, from 0 to 100. 464 | * Providing 0 will do nothing, providing 100 will always return black. 465 | * @param amount - valid between 1-100 466 | */ 467 | shade(amount = 10): TinyColor { 468 | return this.mix('black', amount); 469 | } 470 | 471 | /** 472 | * Desaturate the color a given amount, from 0 to 100. 473 | * Providing 100 will is the same as calling greyscale 474 | * @param amount - valid between 1-100 475 | */ 476 | desaturate(amount = 10): TinyColor { 477 | const hsl = this.toHsl(); 478 | hsl.s -= amount / 100; 479 | hsl.s = clamp01(hsl.s); 480 | return new TinyColor(hsl); 481 | } 482 | 483 | /** 484 | * Saturate the color a given amount, from 0 to 100. 485 | * @param amount - valid between 1-100 486 | */ 487 | saturate(amount = 10): TinyColor { 488 | const hsl = this.toHsl(); 489 | hsl.s += amount / 100; 490 | hsl.s = clamp01(hsl.s); 491 | return new TinyColor(hsl); 492 | } 493 | 494 | /** 495 | * Completely desaturates a color into greyscale. 496 | * Same as calling `desaturate(100)` 497 | */ 498 | greyscale(): TinyColor { 499 | return this.desaturate(100); 500 | } 501 | 502 | /** 503 | * Spin takes a positive or negative amount within [-360, 360] indicating the change of hue. 504 | * Values outside of this range will be wrapped into this range. 505 | */ 506 | spin(amount: number): TinyColor { 507 | const hsl = this.toHsl(); 508 | const hue = (hsl.h + amount) % 360; 509 | hsl.h = hue < 0 ? 360 + hue : hue; 510 | return new TinyColor(hsl); 511 | } 512 | 513 | /** 514 | * Mix the current color a given amount with another color, from 0 to 100. 515 | * 0 means no mixing (return current color). 516 | */ 517 | mix(color: ColorInput, amount = 50): TinyColor { 518 | const rgb1 = this.toRgb(); 519 | const rgb2 = new TinyColor(color).toRgb(); 520 | 521 | const p = amount / 100; 522 | const rgba = { 523 | r: (rgb2.r - rgb1.r) * p + rgb1.r, 524 | g: (rgb2.g - rgb1.g) * p + rgb1.g, 525 | b: (rgb2.b - rgb1.b) * p + rgb1.b, 526 | a: (rgb2.a - rgb1.a) * p + rgb1.a, 527 | }; 528 | 529 | return new TinyColor(rgba); 530 | } 531 | 532 | analogous(results = 6, slices = 30): TinyColor[] { 533 | const hsl = this.toHsl(); 534 | const part = 360 / slices; 535 | const ret: TinyColor[] = [this]; 536 | 537 | for (hsl.h = (hsl.h - ((part * results) >> 1) + 720) % 360; --results; ) { 538 | hsl.h = (hsl.h + part) % 360; 539 | ret.push(new TinyColor(hsl)); 540 | } 541 | 542 | return ret; 543 | } 544 | 545 | /** 546 | * taken from https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js 547 | */ 548 | complement(): TinyColor { 549 | const hsl = this.toHsl(); 550 | hsl.h = (hsl.h + 180) % 360; 551 | return new TinyColor(hsl); 552 | } 553 | 554 | monochromatic(results = 6): TinyColor[] { 555 | const hsv = this.toHsv(); 556 | const { h } = hsv; 557 | const { s } = hsv; 558 | let { v } = hsv; 559 | const res: TinyColor[] = []; 560 | const modification = 1 / results; 561 | 562 | while (results--) { 563 | res.push(new TinyColor({ h, s, v })); 564 | v = (v + modification) % 1; 565 | } 566 | 567 | return res; 568 | } 569 | 570 | splitcomplement(): TinyColor[] { 571 | const hsl = this.toHsl(); 572 | const { h } = hsl; 573 | return [ 574 | this, 575 | new TinyColor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l }), 576 | new TinyColor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l }), 577 | ]; 578 | } 579 | 580 | /** 581 | * Compute how the color would appear on a background 582 | */ 583 | onBackground(background: ColorInput): TinyColor { 584 | const fg = this.toRgb(); 585 | const bg = new TinyColor(background).toRgb(); 586 | const alpha = fg.a + bg.a * (1 - fg.a); 587 | 588 | return new TinyColor({ 589 | r: (fg.r * fg.a + bg.r * bg.a * (1 - fg.a)) / alpha, 590 | g: (fg.g * fg.a + bg.g * bg.a * (1 - fg.a)) / alpha, 591 | b: (fg.b * fg.a + bg.b * bg.a * (1 - fg.a)) / alpha, 592 | a: alpha, 593 | }); 594 | } 595 | 596 | /** 597 | * Alias for `polyad(3)` 598 | */ 599 | triad(): TinyColor[] { 600 | return this.polyad(3); 601 | } 602 | 603 | /** 604 | * Alias for `polyad(4)` 605 | */ 606 | tetrad(): TinyColor[] { 607 | return this.polyad(4); 608 | } 609 | 610 | /** 611 | * Get polyad colors, like (for 1, 2, 3, 4, 5, 6, 7, 8, etc...) 612 | * monad, dyad, triad, tetrad, pentad, hexad, heptad, octad, etc... 613 | */ 614 | polyad(n: number): TinyColor[] { 615 | const hsl = this.toHsl(); 616 | const { h } = hsl; 617 | 618 | const result: TinyColor[] = [this]; 619 | const increment = 360 / n; 620 | for (let i = 1; i < n; i++) { 621 | result.push(new TinyColor({ h: (h + i * increment) % 360, s: hsl.s, l: hsl.l })); 622 | } 623 | 624 | return result; 625 | } 626 | 627 | /** 628 | * compare color vs current color 629 | */ 630 | equals(color?: ColorInput): boolean { 631 | const comparedColor = new TinyColor(color); 632 | 633 | /** 634 | * RGB and CMYK do not have the same color gamut, so a CMYK conversion will never be 100%. 635 | * This means we need to compare CMYK to CMYK to ensure accuracy of the equals function. 636 | */ 637 | if (this.format === 'cmyk' || comparedColor.format === 'cmyk') { 638 | return this.toCmykString() === comparedColor.toCmykString(); 639 | } 640 | 641 | return this.toRgbString() === comparedColor.toRgbString(); 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * convert all properties in an interface to a number 3 | */ 4 | export type Numberify = { 5 | [P in keyof T]: number; 6 | }; 7 | 8 | /** 9 | * A representation of additive color mixing. 10 | * Projection of primary color lights on a white screen shows secondary 11 | * colors where two overlap; the combination of all three of red, green, 12 | * and blue in equal intensities makes white. 13 | */ 14 | export interface RGB { 15 | r: number | string; 16 | g: number | string; 17 | b: number | string; 18 | } 19 | 20 | export interface RGBA extends RGB { 21 | a: number; 22 | } 23 | 24 | /** 25 | * The HSL model describes colors in terms of hue, saturation, 26 | * and lightness (also called luminance). 27 | * @link https://en.wikibooks.org/wiki/Color_Models:_RGB,_HSV,_HSL#HSL 28 | */ 29 | export interface HSL { 30 | h: number | string; 31 | s: number | string; 32 | l: number | string; 33 | } 34 | 35 | export interface HSLA extends HSL { 36 | a: number; 37 | } 38 | 39 | /** 40 | * The HSV, or HSB, model describes colors in terms of 41 | * hue, saturation, and value (brightness). 42 | * @link https://en.wikibooks.org/wiki/Color_Models:_RGB,_HSV,_HSL#HSV 43 | */ 44 | export interface HSV { 45 | h: number | string; 46 | s: number | string; 47 | v: number | string; 48 | } 49 | 50 | export interface HSVA extends HSV { 51 | a: number; 52 | } 53 | 54 | /** 55 | * The CMYK color model is a subtractive color model used in the printing process. 56 | * It described four ink palettes: Cyan, Magenta, Yellow, and Black. 57 | * @link https://en.wikipedia.org/wiki/CMYK_color_model 58 | */ 59 | export interface CMYK { 60 | c: number | string; 61 | m: number | string; 62 | y: number | string; 63 | k: number | string; 64 | } 65 | -------------------------------------------------------------------------------- /src/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './index.js'; 2 | export * from './css-color-names.js'; 3 | export * from './readability.js'; 4 | export * from './to-ms-filter.js'; 5 | export * from './from-ratio.js'; 6 | export * from './format-input.js'; 7 | export * from './random.js'; 8 | export * from './interfaces.js'; 9 | export * from './conversion.js'; 10 | -------------------------------------------------------------------------------- /src/random.ts: -------------------------------------------------------------------------------- 1 | // randomColor by David Merfield under the CC0 license 2 | // https://github.com/davidmerfield/randomColor/ 3 | import { TinyColor } from './index.js'; 4 | import { HSVA } from './interfaces.js'; 5 | 6 | export interface RandomOptions { 7 | seed?: number; 8 | hue?: 9 | | number 10 | | string 11 | | 'red' 12 | | 'orange' 13 | | 'yellow' 14 | | 'green' 15 | | 'blue' 16 | | 'purple' 17 | | 'pink' 18 | | 'monochrome'; 19 | luminosity?: 'random' | 'bright' | 'dark' | 'light'; 20 | alpha?: number; 21 | } 22 | 23 | export interface RandomCountOptions extends RandomOptions { 24 | count?: number | null; 25 | } 26 | 27 | export function random(options?: RandomOptions): TinyColor; 28 | export function random(options?: RandomCountOptions): TinyColor[]; 29 | export function random(options: RandomOptions | RandomCountOptions = {}): TinyColor | TinyColor[] { 30 | // Check if we need to generate multiple colors 31 | if ( 32 | (options as RandomCountOptions).count !== undefined && 33 | (options as RandomCountOptions).count !== null 34 | ) { 35 | const totalColors: number = (options as RandomCountOptions).count!; 36 | const colors: TinyColor[] = []; 37 | 38 | (options as RandomCountOptions).count = undefined; 39 | 40 | while (totalColors > colors.length) { 41 | // Since we're generating multiple colors, 42 | // incremement the seed. Otherwise we'd just 43 | // generate the same color each time... 44 | (options as RandomCountOptions).count = null; 45 | if (options.seed) { 46 | options.seed += 1; 47 | } 48 | 49 | colors.push(random(options as RandomOptions)); 50 | } 51 | 52 | (options as RandomCountOptions).count = totalColors; 53 | return colors; 54 | } 55 | 56 | // First we pick a hue (H) 57 | const h = pickHue(options.hue, options.seed); 58 | 59 | // Then use H to determine saturation (S) 60 | const s = pickSaturation(h, options); 61 | 62 | // Then use S and H to determine brightness (B). 63 | const v = pickBrightness(h, s, options); 64 | const res: Partial = { h, s, v }; 65 | if (options.alpha !== undefined) { 66 | res.a = options.alpha; 67 | } 68 | 69 | // Then we return the HSB color in the desired format 70 | return new TinyColor(res as HSVA); 71 | } 72 | 73 | function pickHue(hue: number | string | undefined, seed?: number): number { 74 | const hueRange = getHueRange(hue); 75 | let res = randomWithin(hueRange, seed); 76 | 77 | // Instead of storing red as two seperate ranges, 78 | // we group them, using negative numbers 79 | if (res < 0) { 80 | res = 360 + res; 81 | } 82 | 83 | return res; 84 | } 85 | 86 | function pickSaturation(hue: number, options: RandomOptions): number { 87 | if (options.hue === 'monochrome') { 88 | return 0; 89 | } 90 | 91 | if (options.luminosity === 'random') { 92 | return randomWithin([0, 100], options.seed); 93 | } 94 | 95 | const { saturationRange } = getColorInfo(hue); 96 | 97 | let sMin = saturationRange[0]; 98 | let sMax = saturationRange[1]; 99 | 100 | switch (options.luminosity) { 101 | case 'bright': 102 | sMin = 55; 103 | break; 104 | case 'dark': 105 | sMin = sMax - 10; 106 | break; 107 | case 'light': 108 | sMax = 55; 109 | break; 110 | default: 111 | break; 112 | } 113 | 114 | return randomWithin([sMin, sMax], options.seed); 115 | } 116 | 117 | function pickBrightness(H: number, S: number, options: RandomOptions): number { 118 | let bMin = getMinimumBrightness(H, S); 119 | let bMax = 100; 120 | 121 | switch (options.luminosity) { 122 | case 'dark': 123 | bMax = bMin + 20; 124 | break; 125 | case 'light': 126 | bMin = (bMax + bMin) / 2; 127 | break; 128 | case 'random': 129 | bMin = 0; 130 | bMax = 100; 131 | break; 132 | default: 133 | break; 134 | } 135 | 136 | return randomWithin([bMin, bMax], options.seed); 137 | } 138 | 139 | function getMinimumBrightness(H: number, S: number): number { 140 | const { lowerBounds } = getColorInfo(H); 141 | 142 | for (let i = 0; i < lowerBounds.length - 1; i++) { 143 | const s1 = lowerBounds[i][0]; 144 | const v1 = lowerBounds[i][1]; 145 | 146 | const s2 = lowerBounds[i + 1][0]; 147 | const v2 = lowerBounds[i + 1][1]; 148 | 149 | if (S >= s1 && S <= s2) { 150 | const m = (v2 - v1) / (s2 - s1); 151 | const b = v1 - m * s1; 152 | 153 | return m * S + b; 154 | } 155 | } 156 | 157 | return 0; 158 | } 159 | 160 | function getHueRange(colorInput?: number | string): [number, number] { 161 | const num = parseInt(colorInput as string, 10); 162 | if (!Number.isNaN(num) && num < 360 && num > 0) { 163 | return [num, num]; 164 | } 165 | 166 | if (typeof colorInput === 'string') { 167 | const namedColor = bounds.find(n => n.name === colorInput); 168 | if (namedColor) { 169 | const color = defineColor(namedColor); 170 | if (color.hueRange) { 171 | return color.hueRange; 172 | } 173 | } 174 | 175 | const parsed = new TinyColor(colorInput); 176 | if (parsed.isValid) { 177 | const hue = parsed.toHsv().h; 178 | return [hue, hue]; 179 | } 180 | } 181 | 182 | return [0, 360]; 183 | } 184 | 185 | function getColorInfo(hue: number): { 186 | name: string; 187 | hueRange: [number, number] | null; 188 | lowerBounds: Array<[number, number]>; 189 | saturationRange: number[]; 190 | brightnessRange: number[]; 191 | } { 192 | // Maps red colors to make picking hue easier 193 | if (hue >= 334 && hue <= 360) { 194 | hue -= 360; 195 | } 196 | 197 | for (const bound of bounds) { 198 | const color = defineColor(bound); 199 | if (color.hueRange && hue >= color.hueRange[0] && hue <= color.hueRange[1]) { 200 | return color; 201 | } 202 | } 203 | 204 | throw Error('Color not found'); 205 | } 206 | 207 | function randomWithin(range: [number, number], seed?: number): number { 208 | if (seed === undefined) { 209 | return Math.floor(range[0] + Math.random() * (range[1] + 1 - range[0])); 210 | } 211 | 212 | // Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ 213 | const max = range[1] || 1; 214 | const min = range[0] || 0; 215 | seed = (seed * 9301 + 49297) % 233280; 216 | const rnd = seed / 233280.0; 217 | return Math.floor(min + rnd * (max - min)); 218 | } 219 | 220 | function defineColor(bound: ColorBound): { 221 | name: string; 222 | hueRange: [number, number] | null; 223 | lowerBounds: Array<[number, number]>; 224 | saturationRange: number[]; 225 | brightnessRange: number[]; 226 | } { 227 | const sMin = bound.lowerBounds[0][0]; 228 | const sMax = bound.lowerBounds[bound.lowerBounds.length - 1][0]; 229 | const bMin = bound.lowerBounds[bound.lowerBounds.length - 1][1]; 230 | const bMax = bound.lowerBounds[0][1]; 231 | 232 | return { 233 | name: bound.name, 234 | hueRange: bound.hueRange, 235 | lowerBounds: bound.lowerBounds, 236 | saturationRange: [sMin, sMax], 237 | brightnessRange: [bMin, bMax], 238 | }; 239 | } 240 | 241 | /** 242 | * @hidden 243 | */ 244 | export interface ColorBound { 245 | name: string; 246 | hueRange: [number, number] | null; 247 | lowerBounds: Array<[number, number]>; 248 | } 249 | 250 | /** 251 | * @hidden 252 | */ 253 | export const bounds: ColorBound[] = [ 254 | { 255 | name: 'monochrome', 256 | hueRange: null, 257 | lowerBounds: [ 258 | [0, 0], 259 | [100, 0], 260 | ], 261 | }, 262 | { 263 | name: 'red', 264 | hueRange: [-26, 18], 265 | lowerBounds: [ 266 | [20, 100], 267 | [30, 92], 268 | [40, 89], 269 | [50, 85], 270 | [60, 78], 271 | [70, 70], 272 | [80, 60], 273 | [90, 55], 274 | [100, 50], 275 | ], 276 | }, 277 | { 278 | name: 'orange', 279 | hueRange: [19, 46], 280 | lowerBounds: [ 281 | [20, 100], 282 | [30, 93], 283 | [40, 88], 284 | [50, 86], 285 | [60, 85], 286 | [70, 70], 287 | [100, 70], 288 | ], 289 | }, 290 | { 291 | name: 'yellow', 292 | hueRange: [47, 62], 293 | lowerBounds: [ 294 | [25, 100], 295 | [40, 94], 296 | [50, 89], 297 | [60, 86], 298 | [70, 84], 299 | [80, 82], 300 | [90, 80], 301 | [100, 75], 302 | ], 303 | }, 304 | { 305 | name: 'green', 306 | hueRange: [63, 178], 307 | lowerBounds: [ 308 | [30, 100], 309 | [40, 90], 310 | [50, 85], 311 | [60, 81], 312 | [70, 74], 313 | [80, 64], 314 | [90, 50], 315 | [100, 40], 316 | ], 317 | }, 318 | { 319 | name: 'blue', 320 | hueRange: [179, 257], 321 | lowerBounds: [ 322 | [20, 100], 323 | [30, 86], 324 | [40, 80], 325 | [50, 74], 326 | [60, 60], 327 | [70, 52], 328 | [80, 44], 329 | [90, 39], 330 | [100, 35], 331 | ], 332 | }, 333 | { 334 | name: 'purple', 335 | hueRange: [258, 282], 336 | lowerBounds: [ 337 | [20, 100], 338 | [30, 87], 339 | [40, 79], 340 | [50, 70], 341 | [60, 65], 342 | [70, 59], 343 | [80, 52], 344 | [90, 45], 345 | [100, 42], 346 | ], 347 | }, 348 | { 349 | name: 'pink', 350 | hueRange: [283, 334], 351 | lowerBounds: [ 352 | [20, 100], 353 | [30, 90], 354 | [40, 86], 355 | [60, 84], 356 | [80, 80], 357 | [90, 75], 358 | [100, 73], 359 | ], 360 | }, 361 | ]; 362 | -------------------------------------------------------------------------------- /src/readability.ts: -------------------------------------------------------------------------------- 1 | import { ColorInput, TinyColor } from './index.js'; 2 | 3 | // Readability Functions 4 | // --------------------- 5 | // false 36 | * new TinyColor().isReadable('#000', '#111', { level: 'AA', size: 'large' }) => false 37 | * ``` 38 | */ 39 | export function isReadable( 40 | color1: ColorInput, 41 | color2: ColorInput, 42 | wcag2: WCAG2Parms = { level: 'AA', size: 'small' }, 43 | ): boolean { 44 | const readabilityLevel = readability(color1, color2); 45 | switch ((wcag2.level ?? 'AA') + (wcag2.size ?? 'small')) { 46 | case 'AAsmall': 47 | case 'AAAlarge': 48 | return readabilityLevel >= 4.5; 49 | case 'AAlarge': 50 | return readabilityLevel >= 3; 51 | case 'AAAsmall': 52 | return readabilityLevel >= 7; 53 | default: 54 | return false; 55 | } 56 | } 57 | 58 | export interface WCAG2FallbackParms extends WCAG2Parms { 59 | includeFallbackColors?: boolean; 60 | } 61 | 62 | /** 63 | * Given a base color and a list of possible foreground or background 64 | * colors for that base, returns the most readable color. 65 | * Optionally returns Black or White if the most readable color is unreadable. 66 | * 67 | * @param baseColor - the base color. 68 | * @param colorList - array of colors to pick the most readable one from. 69 | * @param args - and object with extra arguments 70 | * 71 | * Example 72 | * ```ts 73 | * new TinyColor().mostReadable('#123', ['#124", "#125'], { includeFallbackColors: false }).toHexString(); // "#112255" 74 | * new TinyColor().mostReadable('#123', ['#124", "#125'],{ includeFallbackColors: true }).toHexString(); // "#ffffff" 75 | * new TinyColor().mostReadable('#a8015a', ["#faf3f3"], { includeFallbackColors:true, level: 'AAA', size: 'large' }).toHexString(); // "#faf3f3" 76 | * new TinyColor().mostReadable('#a8015a', ["#faf3f3"], { includeFallbackColors:true, level: 'AAA', size: 'small' }).toHexString(); // "#ffffff" 77 | * ``` 78 | */ 79 | export function mostReadable( 80 | baseColor: ColorInput, 81 | colorList: ColorInput[], 82 | args: WCAG2FallbackParms = { includeFallbackColors: false, level: 'AA', size: 'small' }, 83 | ): TinyColor | null { 84 | let bestColor: TinyColor | null = null; 85 | let bestScore = 0; 86 | const { includeFallbackColors, level, size } = args; 87 | 88 | for (const color of colorList) { 89 | const score = readability(baseColor, color); 90 | if (score > bestScore) { 91 | bestScore = score; 92 | bestColor = new TinyColor(color); 93 | } 94 | } 95 | 96 | if (isReadable(baseColor, bestColor!, { level, size }) || !includeFallbackColors) { 97 | return bestColor; 98 | } 99 | 100 | args.includeFallbackColors = false; 101 | return mostReadable(baseColor, ['#fff', '#000'], args); 102 | } 103 | -------------------------------------------------------------------------------- /src/to-ms-filter.ts: -------------------------------------------------------------------------------- 1 | import { rgbaToArgbHex } from './conversion.js'; 2 | import { ColorInput, TinyColor } from './index.js'; 3 | /** 4 | * Returns the color represented as a Microsoft filter for use in old versions of IE. 5 | */ 6 | export function toMsFilter(firstColor: ColorInput, secondColor?: ColorInput): string { 7 | const color = new TinyColor(firstColor); 8 | const hex8String = '#' + rgbaToArgbHex(color.r, color.g, color.b, color.a); 9 | let secondHex8String = hex8String; 10 | const gradientType: string = color.gradientType ? 'GradientType = 1, ' : ''; 11 | 12 | if (secondColor) { 13 | const s = new TinyColor(secondColor); 14 | secondHex8String = '#' + rgbaToArgbHex(s.r, s.g, s.b, s.a); 15 | } 16 | 17 | return `progid:DXImageTransform.Microsoft.gradient(${gradientType}startColorstr=${hex8String},endColorstr=${secondHex8String})`; 18 | } 19 | -------------------------------------------------------------------------------- /src/umd_api.ts: -------------------------------------------------------------------------------- 1 | import { names } from './css-color-names.js'; 2 | import { inputToRGB, isValidCSSUnit, stringInputToObject } from './format-input.js'; 3 | import { fromRatio, legacyRandom } from './from-ratio.js'; 4 | import { TinyColor } from './index.js'; 5 | import { random } from './random.js'; 6 | import { mostReadable, readability } from './readability.js'; 7 | import { toMsFilter } from './to-ms-filter.js'; 8 | 9 | export interface TinyColorUMD { 10 | TinyColor: typeof TinyColor; 11 | readability: typeof readability; 12 | random: typeof random; 13 | names: typeof names; 14 | fromRatio: typeof fromRatio; 15 | legacyRandom: typeof legacyRandom; 16 | toMsFilter: typeof toMsFilter; 17 | inputToRGB: typeof inputToRGB; 18 | stringInputToObject: typeof stringInputToObject; 19 | isValidCSSUnit: typeof isValidCSSUnit; 20 | mostReadable: typeof mostReadable; 21 | } 22 | const tinycolorumd: TinyColorUMD = { 23 | TinyColor, 24 | readability, 25 | mostReadable, 26 | random, 27 | names, 28 | fromRatio, 29 | legacyRandom, 30 | toMsFilter, 31 | inputToRGB, 32 | stringInputToObject, 33 | isValidCSSUnit, 34 | }; 35 | 36 | export default tinycolorumd; 37 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Take input from [0, n] and return it as [0, 1] 3 | * @hidden 4 | */ 5 | export function bound01(n: any, max: number): number { 6 | if (isOnePointZero(n)) { 7 | n = '100%'; 8 | } 9 | 10 | const isPercent = isPercentage(n); 11 | n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n))); 12 | 13 | // Automatically convert percentage into number 14 | if (isPercent) { 15 | n = parseInt(String(n * max), 10) / 100; 16 | } 17 | 18 | // Handle floating point rounding errors 19 | if (Math.abs(n - max) < 0.000001) { 20 | return 1; 21 | } 22 | 23 | // Convert into [0, 1] range if it isn't already 24 | if (max === 360) { 25 | // If n is a hue given in degrees, 26 | // wrap around out-of-range values into [0, 360] range 27 | // then convert into [0, 1]. 28 | n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max)); 29 | } else { 30 | // If n not a hue given in degrees 31 | // Convert into [0, 1] range if it isn't already. 32 | n = (n % max) / parseFloat(String(max)); 33 | } 34 | 35 | return n; 36 | } 37 | 38 | /** 39 | * Force a number between 0 and 1 40 | * @hidden 41 | */ 42 | export function clamp01(val: number): number { 43 | return Math.min(1, Math.max(0, val)); 44 | } 45 | 46 | /** 47 | * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 48 | * 49 | * @hidden 50 | */ 51 | export function isOnePointZero(n: string | number): boolean { 52 | return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1; 53 | } 54 | 55 | /** 56 | * Check to see if string passed in is a percentage 57 | * @hidden 58 | */ 59 | export function isPercentage(n: string | number): boolean { 60 | return typeof n === 'string' && n.indexOf('%') !== -1; 61 | } 62 | 63 | /** 64 | * Return a valid alpha value [0,1] with all invalid values being set to 1 65 | * @hidden 66 | */ 67 | export function boundAlpha(a?: number | string): number { 68 | a = parseFloat(a as string); 69 | 70 | if (isNaN(a) || a < 0 || a > 1) { 71 | a = 1; 72 | } 73 | 74 | return a; 75 | } 76 | 77 | /** 78 | * Replace a decimal with it's percentage value 79 | * @hidden 80 | */ 81 | export function convertToPercentage(n: number | string): number | string { 82 | if (Number(n) <= 1) { 83 | return `${Number(n) * 100}%`; 84 | } 85 | 86 | return n; 87 | } 88 | 89 | /** 90 | * Force a hex value to have 2 characters 91 | * @hidden 92 | */ 93 | export function pad2(c: string): string { 94 | return c.length === 1 ? '0' + c : String(c); 95 | } 96 | -------------------------------------------------------------------------------- /test/conversions.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { TinyColor } from '../src/public_api.js'; 4 | 5 | import conversions from './conversions.js'; 6 | 7 | describe('TinyColor Conversions', () => { 8 | it('should have color equality', () => { 9 | expect(conversions.length).toBe(16); 10 | for (const c of conversions) { 11 | const tiny = new TinyColor(c.hex); 12 | expect(tiny.isValid).toBe(true); 13 | expect(new TinyColor(c.rgb).equals(c.hex)).toBe(true); 14 | expect(new TinyColor(c.rgb).equals(c.hex8)).toBe(true); 15 | expect(new TinyColor(c.rgb).equals(c.hsl)).toBe(true); 16 | expect(new TinyColor(c.rgb).equals(c.hsv)).toBe(true); 17 | expect(new TinyColor(c.rgb).equals(c.rgb)).toBe(true); 18 | expect(new TinyColor(c.hex).equals(c.hex)).toBe(true); 19 | expect(new TinyColor(c.hex).equals(c.hex8)).toBe(true); 20 | expect(new TinyColor(c.hex).equals(c.hsl)).toBe(true); 21 | expect(new TinyColor(c.hex).equals(c.hsv)).toBe(true); 22 | expect(new TinyColor(c.hsl).equals(c.hsv)).toBe(true); 23 | expect(new TinyColor(c.cmyk).equals(c.hex)).toBe(true); 24 | } 25 | }); 26 | it('HSL Object', () => { 27 | for (const c of conversions) { 28 | const tiny = new TinyColor(c.hex); 29 | expect(tiny.toHexString()).toBe(new TinyColor(tiny.toHsl()).toHexString()); 30 | } 31 | }); 32 | it('HSL String', () => { 33 | for (const c of conversions) { 34 | const tiny = new TinyColor(c.hex); 35 | const input = tiny.toRgb(); 36 | const output = new TinyColor(tiny.toHslString()).toRgb(); 37 | const maxDiff = 2; 38 | 39 | // toHslString red value difference <= ' + maxDiff 40 | expect(Math.abs(input.r - output.r) <= maxDiff).toBe(true); 41 | // toHslString green value difference <= ' + maxDiff 42 | expect(Math.abs(input.g - output.g) <= maxDiff).toBe(true); 43 | // toHslString blue value difference <= ' + maxDiff 44 | expect(Math.abs(input.b - output.b) <= maxDiff).toBe(true); 45 | } 46 | }); 47 | it('HSV String', () => { 48 | for (const c of conversions) { 49 | const tiny = new TinyColor(c.hex); 50 | const input = tiny.toRgb(); 51 | const output = new TinyColor(tiny.toHsvString()).toRgb(); 52 | const maxDiff = 2; 53 | 54 | // toHsvString red value difference <= ' + maxDiff 55 | expect(Math.abs(input.r - output.r) <= maxDiff).toBe(true); 56 | // toHsvString green value difference <= ' + maxDiff 57 | expect(Math.abs(input.g - output.g) <= maxDiff).toBe(true); 58 | // toHsvString blue value difference <= ' + maxDiff 59 | expect(Math.abs(input.b - output.b) <= maxDiff).toBe(true); 60 | } 61 | }); 62 | 63 | it('HSV Object', () => { 64 | for (const c of conversions) { 65 | const tiny = new TinyColor(c.hsv); 66 | expect(tiny.toHexString()).toBe(new TinyColor(tiny.toHsv()).toHexString()); 67 | } 68 | }); 69 | 70 | it('RGB Object', () => { 71 | for (const c of conversions) { 72 | const tiny = new TinyColor(c.hex); 73 | expect(tiny.toHexString()).toBe(new TinyColor(tiny.toRgb()).toHexString()); 74 | } 75 | }); 76 | 77 | it('RGB String', () => { 78 | for (const c of conversions) { 79 | const tiny = new TinyColor(c.hex); 80 | expect(tiny.toHexString()).toBe(new TinyColor(tiny.toRgbString()).toHexString()); 81 | } 82 | }); 83 | 84 | it('PRGB Object', () => { 85 | for (const c of conversions) { 86 | const tiny = new TinyColor(c.hex); 87 | const input = tiny.toRgb(); 88 | const output = new TinyColor(tiny.toPercentageRgb()).toRgb(); 89 | const maxDiff = 2; 90 | 91 | expect(Math.abs(input.r - output.r)).toBeLessThanOrEqual(maxDiff); 92 | expect(Math.abs(input.g - output.g)).toBeLessThanOrEqual(maxDiff); 93 | expect(Math.abs(input.b - output.b)).toBeLessThanOrEqual(maxDiff); 94 | } 95 | }); 96 | 97 | it('PRGB String', () => { 98 | for (const c of conversions) { 99 | const tiny = new TinyColor(c.hex); 100 | const input = tiny.toRgb(); 101 | const output = new TinyColor(tiny.toPercentageRgbString()).toRgb(); 102 | const maxDiff = 2; 103 | 104 | expect(Math.abs(input.r - output.r)).toBeLessThanOrEqual(maxDiff); 105 | expect(Math.abs(input.g - output.g)).toBeLessThanOrEqual(maxDiff); 106 | expect(Math.abs(input.b - output.b)).toBeLessThanOrEqual(maxDiff); 107 | } 108 | }); 109 | it('Object', () => { 110 | for (const c of conversions) { 111 | const tiny = new TinyColor(c.hex); 112 | expect(tiny.toHexString()).toBe(new TinyColor(tiny).toHexString()); 113 | } 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/conversions.ts: -------------------------------------------------------------------------------- 1 | // Taken from convertWikipediaColors.html 2 | export default [ 3 | { 4 | hex: '#FFFFFF', 5 | hex8: '#FFFFFFFF', 6 | rgb: { r: '100.0%', g: '100.0%', b: '100.0%' }, 7 | hsv: { h: '0', s: '0.000', v: '1.000' }, 8 | hsl: { h: '0', s: '0.000', l: '1.000' }, 9 | cmyk: { c: '0', m: '0', y: '0', k: '0' }, 10 | }, 11 | { 12 | hex: '#808080', 13 | hex8: '#808080FF', 14 | rgb: { r: '050.0%', g: '050.0%', b: '050.0%' }, 15 | hsv: { h: '0', s: '0.000', v: '0.500' }, 16 | hsl: { h: '0', s: '0.000', l: '0.500' }, 17 | cmyk: { c: '0', m: '0', y: '0', k: '50' }, 18 | }, 19 | { 20 | hex: '#000000', 21 | hex8: '#000000FF', 22 | rgb: { r: '000.0%', g: '000.0%', b: '000.0%' }, 23 | hsv: { h: '0', s: '0.000', v: '0.000' }, 24 | hsl: { h: '0', s: '0.000', l: '0.000' }, 25 | cmyk: { c: '0', m: '0', y: '0', k: '100' }, 26 | }, 27 | { 28 | hex: '#FF0000', 29 | hex8: '#FF0000FF', 30 | rgb: { r: '100.0%', g: '000.0%', b: '000.0%' }, 31 | hsv: { h: '0.0', s: '1.000', v: '1.000' }, 32 | hsl: { h: '0.0', s: '1.000', l: '0.500' }, 33 | cmyk: { c: '0', m: '100', y: '100', k: '0' }, 34 | }, 35 | { 36 | hex: '#BFBF00', 37 | hex8: '#BFBF00FF', 38 | rgb: { r: '075.0%', g: '075.0%', b: '000.0%' }, 39 | hsv: { h: '60.0', s: '1.000', v: '0.750' }, 40 | hsl: { h: '60.0', s: '1.000', l: '0.375' }, 41 | cmyk: { c: '0', m: '0', y: '100', k: '25' }, 42 | }, 43 | { 44 | hex: '#008000', 45 | hex8: '#008000FF', 46 | rgb: { r: '000.0%', g: '050.0%', b: '000.0%' }, 47 | hsv: { h: '120.0', s: '1.000', v: '0.500' }, 48 | hsl: { h: '120.0', s: '1.000', l: '0.250' }, 49 | cmyk: { c: '100', m: '0', y: '100', k: '50' }, 50 | }, 51 | { 52 | hex: '#80FFFF', 53 | hex8: '#80FFFFFF', 54 | rgb: { r: '050.0%', g: '100.0%', b: '100.0%' }, 55 | hsv: { h: '180.0', s: '0.500', v: '1.000' }, 56 | hsl: { h: '180.0', s: '1.000', l: '0.750' }, 57 | cmyk: { c: '50', m: '0', y: '0', k: '0' }, 58 | }, 59 | { 60 | hex: '#8080FF', 61 | hex8: '#8080FFFF', 62 | rgb: { r: '050.0%', g: '050.0%', b: '100.0%' }, 63 | hsv: { h: '240.0', s: '0.500', v: '1.000' }, 64 | hsl: { h: '240.0', s: '1.000', l: '0.750' }, 65 | cmyk: { c: '50', m: '50', y: '0', k: '0' }, 66 | }, 67 | { 68 | hex: '#BF40BF', 69 | hex8: '#BF40BFFF', 70 | rgb: { r: '075.0%', g: '025.0%', b: '075.0%' }, 71 | hsv: { h: '300.0', s: '0.667', v: '0.750' }, 72 | hsl: { h: '300.0', s: '0.500', l: '0.500' }, 73 | cmyk: { c: '0', m: '66', y: '0', k: '25' }, 74 | }, 75 | { 76 | hex: '#A0A424', 77 | hex8: '#A0A424FF', 78 | rgb: { r: '062.8%', g: '064.3%', b: '014.2%' }, 79 | hsv: { h: '61.8', s: '0.779', v: '0.643' }, 80 | hsl: { h: '61.8', s: '0.638', l: '0.393' }, 81 | cmyk: { c: '2', m: '0', y: '78', k: '36' }, 82 | }, 83 | { 84 | hex: '#1EAC41', 85 | hex8: '#1EAC41FF', 86 | rgb: { r: '011.6%', g: '067.5%', b: '025.5%' }, 87 | hsv: { h: '134.9', s: '0.828', v: '0.675' }, 88 | hsl: { h: '134.9', s: '0.707', l: '0.396' }, 89 | cmyk: { c: '83', m: '0', y: '62', k: '33' }, 90 | }, 91 | { 92 | hex: '#B430E5', 93 | hex8: '#B430E5FF', 94 | rgb: { r: '070.4%', g: '018.7%', b: '089.7%' }, 95 | hsv: { h: '283.7', s: '0.792', v: '0.897' }, 96 | hsl: { h: '283.7', s: '0.775', l: '0.542' }, 97 | cmyk: { c: '21', m: '79', y: '0', k: '10' }, 98 | }, 99 | { 100 | hex: '#FEF888', 101 | hex8: '#FEF888FF', 102 | rgb: { r: '099.8%', g: '097.4%', b: '053.2%' }, 103 | hsv: { h: '56.9', s: '0.467', v: '0.998' }, 104 | hsl: { h: '56.9', s: '0.991', l: '0.765' }, 105 | cmyk: { c: '0', m: '2', y: '46', k: '0' }, 106 | }, 107 | { 108 | hex: '#19CB97', 109 | hex8: '#19CB97FF', 110 | rgb: { r: '009.9%', g: '079.5%', b: '059.1%' }, 111 | hsv: { h: '162.4', s: '0.875', v: '0.795' }, 112 | hsl: { h: '162.4', s: '0.779', l: '0.447' }, 113 | cmyk: { c: '88', m: '0', y: '26', k: '20' }, 114 | }, 115 | { 116 | hex: '#362698', 117 | hex8: '#362698FF', 118 | rgb: { r: '021.1%', g: '014.9%', b: '059.7%' }, 119 | hsv: { h: '248.3', s: '0.750', v: '0.597' }, 120 | hsl: { h: '248.3', s: '0.601', l: '0.373' }, 121 | cmyk: { c: '64', m: '75', y: '0', k: '40' }, 122 | }, 123 | { 124 | hex: '#7E7EB8', 125 | hex8: '#7E7EB8FF', 126 | rgb: { r: '049.5%', g: '049.3%', b: '072.1%' }, 127 | hsv: { h: '240.5', s: '0.316', v: '0.721' }, 128 | hsl: { h: '240.5', s: '0.290', l: '0.607' }, 129 | cmyk: { c: '32', m: '32', y: '0', k: '28' }, 130 | }, 131 | ]; 132 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | fromRatio, 5 | isReadable, 6 | legacyRandom, 7 | mostReadable, 8 | names, 9 | readability, 10 | TinyColor, 11 | toMsFilter, 12 | } from '../src/public_api.js'; 13 | 14 | import { 15 | BRIGHTENS, 16 | DARKENS, 17 | DESATURATIONS, 18 | LIGHTENS, 19 | SATURATIONS, 20 | SHADES, 21 | TINTS, 22 | } from './modifications.js'; 23 | 24 | describe('TinyColor', () => { 25 | it('should init', () => { 26 | const r = new TinyColor('red'); 27 | expect(r.toName()).toBe('red'); 28 | expect(r).toBeTruthy(); 29 | }); 30 | it('should clone', () => { 31 | const color1 = new TinyColor('red'); 32 | const color2 = new TinyColor('red').clone(); 33 | color2.setAlpha(0.5); 34 | expect(color2.isValid).toBeTruthy(); 35 | expect(color2.toString()).toBe('rgba(255, 0, 0, 0.5)'); 36 | expect(color1.toString()).toBe('red'); 37 | }); 38 | it('should parse options', () => { 39 | expect(new TinyColor('red', { format: 'hex' }).toString()).toEqual('#ff0000'); 40 | expect(fromRatio({ r: 1, g: 0, b: 0 }, { format: 'hex' }).toString()).toEqual('#ff0000'); 41 | }); 42 | it('should get original input', () => { 43 | const colorRgbUp = 'RGB(39, 39, 39)'; 44 | const colorRgbLow = 'rgb(39, 39, 39)'; 45 | const colorRgbMix = 'RgB(39, 39, 39)'; 46 | const tinycolorObj = new TinyColor(colorRgbMix); 47 | const inputObj = { r: 100, g: 100, b: 100 }; 48 | // original lowercase input is returned 49 | expect(new TinyColor(colorRgbLow).originalInput).toBe(colorRgbLow); 50 | // original uppercase input is returned 51 | expect(new TinyColor(colorRgbUp).originalInput).toBe(colorRgbUp); 52 | // original mixed input is returned 53 | expect(new TinyColor(colorRgbMix).originalInput).toBe(colorRgbMix); 54 | // when given a tinycolor instance, the color string is returned 55 | expect(new TinyColor(tinycolorObj).originalInput).toBe(colorRgbMix); 56 | // when given an object, the object is returned 57 | expect(new TinyColor(inputObj).originalInput).toBe(inputObj); 58 | // when given an empty string, an empty string is returned 59 | expect(new TinyColor('').originalInput).toBe(''); 60 | // when given an undefined value, an empty string is returned 61 | expect(new TinyColor().originalInput).toBe(''); 62 | }); 63 | it('should parse ratio', () => { 64 | // with ratio 65 | // white 66 | expect(fromRatio({ r: 1, g: 1, b: 1 }).toHexString()).toBe('#ffffff'); 67 | // alpha works when ratio is parsed 68 | expect(fromRatio({ r: 1, g: 0, b: 0, a: 0.5 }).toRgbString()).toBe('rgba(255, 0, 0, 0.5)'); 69 | // alpha = 1 works when ratio is parsed 70 | expect(fromRatio({ r: 1, g: 0, b: 0, a: 1 }).toRgbString()).toBe('rgb(255, 0, 0)'); 71 | // alpha > 1 works when ratio is parsed 72 | expect(fromRatio({ r: 1, g: 0, b: 0, a: 10 }).toRgbString()).toBe('rgb(255, 0, 0)'); 73 | // alpha < 1 works when ratio is parsed 74 | expect(fromRatio({ r: 1, g: 0, b: 0, a: -1 }).toRgbString()).toBe('rgb(255, 0, 0)'); 75 | 76 | // without ratio 77 | expect(new TinyColor({ r: 1, g: 1, b: 1 }).toHexString()).toBe('#010101'); 78 | expect(new TinyColor({ r: 0.1, g: 0.1, b: 0.1 }).toHexString()).toBe('#000000'); 79 | expect(new TinyColor('rgb .1 .1 .1').toHexString()).toBe('#000000'); 80 | }); 81 | it('should parse hex', () => { 82 | expect(new TinyColor('#000').toHexString(true)).toBe('#000'); 83 | expect(new TinyColor('#0000').toHexString(true)).toBe('#000'); 84 | expect(new TinyColor('#000').getAlpha()).toBe(1); 85 | // Not sure this is expected behavior 86 | expect(new TinyColor('#0000').getAlpha()).toBe(0); 87 | }); 88 | it('should parse rgb', () => { 89 | // spaced input 90 | expect(new TinyColor('rgb 255 0 0').toHexString()).toBe('#ff0000'); 91 | // parenthesized input 92 | expect(new TinyColor('rgb(255, 0, 0)').toHexString()).toBe('#ff0000'); 93 | // parenthesized spaced input 94 | expect(new TinyColor('rgb (255, 0, 0)').toHexString()).toBe('#ff0000'); 95 | // object input 96 | expect(new TinyColor({ r: 255, g: 0, b: 0 }).toHexString()).toBe('#ff0000'); 97 | // object input and compare 98 | expect(new TinyColor({ r: 255, g: 0, b: 0 }).toRgb()).toEqual({ r: 255, g: 0, b: 0, a: 1 }); 99 | 100 | expect(new TinyColor({ r: 200, g: 100, b: 0 }).equals('rgb(200, 100, 0)')).toBe(true); 101 | expect(new TinyColor({ r: 200, g: 100, b: 0 }).equals('rgb 200 100 0')).toBe(true); 102 | expect(new TinyColor({ r: 200, g: 100, b: 0 }).equals('rgb 200 100 0')).toBe(true); 103 | expect(new TinyColor({ r: 200, g: 100, b: 0, a: 0.4 }).equals('rgba 200 100 0 .4')).toBe(true); 104 | expect(new TinyColor({ r: 199, g: 100, b: 0 }).equals()).toBe(false); 105 | 106 | expect(new TinyColor().equals(new TinyColor({ r: 200, g: 100, b: 0 }))).toBe(false); 107 | expect(new TinyColor().equals(new TinyColor({ r: 200, g: 100, b: 0 }))).toBe(false); 108 | expect(new TinyColor().equals(new TinyColor({ r: 200, g: 100, b: 0 }))).toBe(false); 109 | }); 110 | it('should parse percentage rgb text', () => { 111 | // spaced input 112 | expect(new TinyColor('rgb 100% 0% 0%').toHexString()).toBe('#ff0000'); 113 | // parenthesized input 114 | expect(new TinyColor('rgb(100%, 0%, 0%)').toHexString()).toBe('#ff0000'); 115 | // parenthesized spaced input 116 | expect(new TinyColor('rgb (100%, 0%, 0%)').toHexString()).toBe('#ff0000'); 117 | // object input 118 | expect(new TinyColor({ r: '100%', g: '0%', b: '0%' }).toHexString()).toBe('#ff0000'); 119 | // object input and compare 120 | expect(new TinyColor({ r: '100%', g: '0%', b: '0%' }).toRgb()).toEqual({ 121 | r: 255, 122 | g: 0, 123 | b: 0, 124 | a: 1, 125 | }); 126 | 127 | expect(new TinyColor({ r: '90%', g: '45%', b: '0%' }).equals('rgb(90%, 45%, 0%)')).toBe(true); 128 | expect(new TinyColor({ r: '90%', g: '45%', b: '0%' }).equals('rgb 90% 45% 0%')).toBe(true); 129 | expect(new TinyColor({ r: '90%', g: '45%', b: '0%' }).equals('rgb 90% 45% 0%')).toBe(true); 130 | expect( 131 | new TinyColor({ r: '90%', g: '45%', b: '0%', a: 0.4 }).equals('rgba 90% 45% 0% .4'), 132 | ).toBe(true); 133 | expect(new TinyColor({ r: '89%', g: '45%', b: '0%' }).equals('rgba 90% 45% 0% 1')).toBe(false); 134 | 135 | expect(new TinyColor({ r: '89%', g: '45%', b: '0%' }).equals('rgb(90%, 45%, 0%)')).toBe(false); 136 | expect(new TinyColor({ r: '89%', g: '45%', b: '0%' }).equals('rgb 90% 45% 0%')).toBe(false); 137 | expect(new TinyColor({ r: '89%', g: '45%', b: '0%' }).equals('rgb 90% 45% 0%')).toBe(false); 138 | 139 | expect( 140 | new TinyColor({ r: '90%', g: '45%', b: '0%' }).equals(new TinyColor('rgb 90% 45% 0%')), 141 | ).toBe(true); 142 | expect( 143 | new TinyColor({ r: '90%', g: '45%', b: '0%' }).equals(new TinyColor('rgb 90% 45% 0%')), 144 | ).toBe(true); 145 | expect( 146 | new TinyColor({ r: '90%', g: '45%', b: '0%' }).equals(new TinyColor('rgb(90%, 45%, 0%)')), 147 | ).toBe(true); 148 | }); 149 | it('should parse HSL', () => { 150 | // to hex 151 | expect(new TinyColor({ h: 251, s: 100, l: 0.38 }).toHexString()).toBe('#2400c2'); 152 | // to rgb 153 | expect(new TinyColor({ h: 251, s: 100, l: 0.38 }).toRgbString()).toBe('rgb(36, 0, 194)'); 154 | // to hsl 155 | expect(new TinyColor({ h: 251, s: 100, l: 0.38 }).toHslString()).toBe('hsl(251, 100%, 38%)'); 156 | expect(new TinyColor({ h: 251, s: 100, l: 0.38, a: 0.38 }).toHslString()).toBe( 157 | 'hsla(251, 100%, 38%, 0.38)', 158 | ); 159 | // to hex 160 | expect(new TinyColor('hsl(251, 100, 38)').toHexString()).toBe('#2400c2'); 161 | // to rgb 162 | expect(new TinyColor('hsl(251, 100%, 38%)').toRgbString()).toBe('rgb(36, 0, 194)'); 163 | // to hsl 164 | expect(new TinyColor('hsl(251, 100%, 38%)').toHslString()).toBe('hsl(251, 100%, 38%)'); 165 | // problematic hsl 166 | expect(new TinyColor('hsl 100 20 10').toHslString()).toBe('hsl(100, 20%, 10%)'); 167 | expect(new TinyColor('hsla 100 20 10 0.38').toHslString()).toBe('hsla(100, 20%, 10%, 0.38)'); 168 | // wrap out of bounds hue 169 | expect(new TinyColor('hsl -700 20 10').toHslString()).toBe('hsl(20, 20%, 10%)'); 170 | expect(new TinyColor('hsl -490 100% 50%').toHslString()).toBe('hsl(230, 100%, 50%)'); 171 | }); 172 | it('should parse rgb strings', () => { 173 | expect(new TinyColor('rgb 255 0 0').toHexString()).toBe('#ff0000'); 174 | expect(new TinyColor('rgb 255 0 0').toHexString(true)).toBe('#f00'); 175 | expect(new TinyColor('rgba 255 0 0 0.5').toHex8String()).toBe('#ff000080'); 176 | expect(new TinyColor('rgba 255 0 0 0').toHex8String()).toBe('#ff000000'); 177 | expect(new TinyColor('rgba 255 0 0 1').toHex8String()).toBe('#ff0000ff'); 178 | expect(new TinyColor('rgba 255 0 0 1').toHex8String(true)).toBe('#f00f'); 179 | expect(new TinyColor('rgba 255 0 0 0').toHexShortString()).toBe('#ff000000'); 180 | expect(new TinyColor('rgba 255 0 0 0').toHexShortString(true)).toBe('#f000'); 181 | expect(new TinyColor('rgba 255 0 0 1').toHexShortString()).toBe('#ff0000'); 182 | expect(new TinyColor('rgba 255 0 0 1').toHexShortString(true)).toBe('#f00'); 183 | expect(new TinyColor('rgb 255 0 0').toHex()).toBe('ff0000'); 184 | expect(new TinyColor('rgb 255 0 0').toHex(true)).toBe('f00'); 185 | expect(new TinyColor('rgba 255 0 0 0.5').toHex8()).toBe('ff000080'); 186 | }); 187 | it('should parse hsv string', () => { 188 | expect(new TinyColor('hsv 251.1 0.887 .918').format).toBe('hsv'); 189 | expect(new TinyColor('hsv 251.1 0.887 .918').toHsvString()).toBe('hsv(251, 89%, 92%)'); 190 | expect(new TinyColor('hsv 251.1 0.887 0.918').toHsvString()).toBe('hsv(251, 89%, 92%)'); 191 | expect(new TinyColor('hsva 251.1 0.887 0.918 0.5').toHsvString()).toBe( 192 | 'hsva(251, 89%, 92%, 0.5)', 193 | ); 194 | }); 195 | it('should parse CMYK string', () => { 196 | expect(new TinyColor('cmyk(25, 0, 93, 46)').format).toBe('cmyk'); 197 | expect(new TinyColor('cmyk(25, 0, 93, 46)').toRgbString()).toBe('rgb(103, 138, 10)'); 198 | expect(new TinyColor('cmyk(25, 0, 93, 46)').toHsvString()).toBe('hsv(76, 93%, 54%)'); 199 | }); 200 | it('should parse invalid input', () => { 201 | let invalidColor = new TinyColor('not a color'); 202 | expect(invalidColor.toHexString()).toBe('#000000'); 203 | expect(invalidColor.isValid).toBe(false); 204 | 205 | invalidColor = new TinyColor('#red'); 206 | expect(invalidColor.toHexString()).toBe('#000000'); 207 | expect(invalidColor.isValid).toBe(false); 208 | 209 | invalidColor = new TinyColor(' #red'); 210 | expect(invalidColor.toHexString()).toBe('#000000'); 211 | expect(invalidColor.isValid).toBe(false); 212 | 213 | invalidColor = new TinyColor('##123456'); 214 | expect(invalidColor.toHexString()).toBe('#000000'); 215 | expect(invalidColor.isValid).toBe(false); 216 | 217 | invalidColor = new TinyColor(' ##123456'); 218 | expect(invalidColor.toHexString()).toBe('#000000'); 219 | expect(invalidColor.isValid).toBe(false); 220 | 221 | invalidColor = new TinyColor({ r: 'invalid', g: 'invalid', b: 'invalid' }); 222 | expect(invalidColor.toHexString()).toBe('#000000'); 223 | expect(invalidColor.isValid).toBe(false); 224 | 225 | invalidColor = new TinyColor({ h: 'invalid', s: 'invalid', l: 'invalid' } as any); 226 | expect(invalidColor.toHexString()).toBe('#000000'); 227 | expect(invalidColor.isValid).toBe(false); 228 | 229 | invalidColor = new TinyColor({ h: 'invalid', s: 'invalid', v: 'invalid' } as any); 230 | expect(invalidColor.toHexString()).toBe('#000000'); 231 | expect(invalidColor.isValid).toBe(false); 232 | 233 | invalidColor = new TinyColor(); 234 | expect(invalidColor.toHexString()).toBe('#000000'); 235 | expect(invalidColor.isValid).toBe(false); 236 | }); 237 | it('should parse named colors', () => { 238 | expect(new TinyColor('aliceblue').toHex()).toBe('f0f8ff'); 239 | expect(new TinyColor('antiquewhite').toHex()).toBe('faebd7'); 240 | expect(new TinyColor('aqua').toHex()).toBe('00ffff'); 241 | expect(new TinyColor('aquamarine').toHex()).toBe('7fffd4'); 242 | expect(new TinyColor('azure').toHex()).toBe('f0ffff'); 243 | expect(new TinyColor('beige').toHex()).toBe('f5f5dc'); 244 | expect(new TinyColor('bisque').toHex()).toBe('ffe4c4'); 245 | expect(new TinyColor('black').toHex()).toBe('000000'); 246 | expect(new TinyColor('blanchedalmond').toHex()).toBe('ffebcd'); 247 | expect(new TinyColor('blue').toHex()).toBe('0000ff'); 248 | expect(new TinyColor('blueviolet').toHex()).toBe('8a2be2'); 249 | expect(new TinyColor('brown').toHex()).toBe('a52a2a'); 250 | expect(new TinyColor('burlywood').toHex()).toBe('deb887'); 251 | expect(new TinyColor('cadetblue').toHex()).toBe('5f9ea0'); 252 | expect(new TinyColor('chartreuse').toHex()).toBe('7fff00'); 253 | expect(new TinyColor('chocolate').toHex()).toBe('d2691e'); 254 | expect(new TinyColor('coral').toHex()).toBe('ff7f50'); 255 | expect(new TinyColor('cornflowerblue').toHex()).toBe('6495ed'); 256 | expect(new TinyColor('cornsilk').toHex()).toBe('fff8dc'); 257 | expect(new TinyColor('crimson').toHex()).toBe('dc143c'); 258 | expect(new TinyColor('cyan').toHex()).toBe('00ffff'); 259 | expect(new TinyColor('darkblue').toHex()).toBe('00008b'); 260 | expect(new TinyColor('darkcyan').toHex()).toBe('008b8b'); 261 | expect(new TinyColor('darkgoldenrod').toHex()).toBe('b8860b'); 262 | expect(new TinyColor('darkgray').toHex()).toBe('a9a9a9'); 263 | expect(new TinyColor('darkgreen').toHex()).toBe('006400'); 264 | expect(new TinyColor('darkkhaki').toHex()).toBe('bdb76b'); 265 | expect(new TinyColor('darkmagenta').toHex()).toBe('8b008b'); 266 | expect(new TinyColor('darkolivegreen').toHex()).toBe('556b2f'); 267 | expect(new TinyColor('darkorange').toHex()).toBe('ff8c00'); 268 | expect(new TinyColor('darkorchid').toHex()).toBe('9932cc'); 269 | expect(new TinyColor('darkred').toHex()).toBe('8b0000'); 270 | expect(new TinyColor('darksalmon').toHex()).toBe('e9967a'); 271 | expect(new TinyColor('darkseagreen').toHex()).toBe('8fbc8f'); 272 | expect(new TinyColor('darkslateblue').toHex()).toBe('483d8b'); 273 | expect(new TinyColor('darkslategray').toHex()).toBe('2f4f4f'); 274 | expect(new TinyColor('darkturquoise').toHex()).toBe('00ced1'); 275 | expect(new TinyColor('darkviolet').toHex()).toBe('9400d3'); 276 | expect(new TinyColor('deeppink').toHex()).toBe('ff1493'); 277 | expect(new TinyColor('deepskyblue').toHex()).toBe('00bfff'); 278 | expect(new TinyColor('dimgray').toHex()).toBe('696969'); 279 | expect(new TinyColor('dodgerblue').toHex()).toBe('1e90ff'); 280 | expect(new TinyColor('firebrick').toHex()).toBe('b22222'); 281 | expect(new TinyColor('floralwhite').toHex()).toBe('fffaf0'); 282 | expect(new TinyColor('forestgreen').toHex()).toBe('228b22'); 283 | expect(new TinyColor('fuchsia').toHex()).toBe('ff00ff'); 284 | expect(new TinyColor('gainsboro').toHex()).toBe('dcdcdc'); 285 | expect(new TinyColor('ghostwhite').toHex()).toBe('f8f8ff'); 286 | expect(new TinyColor('gold').toHex()).toBe('ffd700'); 287 | expect(new TinyColor('goldenrod').toHex()).toBe('daa520'); 288 | expect(new TinyColor('gray').toHex()).toBe('808080'); 289 | expect(new TinyColor('grey').toHex()).toBe('808080'); 290 | expect(new TinyColor('green').toHex()).toBe('008000'); 291 | expect(new TinyColor('greenyellow').toHex()).toBe('adff2f'); 292 | expect(new TinyColor('honeydew').toHex()).toBe('f0fff0'); 293 | expect(new TinyColor('hotpink').toHex()).toBe('ff69b4'); 294 | expect(new TinyColor('indianred ').toHex()).toBe('cd5c5c'); 295 | expect(new TinyColor('indigo ').toHex()).toBe('4b0082'); 296 | expect(new TinyColor('ivory').toHex()).toBe('fffff0'); 297 | expect(new TinyColor('khaki').toHex()).toBe('f0e68c'); 298 | expect(new TinyColor('lavender').toHex()).toBe('e6e6fa'); 299 | expect(new TinyColor('lavenderblush').toHex()).toBe('fff0f5'); 300 | expect(new TinyColor('lawngreen').toHex()).toBe('7cfc00'); 301 | expect(new TinyColor('lemonchiffon').toHex()).toBe('fffacd'); 302 | expect(new TinyColor('lightblue').toHex()).toBe('add8e6'); 303 | expect(new TinyColor('lightcoral').toHex()).toBe('f08080'); 304 | expect(new TinyColor('lightcyan').toHex()).toBe('e0ffff'); 305 | expect(new TinyColor('lightgoldenrodyellow').toHex()).toBe('fafad2'); 306 | expect(new TinyColor('lightgrey').toHex()).toBe('d3d3d3'); 307 | expect(new TinyColor('lightgreen').toHex()).toBe('90ee90'); 308 | expect(new TinyColor('lightpink').toHex()).toBe('ffb6c1'); 309 | expect(new TinyColor('lightsalmon').toHex()).toBe('ffa07a'); 310 | expect(new TinyColor('lightseagreen').toHex()).toBe('20b2aa'); 311 | expect(new TinyColor('lightskyblue').toHex()).toBe('87cefa'); 312 | expect(new TinyColor('lightslategray').toHex()).toBe('778899'); 313 | expect(new TinyColor('lightsteelblue').toHex()).toBe('b0c4de'); 314 | expect(new TinyColor('lightyellow').toHex()).toBe('ffffe0'); 315 | expect(new TinyColor('lime').toHex()).toBe('00ff00'); 316 | expect(new TinyColor('limegreen').toHex()).toBe('32cd32'); 317 | expect(new TinyColor('linen').toHex()).toBe('faf0e6'); 318 | expect(new TinyColor('magenta').toHex()).toBe('ff00ff'); 319 | expect(new TinyColor('maroon').toHex()).toBe('800000'); 320 | expect(new TinyColor('mediumaquamarine').toHex()).toBe('66cdaa'); 321 | expect(new TinyColor('mediumblue').toHex()).toBe('0000cd'); 322 | expect(new TinyColor('mediumorchid').toHex()).toBe('ba55d3'); 323 | expect(new TinyColor('mediumpurple').toHex()).toBe('9370db'); 324 | expect(new TinyColor('mediumseagreen').toHex()).toBe('3cb371'); 325 | expect(new TinyColor('mediumslateblue').toHex()).toBe('7b68ee'); 326 | expect(new TinyColor('mediumspringgreen').toHex()).toBe('00fa9a'); 327 | expect(new TinyColor('mediumturquoise').toHex()).toBe('48d1cc'); 328 | expect(new TinyColor('mediumvioletred').toHex()).toBe('c71585'); 329 | expect(new TinyColor('midnightblue').toHex()).toBe('191970'); 330 | expect(new TinyColor('mintcream').toHex()).toBe('f5fffa'); 331 | expect(new TinyColor('mistyrose').toHex()).toBe('ffe4e1'); 332 | expect(new TinyColor('moccasin').toHex()).toBe('ffe4b5'); 333 | expect(new TinyColor('navajowhite').toHex()).toBe('ffdead'); 334 | expect(new TinyColor('navy').toHex()).toBe('000080'); 335 | expect(new TinyColor('oldlace').toHex()).toBe('fdf5e6'); 336 | expect(new TinyColor('olive').toHex()).toBe('808000'); 337 | expect(new TinyColor('olivedrab').toHex()).toBe('6b8e23'); 338 | expect(new TinyColor('orange').toHex()).toBe('ffa500'); 339 | expect(new TinyColor('orangered').toHex()).toBe('ff4500'); 340 | expect(new TinyColor('orchid').toHex()).toBe('da70d6'); 341 | expect(new TinyColor('palegoldenrod').toHex()).toBe('eee8aa'); 342 | expect(new TinyColor('palegreen').toHex()).toBe('98fb98'); 343 | expect(new TinyColor('paleturquoise').toHex()).toBe('afeeee'); 344 | expect(new TinyColor('palevioletred').toHex()).toBe('db7093'); 345 | expect(new TinyColor('papayawhip').toHex()).toBe('ffefd5'); 346 | expect(new TinyColor('peachpuff').toHex()).toBe('ffdab9'); 347 | expect(new TinyColor('peru').toHex()).toBe('cd853f'); 348 | expect(new TinyColor('pink').toHex()).toBe('ffc0cb'); 349 | expect(new TinyColor('plum').toHex()).toBe('dda0dd'); 350 | expect(new TinyColor('powderblue').toHex()).toBe('b0e0e6'); 351 | expect(new TinyColor('purple').toHex()).toBe('800080'); 352 | expect(new TinyColor('rebeccapurple').toHex()).toBe('663399'); 353 | expect(new TinyColor('red').toHex()).toBe('ff0000'); 354 | expect(new TinyColor('rosybrown').toHex()).toBe('bc8f8f'); 355 | expect(new TinyColor('royalblue').toHex()).toBe('4169e1'); 356 | expect(new TinyColor('saddlebrown').toHex()).toBe('8b4513'); 357 | expect(new TinyColor('salmon').toHex()).toBe('fa8072'); 358 | expect(new TinyColor('sandybrown').toHex()).toBe('f4a460'); 359 | expect(new TinyColor('seagreen').toHex()).toBe('2e8b57'); 360 | expect(new TinyColor('seashell').toHex()).toBe('fff5ee'); 361 | expect(new TinyColor('sienna').toHex()).toBe('a0522d'); 362 | expect(new TinyColor('silver').toHex()).toBe('c0c0c0'); 363 | expect(new TinyColor('skyblue').toHex()).toBe('87ceeb'); 364 | expect(new TinyColor('slateblue').toHex()).toBe('6a5acd'); 365 | expect(new TinyColor('slategray').toHex()).toBe('708090'); 366 | expect(new TinyColor('snow').toHex()).toBe('fffafa'); 367 | expect(new TinyColor('springgreen').toHex()).toBe('00ff7f'); 368 | expect(new TinyColor('steelblue').toHex()).toBe('4682b4'); 369 | expect(new TinyColor('tan').toHex()).toBe('d2b48c'); 370 | expect(new TinyColor('teal').toHex()).toBe('008080'); 371 | expect(new TinyColor('thistle').toHex()).toBe('d8bfd8'); 372 | expect(new TinyColor('tomato').toHex()).toBe('ff6347'); 373 | expect(new TinyColor('turquoise').toHex()).toBe('40e0d0'); 374 | expect(new TinyColor('violet').toHex()).toBe('ee82ee'); 375 | expect(new TinyColor('wheat').toHex()).toBe('f5deb3'); 376 | expect(new TinyColor('white').toHex()).toBe('ffffff'); 377 | expect(new TinyColor('whitesmoke').toHex()).toBe('f5f5f5'); 378 | expect(new TinyColor('yellow').toHex()).toBe('ffff00'); 379 | expect(new TinyColor('yellowgreen').toHex()).toBe('9acd32'); 380 | 381 | expect(new TinyColor('#f00').toName()).toBe('red'); 382 | expect(new TinyColor('#fa0a0a').toName()).toBe(false); 383 | }); 384 | it('Invalid alpha should normalize to 1', () => { 385 | // Negative value 386 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: -1 }).toRgbString()).toBe('rgb(255, 20, 10)'); 387 | // Negative 0 388 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: -0 }).toRgbString()).toBe( 389 | 'rgba(255, 20, 10, 0)', 390 | ); 391 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: 0 }).toRgbString()).toBe( 392 | 'rgba(255, 20, 10, 0)', 393 | ); 394 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: 0.5 }).toRgbString()).toBe( 395 | 'rgba(255, 20, 10, 0.5)', 396 | ); 397 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: 1 }).toRgbString()).toBe('rgb(255, 20, 10)'); 398 | // Greater than 1 399 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: 100 }).toRgbString()).toBe('rgb(255, 20, 10)'); 400 | // Non Numeric 401 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: 'asdfasd' } as any).toRgbString()).toBe( 402 | 'rgb(255, 20, 10)', 403 | ); 404 | 405 | expect(new TinyColor('#fff').toRgbString()).toBe('rgb(255, 255, 255)'); 406 | // Greater than 1 in string parsing 407 | expect(new TinyColor('rgba 255 0 0 100').toRgbString()).toBe('rgb(255, 0, 0)'); 408 | }); 409 | it('should translate toString with alpha set', () => { 410 | const redNamed = fromRatio({ r: 255, g: 0, b: 0, a: 0.6 }, { format: 'name' }); 411 | const redHex = fromRatio({ r: 255, g: 0, b: 0, a: 0.4 }, { format: 'hex' }); 412 | 413 | expect(redNamed.format).toBe('name'); 414 | expect(redHex.format).toBe('hex'); 415 | 416 | // Names should default to rgba if alpha is < 1 417 | expect(redNamed.toString()).toBe('rgba(255, 0, 0, 0.6)'); 418 | // Hex should default to rgba if alpha is < 1 419 | expect(redHex.toString()).toBe('rgba(255, 0, 0, 0.4)'); 420 | 421 | // Names should not be returned as rgba if format is specified 422 | expect(redNamed.toString('hex')).toBe('#ff0000'); 423 | // Names should not be returned as rgba if format is specified 424 | expect(redNamed.toString('hex6')).toBe('#ff0000'); 425 | // Names should not be returned as rgba if format is specified 426 | expect(redNamed.toString('hex3')).toBe('#f00'); 427 | // Names should not be returned as rgba if format is specified 428 | expect(redNamed.toString('hex8')).toBe('#ff000099'); 429 | // Names should not be returned as rgba if format is specified 430 | expect(redNamed.toString('hex4')).toBe('#f009'); 431 | // Semi transparent names should return hex in toString() if name format is specified 432 | expect(redNamed.toString('name')).toBe('#ff0000'); 433 | // Semi transparent names should be false in toName() 434 | expect(redNamed.toName()).toBe(false); 435 | // Hex should default to rgba if alpha is < 1 436 | expect(redHex.toString()).toBe('rgba(255, 0, 0, 0.4)'); 437 | // Named color should equal transparent if alpha == 0 438 | const transparentNamed = fromRatio({ r: 255, g: 0, b: 0, a: 0 }, { format: 'name' }); 439 | expect(transparentNamed.toString()).toBe('transparent'); 440 | 441 | redHex.setAlpha(0); 442 | // Hex should default to rgba if alpha is = 0 443 | expect(redHex.toString()).toBe('rgba(255, 0, 0, 0)'); 444 | redHex.setAlpha(0.38); 445 | expect(redHex.toString()).toBe('rgba(255, 0, 0, 0.38)'); 446 | }); 447 | it('should get alpha', () => { 448 | const hexSetter = new TinyColor('rgba(255, 0, 0, 1)'); 449 | // Alpha should start as 1 450 | expect(hexSetter.getAlpha()).toBe(1); 451 | const returnedFromSetAlpha = hexSetter.setAlpha(0.9); 452 | // setAlpha should change alpha value 453 | expect(returnedFromSetAlpha.getAlpha()).toBe(0.9); 454 | hexSetter.setAlpha(0.5); 455 | // setAlpha should change alpha value 456 | expect(hexSetter.getAlpha()).toBe(0.5); 457 | }); 458 | it('should set alpha', () => { 459 | const hexSetter = new TinyColor('rgba(255, 0, 0, 1)'); 460 | // Alpha should start as 1 461 | expect(hexSetter.a).toBe(1); 462 | const returnedFromSetAlpha = hexSetter.setAlpha(0.9); 463 | // setAlpha return value should be the color 464 | expect(returnedFromSetAlpha).toBe(hexSetter); 465 | // setAlpha should change alpha value 466 | expect(hexSetter.a).toBe(0.9); 467 | hexSetter.setAlpha(0.5); 468 | // setAlpha should change alpha value 469 | expect(hexSetter.a).toBe(0.5); 470 | hexSetter.setAlpha(0); 471 | // setAlpha should change alpha value 472 | expect(hexSetter.a).toBe(0); 473 | hexSetter.setAlpha(-1); 474 | // setAlpha with value < 0 should be bound to 1 475 | expect(hexSetter.a).toBe(1); 476 | hexSetter.setAlpha(2); 477 | // setAlpha with value > 1 should be bound to 1 478 | expect(hexSetter.a).toBe(1); 479 | hexSetter.setAlpha(); 480 | // setAlpha with invalid value should be bound to 1 481 | expect(hexSetter.a).toBe(1); 482 | hexSetter.setAlpha(null as any); 483 | // setAlpha with invalid value should be bound to 1 484 | expect(hexSetter.a).toBe(1); 485 | hexSetter.setAlpha('test'); 486 | // setAlpha with invalid value should be bound to 1 487 | expect(hexSetter.a).toBe(1); 488 | // Check abiliity to chain 489 | hexSetter.setAlpha(1).toHex(); 490 | }); 491 | it('Alpha = 0 should act differently on toName()', () => { 492 | expect(new TinyColor({ r: 255, g: 20, b: 10, a: 0 }).toName()).toBe('transparent'); 493 | expect(new TinyColor('transparent').toString()).toBe('transparent'); 494 | expect(new TinyColor('transparent').toHex()).toBe('000000'); 495 | }); 496 | it('should getBrightness', () => { 497 | expect(new TinyColor('#000').getBrightness()).toBe(0); 498 | expect(new TinyColor('#fff').getBrightness()).toBe(255); 499 | }); 500 | 501 | it('should getLuminance', () => { 502 | expect(new TinyColor('#000').getLuminance()).toBe(0); 503 | expect(new TinyColor('#fff').getLuminance()).toBe(1); 504 | }); 505 | 506 | it('isDark returns true/false for dark/light colors', () => { 507 | expect(new TinyColor('#000').isDark()).toBe(true); 508 | expect(new TinyColor('#111').isDark()).toBe(true); 509 | expect(new TinyColor('#222').isDark()).toBe(true); 510 | expect(new TinyColor('#333').isDark()).toBe(true); 511 | expect(new TinyColor('#444').isDark()).toBe(true); 512 | expect(new TinyColor('#555').isDark()).toBe(true); 513 | expect(new TinyColor('#666').isDark()).toBe(true); 514 | expect(new TinyColor('#777').isDark()).toBe(true); 515 | expect(new TinyColor('#888').isDark()).toBe(false); 516 | expect(new TinyColor('#999').isDark()).toBe(false); 517 | expect(new TinyColor('#aaa').isDark()).toBe(false); 518 | expect(new TinyColor('#bbb').isDark()).toBe(false); 519 | expect(new TinyColor('#ccc').isDark()).toBe(false); 520 | expect(new TinyColor('#ddd').isDark()).toBe(false); 521 | expect(new TinyColor('#eee').isDark()).toBe(false); 522 | expect(new TinyColor('#fff').isDark()).toBe(false); 523 | }); 524 | it('isLight returns true/false for light/dark colors', () => { 525 | expect(new TinyColor('#000').isLight()).toBe(false); 526 | expect(new TinyColor('#111').isLight()).toBe(false); 527 | expect(new TinyColor('#222').isLight()).toBe(false); 528 | expect(new TinyColor('#333').isLight()).toBe(false); 529 | expect(new TinyColor('#444').isLight()).toBe(false); 530 | expect(new TinyColor('#555').isLight()).toBe(false); 531 | expect(new TinyColor('#666').isLight()).toBe(false); 532 | expect(new TinyColor('#777').isLight()).toBe(false); 533 | expect(new TinyColor('#888').isLight()).toBe(true); 534 | expect(new TinyColor('#999').isLight()).toBe(true); 535 | expect(new TinyColor('#aaa').isLight()).toBe(true); 536 | expect(new TinyColor('#bbb').isLight()).toBe(true); 537 | expect(new TinyColor('#ccc').isLight()).toBe(true); 538 | expect(new TinyColor('#ddd').isLight()).toBe(true); 539 | expect(new TinyColor('#eee').isLight()).toBe(true); 540 | expect(new TinyColor('#fff').isLight()).toBe(true); 541 | }); 542 | it('Color equality', () => { 543 | expect(new TinyColor('#ff0000').equals('#ff0000')).toBe(true); 544 | expect(new TinyColor('#ff0000').equals('rgb(255, 0, 0)')).toBe(true); 545 | expect(new TinyColor('#ff0000').equals('rgba(255, 0, 0, .1)')).toBe(false); 546 | expect(new TinyColor('#ff000066').equals('rgba(255, 0, 0, .4)')).toBe(true); 547 | expect(new TinyColor('#f009').equals('rgba(255, 0, 0, .6)')).toBe(true); 548 | expect(new TinyColor('#336699CC').equals('369C')).toBe(true); 549 | expect(new TinyColor('ff0000').equals('#ff0000')).toBe(true); 550 | expect(new TinyColor('#f00').equals('#ff0000')).toBe(true); 551 | expect(new TinyColor('#f00').equals('#ff0000')).toBe(true); 552 | expect(new TinyColor('f00').equals('#ff0000')).toBe(true); 553 | expect(new TinyColor('010101').toHexString()).toBe('#010101'); 554 | expect(new TinyColor('#ff0000').equals('#00ff00')).toBe(false); 555 | expect(new TinyColor('#ff8000').equals('rgb(100%, 50%, 0%)')).toBe(true); 556 | }); 557 | it('isReadable', () => { 558 | // "#ff0088", "#8822aa" (values used in old WCAG1 tests) 559 | expect(isReadable('#000000', '#ffffff', { level: 'AA', size: 'small' })).toBe(true); 560 | expect(isReadable('#ff0088', '#5c1a72', {})).toBe(false); 561 | expect(isReadable('#ff0088', '#8822aa', { level: 'AA', size: 'small' })).toBe(false); 562 | expect(isReadable('#ff0088', '#8822aa', { level: 'AA', size: 'large' })).toBe(false); 563 | expect(isReadable('#ff0088', '#8822aa', { level: 'AAA', size: 'small' })).toBe(false); 564 | expect(isReadable('#ff0088', '#8822aa', { level: 'AAA', size: 'large' })).toBe(false); 565 | 566 | // values derived from and validated using the calculators at http://www.dasplankton.de/ContrastA/ 567 | // and http://webaim.org/resources/contrastchecker/ 568 | 569 | // "#ff0088", "#5c1a72": contrast ratio 3.04 570 | expect(isReadable('#ff0088', '#5c1a72', { level: 'AA', size: 'small' })).toBe(false); 571 | expect(isReadable('#ff0088', '#5c1a72', { level: 'AA', size: 'large' })).toBe(true); 572 | expect(isReadable('#ff0088', '#5c1a72', { level: 'AAA', size: 'small' })).toBe(false); 573 | expect(isReadable('#ff0088', '#5c1a72', { level: 'AAA', size: 'large' })).toBe(false); 574 | 575 | // "#ff0088", "#2e0c3a": contrast ratio 4.56 576 | expect(isReadable('#ff0088', '#2e0c3a', { level: 'AA', size: 'small' })).toBe(true); 577 | expect(isReadable('#ff0088', '#2e0c3a', { level: 'AA', size: 'large' })).toBe(true); 578 | expect(isReadable('#ff0088', '#2e0c3a', { level: 'AAA', size: 'small' })).toBe(false); 579 | expect(isReadable('#ff0088', '#2e0c3a', { level: 'AAA', size: 'large' })).toBe(true); 580 | 581 | // "#db91b8", "#2e0c3a": contrast ratio 7.12 582 | expect(isReadable('#db91b8', '#2e0c3a', { level: 'AA', size: 'small' })).toBe(true); 583 | expect(isReadable('#db91b8', '#2e0c3a', { level: 'AA', size: 'large' })).toBe(true); 584 | expect(isReadable('#db91b8', '#2e0c3a', { level: 'AAA', size: 'small' })).toBe(true); 585 | expect(isReadable('#db91b8', '#2e0c3a', { level: 'AAA', size: 'large' })).toBe(true); 586 | expect(isReadable('#db91b8', '#2e0c3a', { level: 'ZZZ', size: 'large' } as any)).toBe(false); 587 | }); 588 | it('readability', () => { 589 | // check return values from readability function. See isReadable above for standards tests. 590 | expect(readability('#000', '#000')).toBe(1); 591 | expect(readability('#000', '#111')).toBe(1.1121078324840545); 592 | expect(readability('#000', '#fff')).toBe(21); 593 | }); 594 | 595 | it('mostReadable', () => { 596 | expect(mostReadable('#000', ['#111', '#222'])!.toHexString()).toBe('#222222'); 597 | expect(mostReadable('#f00', ['#d00', '#0d0'])!.toHexString()).toBe('#00dd00'); 598 | expect( 599 | mostReadable(new TinyColor('#f00'), [ 600 | new TinyColor('#d00'), 601 | new TinyColor('#0d0'), 602 | ])!.toHexString(), 603 | ).toBe('#00dd00'); 604 | expect(mostReadable('#fff', ['#fff', '#fff'])!.toHexString()).toBe('#ffffff'); 605 | // includeFallbackColors 606 | expect( 607 | mostReadable('#fff', ['#fff', '#fff'], { includeFallbackColors: true })!.toHexString(), 608 | ).toBe('#000000'); 609 | // no readable color in list 610 | expect( 611 | mostReadable('#123', ['#124', '#125'], { includeFallbackColors: false })!.toHexString(), 612 | ).toBe('#112255'); 613 | expect( 614 | mostReadable('#123', ['#000', '#fff'], { includeFallbackColors: false })!.toHexString(), 615 | ).toBe('#ffffff'); 616 | // no readable color in list 617 | expect( 618 | mostReadable('#123', ['#124', '#125'], { includeFallbackColors: true })!.toHexString(), 619 | ).toBe('#ffffff'); 620 | 621 | expect( 622 | mostReadable('#ff0088', ['#000', '#fff'], { includeFallbackColors: false })!.toHexString(), 623 | ).toBe('#000000'); 624 | expect( 625 | mostReadable('#ff0088', ['#2e0c3a'], { 626 | includeFallbackColors: true, 627 | level: 'AAA', 628 | size: 'large', 629 | })!.toHexString(), 630 | ).toBe('#2e0c3a'); 631 | expect( 632 | mostReadable('#ff0088', ['#2e0c3a'], { 633 | includeFallbackColors: true, 634 | level: 'AAA', 635 | size: 'small', 636 | })!.toHexString(), 637 | ).toBe('#000000'); 638 | 639 | expect( 640 | mostReadable('#371b2c', ['#000', '#fff'], { includeFallbackColors: false })!.toHexString(), 641 | ).toBe('#ffffff'); 642 | expect( 643 | mostReadable('#371b2c', ['#a9acb6'], { 644 | includeFallbackColors: true, 645 | level: 'AAA', 646 | size: 'large', 647 | })!.toHexString(), 648 | ).toBe('#a9acb6'); 649 | expect( 650 | mostReadable('#371b2c', ['#a9acb6'], { 651 | includeFallbackColors: true, 652 | level: 'AAA', 653 | size: 'small', 654 | })!.toHexString(), 655 | ).toBe('#ffffff'); 656 | }); 657 | 658 | it('should create microsoft filter', () => { 659 | expect(toMsFilter('red')).toBe( 660 | 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffff0000,endColorstr=#ffff0000)', 661 | ); 662 | expect(toMsFilter('red', 'blue')).toBe( 663 | 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffff0000,endColorstr=#ff0000ff)', 664 | ); 665 | 666 | expect(toMsFilter('transparent')).toBe( 667 | 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#00000000,endColorstr=#00000000)', 668 | ); 669 | expect(toMsFilter('transparent', 'red')).toBe( 670 | 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#00000000,endColorstr=#ffff0000)', 671 | ); 672 | 673 | expect(toMsFilter('#f0f0f0dd')).toBe( 674 | 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#ddf0f0f0,endColorstr=#ddf0f0f0)', 675 | ); 676 | expect(toMsFilter('rgba(0, 0, 255, .5')).toBe( 677 | 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#800000ff,endColorstr=#800000ff)', 678 | ); 679 | }); 680 | it('Modifications', () => { 681 | for (let i = 0; i <= 100; i++) { 682 | expect(new TinyColor('red').desaturate(i).toHex()).toBe(DESATURATIONS[i]); 683 | } 684 | 685 | for (let i = 0; i <= 100; i++) { 686 | expect(new TinyColor('red').saturate(i).toHex()).toBe(SATURATIONS[i]); 687 | } 688 | 689 | for (let i = 0; i <= 100; i++) { 690 | expect(new TinyColor('red').lighten(i).toHex()).toBe(LIGHTENS[i]); 691 | } 692 | 693 | for (let i = 0; i <= 100; i++) { 694 | expect(new TinyColor('red').brighten(i).toHex()).toBe(BRIGHTENS[i]); 695 | } 696 | 697 | for (let i = 0; i <= 100; i++) { 698 | expect(new TinyColor('red').darken(i).toHex()).toBe(DARKENS[i]); 699 | } 700 | 701 | for (let i = 0; i <= 100; i++) { 702 | expect(new TinyColor('red').tint(i).toHex()).toBe(TINTS[i]); 703 | } 704 | 705 | for (let i = 0; i <= 100; i++) { 706 | expect(new TinyColor('red').shade(i).toHex()).toBe(SHADES[i]); 707 | } 708 | 709 | expect(new TinyColor('red').greyscale().toHex()).toBe('808080'); 710 | }); 711 | it('Spin', () => { 712 | expect(Math.round(new TinyColor('#f00').spin(-1234).toHsl().h)).toBe(206); 713 | expect(Math.round(new TinyColor('#f00').spin(-360).toHsl().h)).toBe(0); 714 | expect(Math.round(new TinyColor('#f00').spin(-120).toHsl().h)).toBe(240); 715 | expect(Math.round(new TinyColor('#f00').spin(0).toHsl().h)).toBe(0); 716 | expect(Math.round(new TinyColor('#f00').spin(10).toHsl().h)).toBe(10); 717 | expect(Math.round(new TinyColor('#f00').spin(360).toHsl().h)).toBe(0); 718 | expect(Math.round(new TinyColor('#f00').spin(2345).toHsl().h)).toBe(185); 719 | 720 | [-360, 0, 360].forEach(delta => { 721 | Object.keys(names).forEach(name => { 722 | expect(new TinyColor(name).toHex()).toBe(new TinyColor(name).spin(delta).toHex()); 723 | }); 724 | }); 725 | }); 726 | it('Mix', () => { 727 | // amount 0 or none 728 | expect(new TinyColor('#000').mix('#fff').toHsl().l).toBe(0.5); 729 | expect(new TinyColor('#f00').mix('#000', 0).toHex()).toBe('ff0000'); 730 | // This case checks the the problem with floating point numbers (eg 255/90) 731 | expect(new TinyColor('#fff').mix('#000', 90).toHex()).toBe('1a1a1a'); 732 | 733 | // black and white 734 | for (let i = 0; i < 100; i++) { 735 | expect(Math.round(new TinyColor('#000').mix('#fff', i).toHsl().l * 100) / 100).toBe(i / 100); 736 | } 737 | 738 | // with colors 739 | for (let i = 0; i < 100; i++) { 740 | let newHex = Math.round((255 * (100 - i)) / 100).toString(16); 741 | 742 | if (newHex.length === 1) { 743 | newHex = '0' + newHex; 744 | } 745 | 746 | expect(new TinyColor('#f00').mix('#000', i).toHex()).toBe(newHex + '0000'); 747 | expect(new TinyColor('#0f0').mix('#000', i).toHex()).toBe(`00${newHex}00`); 748 | expect(new TinyColor('#00f').mix('#000', i).toHex()).toBe('0000' + newHex); 749 | expect(new TinyColor('transparent').mix('#000', i).toRgb().a).toBe(i / 100); 750 | } 751 | }); 752 | it('onBackground', () => { 753 | expect(new TinyColor('#ffffff').onBackground('#000').toHex()).toBe('ffffff'); 754 | expect(new TinyColor('#ffffff00').onBackground('#000').toHex()).toBe('000000'); 755 | expect(new TinyColor('#ffffff77').onBackground('#000').toHex()).toBe('777777'); 756 | expect(new TinyColor('#262a6d82').onBackground('#644242').toHex()).toBe('443658'); 757 | expect(new TinyColor('rgba(255,0,0,0.5)').onBackground('rgba(0,255,0,0.5)').toRgbString()).toBe( 758 | 'rgba(170, 85, 0, 0.75)', 759 | ); 760 | expect(new TinyColor('rgba(255,0,0,0.5)').onBackground('rgba(0,0,255,1)').toRgbString()).toBe( 761 | 'rgb(128, 0, 128)', 762 | ); 763 | expect(new TinyColor('rgba(0,0,255,1)').onBackground('rgba(0,0,0,0.5)').toRgbString()).toBe( 764 | 'rgb(0, 0, 255)', 765 | ); 766 | }); 767 | it('complement', () => { 768 | const complementDoesntModifyInstance = new TinyColor('red'); 769 | expect(complementDoesntModifyInstance.complement().toHex()).toBe('00ffff'); 770 | expect(complementDoesntModifyInstance.toHex()).toBe('ff0000'); 771 | }); 772 | 773 | it('analogous', () => { 774 | const combination = new TinyColor('red').analogous(); 775 | expect(colorsToHexString(combination)).toBe('ff0000,ff0066,ff0033,ff0000,ff3300,ff6600'); 776 | }); 777 | 778 | it('monochromatic', () => { 779 | const combination = new TinyColor('red').monochromatic(); 780 | expect(colorsToHexString(combination)).toBe('ff0000,2a0000,550000,800000,aa0000,d40000'); 781 | }); 782 | 783 | it('splitcomplement', () => { 784 | const combination = new TinyColor('red').splitcomplement(); 785 | expect(colorsToHexString(combination)).toBe('ff0000,ccff00,0066ff'); 786 | }); 787 | 788 | it('triad', () => { 789 | const combination = new TinyColor('red').triad(); 790 | expect(colorsToHexString(combination)).toBe('ff0000,00ff00,0000ff'); 791 | }); 792 | 793 | it('tetrad', () => { 794 | const combination = new TinyColor('red').tetrad(); 795 | expect(colorsToHexString(combination)).toBe('ff0000,80ff00,00ffff,7f00ff'); 796 | }); 797 | 798 | it('legacy random', () => { 799 | expect(legacyRandom().isValid).toBeTruthy(); 800 | }); 801 | }); 802 | 803 | function colorsToHexString(colors: TinyColor[]): string { 804 | return colors 805 | .map(c => { 806 | return c.toHex(); 807 | }) 808 | .join(','); 809 | } 810 | -------------------------------------------------------------------------------- /test/modifications.ts: -------------------------------------------------------------------------------- 1 | /* Originally generated with: 2 | var results = []; 3 | for (var i = 0; i <= 100; i++) results.push( tinycolor.saturate("red", i).toHex() ) 4 | console.log(JSON.stringify(results)) 5 | */ 6 | export const DESATURATIONS = [ 7 | 'ff0000', 8 | 'fe0101', 9 | 'fc0303', 10 | 'fb0404', 11 | 'fa0505', 12 | 'f90606', 13 | 'f70808', 14 | 'f60909', 15 | 'f50a0a', 16 | 'f40b0b', 17 | 'f20d0d', 18 | 'f10e0e', 19 | 'f00f0f', 20 | 'ee1111', 21 | 'ed1212', 22 | 'ec1313', 23 | 'eb1414', 24 | 'e91616', 25 | 'e81717', 26 | 'e71818', 27 | 'e61919', 28 | 'e41b1b', 29 | 'e31c1c', 30 | 'e21d1d', 31 | 'e01f1f', 32 | 'df2020', 33 | 'de2121', 34 | 'dd2222', 35 | 'db2424', 36 | 'da2525', 37 | 'd92626', 38 | 'd72828', 39 | 'd62929', 40 | 'd52a2a', 41 | 'd42b2b', 42 | 'd22d2d', 43 | 'd12e2e', 44 | 'd02f2f', 45 | 'cf3030', 46 | 'cd3232', 47 | 'cc3333', 48 | 'cb3434', 49 | 'c93636', 50 | 'c83737', 51 | 'c73838', 52 | 'c63939', 53 | 'c43b3b', 54 | 'c33c3c', 55 | 'c23d3d', 56 | 'c13e3e', 57 | 'bf4040', 58 | 'be4141', 59 | 'bd4242', 60 | 'bb4444', 61 | 'ba4545', 62 | 'b94646', 63 | 'b84747', 64 | 'b64949', 65 | 'b54a4a', 66 | 'b44b4b', 67 | 'b34d4d', 68 | 'b14e4e', 69 | 'b04f4f', 70 | 'af5050', 71 | 'ad5252', 72 | 'ac5353', 73 | 'ab5454', 74 | 'aa5555', 75 | 'a85757', 76 | 'a75858', 77 | 'a65959', 78 | 'a45b5b', 79 | 'a35c5c', 80 | 'a25d5d', 81 | 'a15e5e', 82 | '9f6060', 83 | '9e6161', 84 | '9d6262', 85 | '9c6363', 86 | '9a6565', 87 | '996666', 88 | '986767', 89 | '966969', 90 | '956a6a', 91 | '946b6b', 92 | '936c6c', 93 | '916e6e', 94 | '906f6f', 95 | '8f7070', 96 | '8e7171', 97 | '8c7373', 98 | '8b7474', 99 | '8a7575', 100 | '887777', 101 | '877878', 102 | '867979', 103 | '857a7a', 104 | '837c7c', 105 | '827d7d', 106 | '817e7e', 107 | '808080', 108 | ]; 109 | export const SATURATIONS = [ 110 | 'ff0000', 111 | 'ff0000', 112 | 'ff0000', 113 | 'ff0000', 114 | 'ff0000', 115 | 'ff0000', 116 | 'ff0000', 117 | 'ff0000', 118 | 'ff0000', 119 | 'ff0000', 120 | 'ff0000', 121 | 'ff0000', 122 | 'ff0000', 123 | 'ff0000', 124 | 'ff0000', 125 | 'ff0000', 126 | 'ff0000', 127 | 'ff0000', 128 | 'ff0000', 129 | 'ff0000', 130 | 'ff0000', 131 | 'ff0000', 132 | 'ff0000', 133 | 'ff0000', 134 | 'ff0000', 135 | 'ff0000', 136 | 'ff0000', 137 | 'ff0000', 138 | 'ff0000', 139 | 'ff0000', 140 | 'ff0000', 141 | 'ff0000', 142 | 'ff0000', 143 | 'ff0000', 144 | 'ff0000', 145 | 'ff0000', 146 | 'ff0000', 147 | 'ff0000', 148 | 'ff0000', 149 | 'ff0000', 150 | 'ff0000', 151 | 'ff0000', 152 | 'ff0000', 153 | 'ff0000', 154 | 'ff0000', 155 | 'ff0000', 156 | 'ff0000', 157 | 'ff0000', 158 | 'ff0000', 159 | 'ff0000', 160 | 'ff0000', 161 | 'ff0000', 162 | 'ff0000', 163 | 'ff0000', 164 | 'ff0000', 165 | 'ff0000', 166 | 'ff0000', 167 | 'ff0000', 168 | 'ff0000', 169 | 'ff0000', 170 | 'ff0000', 171 | 'ff0000', 172 | 'ff0000', 173 | 'ff0000', 174 | 'ff0000', 175 | 'ff0000', 176 | 'ff0000', 177 | 'ff0000', 178 | 'ff0000', 179 | 'ff0000', 180 | 'ff0000', 181 | 'ff0000', 182 | 'ff0000', 183 | 'ff0000', 184 | 'ff0000', 185 | 'ff0000', 186 | 'ff0000', 187 | 'ff0000', 188 | 'ff0000', 189 | 'ff0000', 190 | 'ff0000', 191 | 'ff0000', 192 | 'ff0000', 193 | 'ff0000', 194 | 'ff0000', 195 | 'ff0000', 196 | 'ff0000', 197 | 'ff0000', 198 | 'ff0000', 199 | 'ff0000', 200 | 'ff0000', 201 | 'ff0000', 202 | 'ff0000', 203 | 'ff0000', 204 | 'ff0000', 205 | 'ff0000', 206 | 'ff0000', 207 | 'ff0000', 208 | 'ff0000', 209 | 'ff0000', 210 | 'ff0000', 211 | ]; 212 | export const LIGHTENS = [ 213 | 'ff0000', 214 | 'ff0505', 215 | 'ff0a0a', 216 | 'ff0f0f', 217 | 'ff1414', 218 | 'ff1a1a', 219 | 'ff1f1f', 220 | 'ff2424', 221 | 'ff2929', 222 | 'ff2e2e', 223 | 'ff3333', 224 | 'ff3838', 225 | 'ff3d3d', 226 | 'ff4242', 227 | 'ff4747', 228 | 'ff4d4d', 229 | 'ff5252', 230 | 'ff5757', 231 | 'ff5c5c', 232 | 'ff6161', 233 | 'ff6666', 234 | 'ff6b6b', 235 | 'ff7070', 236 | 'ff7575', 237 | 'ff7a7a', 238 | 'ff8080', 239 | 'ff8585', 240 | 'ff8a8a', 241 | 'ff8f8f', 242 | 'ff9494', 243 | 'ff9999', 244 | 'ff9e9e', 245 | 'ffa3a3', 246 | 'ffa8a8', 247 | 'ffadad', 248 | 'ffb3b3', 249 | 'ffb8b8', 250 | 'ffbdbd', 251 | 'ffc2c2', 252 | 'ffc7c7', 253 | 'ffcccc', 254 | 'ffd1d1', 255 | 'ffd6d6', 256 | 'ffdbdb', 257 | 'ffe0e0', 258 | 'ffe5e5', 259 | 'ffebeb', 260 | 'fff0f0', 261 | 'fff5f5', 262 | 'fffafa', 263 | 'ffffff', 264 | 'ffffff', 265 | 'ffffff', 266 | 'ffffff', 267 | 'ffffff', 268 | 'ffffff', 269 | 'ffffff', 270 | 'ffffff', 271 | 'ffffff', 272 | 'ffffff', 273 | 'ffffff', 274 | 'ffffff', 275 | 'ffffff', 276 | 'ffffff', 277 | 'ffffff', 278 | 'ffffff', 279 | 'ffffff', 280 | 'ffffff', 281 | 'ffffff', 282 | 'ffffff', 283 | 'ffffff', 284 | 'ffffff', 285 | 'ffffff', 286 | 'ffffff', 287 | 'ffffff', 288 | 'ffffff', 289 | 'ffffff', 290 | 'ffffff', 291 | 'ffffff', 292 | 'ffffff', 293 | 'ffffff', 294 | 'ffffff', 295 | 'ffffff', 296 | 'ffffff', 297 | 'ffffff', 298 | 'ffffff', 299 | 'ffffff', 300 | 'ffffff', 301 | 'ffffff', 302 | 'ffffff', 303 | 'ffffff', 304 | 'ffffff', 305 | 'ffffff', 306 | 'ffffff', 307 | 'ffffff', 308 | 'ffffff', 309 | 'ffffff', 310 | 'ffffff', 311 | 'ffffff', 312 | 'ffffff', 313 | 'ffffff', 314 | ]; 315 | export const BRIGHTENS = [ 316 | 'ff0000', 317 | 'ff0303', 318 | 'ff0505', 319 | 'ff0808', 320 | 'ff0a0a', 321 | 'ff0d0d', 322 | 'ff0f0f', 323 | 'ff1212', 324 | 'ff1414', 325 | 'ff1717', 326 | 'ff1919', 327 | 'ff1c1c', 328 | 'ff1f1f', 329 | 'ff2121', 330 | 'ff2424', 331 | 'ff2626', 332 | 'ff2929', 333 | 'ff2b2b', 334 | 'ff2e2e', 335 | 'ff3030', 336 | 'ff3333', 337 | 'ff3636', 338 | 'ff3838', 339 | 'ff3b3b', 340 | 'ff3d3d', 341 | 'ff4040', 342 | 'ff4242', 343 | 'ff4545', 344 | 'ff4747', 345 | 'ff4a4a', 346 | 'ff4c4c', 347 | 'ff4f4f', 348 | 'ff5252', 349 | 'ff5454', 350 | 'ff5757', 351 | 'ff5959', 352 | 'ff5c5c', 353 | 'ff5e5e', 354 | 'ff6161', 355 | 'ff6363', 356 | 'ff6666', 357 | 'ff6969', 358 | 'ff6b6b', 359 | 'ff6e6e', 360 | 'ff7070', 361 | 'ff7373', 362 | 'ff7575', 363 | 'ff7878', 364 | 'ff7a7a', 365 | 'ff7d7d', 366 | 'ff7f7f', 367 | 'ff8282', 368 | 'ff8585', 369 | 'ff8787', 370 | 'ff8a8a', 371 | 'ff8c8c', 372 | 'ff8f8f', 373 | 'ff9191', 374 | 'ff9494', 375 | 'ff9696', 376 | 'ff9999', 377 | 'ff9c9c', 378 | 'ff9e9e', 379 | 'ffa1a1', 380 | 'ffa3a3', 381 | 'ffa6a6', 382 | 'ffa8a8', 383 | 'ffabab', 384 | 'ffadad', 385 | 'ffb0b0', 386 | 'ffb2b2', 387 | 'ffb5b5', 388 | 'ffb8b8', 389 | 'ffbaba', 390 | 'ffbdbd', 391 | 'ffbfbf', 392 | 'ffc2c2', 393 | 'ffc4c4', 394 | 'ffc7c7', 395 | 'ffc9c9', 396 | 'ffcccc', 397 | 'ffcfcf', 398 | 'ffd1d1', 399 | 'ffd4d4', 400 | 'ffd6d6', 401 | 'ffd9d9', 402 | 'ffdbdb', 403 | 'ffdede', 404 | 'ffe0e0', 405 | 'ffe3e3', 406 | 'ffe5e5', 407 | 'ffe8e8', 408 | 'ffebeb', 409 | 'ffeded', 410 | 'fff0f0', 411 | 'fff2f2', 412 | 'fff5f5', 413 | 'fff7f7', 414 | 'fffafa', 415 | 'fffcfc', 416 | 'ffffff', 417 | ]; 418 | export const DARKENS = [ 419 | 'ff0000', 420 | 'fa0000', 421 | 'f50000', 422 | 'f00000', 423 | 'eb0000', 424 | 'e60000', 425 | 'e00000', 426 | 'db0000', 427 | 'd60000', 428 | 'd10000', 429 | 'cc0000', 430 | 'c70000', 431 | 'c20000', 432 | 'bd0000', 433 | 'b80000', 434 | 'b30000', 435 | 'ad0000', 436 | 'a80000', 437 | 'a30000', 438 | '9e0000', 439 | '990000', 440 | '940000', 441 | '8f0000', 442 | '8a0000', 443 | '850000', 444 | '800000', 445 | '7a0000', 446 | '750000', 447 | '700000', 448 | '6b0000', 449 | '660000', 450 | '610000', 451 | '5c0000', 452 | '570000', 453 | '520000', 454 | '4d0000', 455 | '470000', 456 | '420000', 457 | '3d0000', 458 | '380000', 459 | '330000', 460 | '2e0000', 461 | '290000', 462 | '240000', 463 | '1f0000', 464 | '190000', 465 | '140000', 466 | '0f0000', 467 | '0a0000', 468 | '050000', 469 | '000000', 470 | '000000', 471 | '000000', 472 | '000000', 473 | '000000', 474 | '000000', 475 | '000000', 476 | '000000', 477 | '000000', 478 | '000000', 479 | '000000', 480 | '000000', 481 | '000000', 482 | '000000', 483 | '000000', 484 | '000000', 485 | '000000', 486 | '000000', 487 | '000000', 488 | '000000', 489 | '000000', 490 | '000000', 491 | '000000', 492 | '000000', 493 | '000000', 494 | '000000', 495 | '000000', 496 | '000000', 497 | '000000', 498 | '000000', 499 | '000000', 500 | '000000', 501 | '000000', 502 | '000000', 503 | '000000', 504 | '000000', 505 | '000000', 506 | '000000', 507 | '000000', 508 | '000000', 509 | '000000', 510 | '000000', 511 | '000000', 512 | '000000', 513 | '000000', 514 | '000000', 515 | '000000', 516 | '000000', 517 | '000000', 518 | '000000', 519 | '000000', 520 | ]; 521 | export const TINTS = [ 522 | 'ff0000', 523 | 'ff0303', 524 | 'ff0505', 525 | 'ff0808', 526 | 'ff0a0a', 527 | 'ff0d0d', 528 | 'ff0f0f', 529 | 'ff1212', 530 | 'ff1414', 531 | 'ff1717', 532 | 'ff1a1a', 533 | 'ff1c1c', 534 | 'ff1f1f', 535 | 'ff2121', 536 | 'ff2424', 537 | 'ff2626', 538 | 'ff2929', 539 | 'ff2b2b', 540 | 'ff2e2e', 541 | 'ff3030', 542 | 'ff3333', 543 | 'ff3636', 544 | 'ff3838', 545 | 'ff3b3b', 546 | 'ff3d3d', 547 | 'ff4040', 548 | 'ff4242', 549 | 'ff4545', 550 | 'ff4747', 551 | 'ff4a4a', 552 | 'ff4d4d', 553 | 'ff4f4f', 554 | 'ff5252', 555 | 'ff5454', 556 | 'ff5757', 557 | 'ff5959', 558 | 'ff5c5c', 559 | 'ff5e5e', 560 | 'ff6161', 561 | 'ff6363', 562 | 'ff6666', 563 | 'ff6969', 564 | 'ff6b6b', 565 | 'ff6e6e', 566 | 'ff7070', 567 | 'ff7373', 568 | 'ff7575', 569 | 'ff7878', 570 | 'ff7a7a', 571 | 'ff7d7d', 572 | 'ff8080', 573 | 'ff8282', 574 | 'ff8585', 575 | 'ff8787', 576 | 'ff8a8a', 577 | 'ff8c8c', 578 | 'ff8f8f', 579 | 'ff9191', 580 | 'ff9494', 581 | 'ff9696', 582 | 'ff9999', 583 | 'ff9c9c', 584 | 'ff9e9e', 585 | 'ffa1a1', 586 | 'ffa3a3', 587 | 'ffa6a6', 588 | 'ffa8a8', 589 | 'ffabab', 590 | 'ffadad', 591 | 'ffb0b0', 592 | 'ffb3b3', 593 | 'ffb5b5', 594 | 'ffb8b8', 595 | 'ffbaba', 596 | 'ffbdbd', 597 | 'ffbfbf', 598 | 'ffc2c2', 599 | 'ffc4c4', 600 | 'ffc7c7', 601 | 'ffc9c9', 602 | 'ffcccc', 603 | 'ffcfcf', 604 | 'ffd1d1', 605 | 'ffd4d4', 606 | 'ffd6d6', 607 | 'ffd9d9', 608 | 'ffdbdb', 609 | 'ffdede', 610 | 'ffe0e0', 611 | 'ffe3e3', 612 | 'ffe6e6', 613 | 'ffe8e8', 614 | 'ffebeb', 615 | 'ffeded', 616 | 'fff0f0', 617 | 'fff2f2', 618 | 'fff5f5', 619 | 'fff7f7', 620 | 'fffafa', 621 | 'fffcfc', 622 | 'ffffff', 623 | ]; 624 | export const SHADES = [ 625 | 'ff0000', 626 | 'fc0000', 627 | 'fa0000', 628 | 'f70000', 629 | 'f50000', 630 | 'f20000', 631 | 'f00000', 632 | 'ed0000', 633 | 'eb0000', 634 | 'e80000', 635 | 'e60000', 636 | 'e30000', 637 | 'e00000', 638 | 'de0000', 639 | 'db0000', 640 | 'd90000', 641 | 'd60000', 642 | 'd40000', 643 | 'd10000', 644 | 'cf0000', 645 | 'cc0000', 646 | 'c90000', 647 | 'c70000', 648 | 'c40000', 649 | 'c20000', 650 | 'bf0000', 651 | 'bd0000', 652 | 'ba0000', 653 | 'b80000', 654 | 'b50000', 655 | 'b30000', 656 | 'b00000', 657 | 'ad0000', 658 | 'ab0000', 659 | 'a80000', 660 | 'a60000', 661 | 'a30000', 662 | 'a10000', 663 | '9e0000', 664 | '9c0000', 665 | '990000', 666 | '960000', 667 | '940000', 668 | '910000', 669 | '8f0000', 670 | '8c0000', 671 | '8a0000', 672 | '870000', 673 | '850000', 674 | '820000', 675 | '800000', 676 | '7d0000', 677 | '7a0000', 678 | '780000', 679 | '750000', 680 | '730000', 681 | '700000', 682 | '6e0000', 683 | '6b0000', 684 | '690000', 685 | '660000', 686 | '630000', 687 | '610000', 688 | '5e0000', 689 | '5c0000', 690 | '590000', 691 | '570000', 692 | '540000', 693 | '520000', 694 | '4f0000', 695 | '4d0000', 696 | '4a0000', 697 | '470000', 698 | '450000', 699 | '420000', 700 | '400000', 701 | '3d0000', 702 | '3b0000', 703 | '380000', 704 | '360000', 705 | '330000', 706 | '300000', 707 | '2e0000', 708 | '2b0000', 709 | '290000', 710 | '260000', 711 | '240000', 712 | '210000', 713 | '1f0000', 714 | '1c0000', 715 | '1a0000', 716 | '170000', 717 | '140000', 718 | '120000', 719 | '0f0000', 720 | '0d0000', 721 | '0a0000', 722 | '080000', 723 | '050000', 724 | '030000', 725 | '000000', 726 | ]; 727 | -------------------------------------------------------------------------------- /test/random.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { random } from '../src/random.js'; 4 | 5 | describe('random', () => { 6 | it('should accept count', () => { 7 | expect(random({ count: 10 }).length).toBe(10); 8 | }); 9 | it('should accept hue', () => { 10 | let colors = random({ hue: 'purple', count: 3, seed: 11100 }).map(n => n.toHexString()); 11 | expect(colors).toEqual(['#9b22e6', '#9f1ceb', '#a316f0']); 12 | colors = random({ hue: 'red', count: 3, seed: 13371337 }).map(n => n.toHexString()); 13 | expect(colors).toEqual(['#e34236', '#e34230', '#e8432a']); 14 | colors = random({ hue: 'blue', count: 3, seed: 13371337 }).map(n => n.toHexString()); 15 | expect(colors).toEqual(['#3346d6', '#2e39d9', '#2828de']); 16 | colors = random({ hue: 'purple', count: 3, seed: 13371337 }).map(n => n.toHexString()); 17 | expect(colors).toEqual(['#9335db', '#952fde', '#9929e3']); 18 | colors = random({ hue: 'orange', count: 3, seed: 13371337 }).map(n => n.toHexString()); 19 | expect(colors).toEqual(['#e8a438', '#ebaa31', '#edac2b']); 20 | colors = random({ hue: 'pink', count: 3, seed: 13371337 }).map(n => n.toHexString()); 21 | expect(colors).toEqual(['#f03ab9', '#f032b1', '#f22ca9']); 22 | colors = random({ hue: '#E6E6FA', count: 3, seed: 420420 }).map(n => n.toHexString()); 23 | expect(colors).toEqual(['#4141d1', '#3939d4', '#3333d6']); 24 | colors = random({ hue: 999, count: 3, seed: 420420 }).map(n => n.toHexString()); 25 | expect(colors).toEqual(['#4167d1', '#393cd4', '#5733d6']); 26 | colors = random({ hue: 'monochrome', count: 3, seed: 420420 }).map(n => n.toHexString()); 27 | expect(colors).toEqual(['#9e9e9e', '#a8a8a8', '#b3b3b3']); 28 | colors = random({ hue: NaN, count: 3, seed: 420420 }).map(n => n.toHexString()); 29 | expect(colors).toEqual(['#4167d1', '#393cd4', '#5733d6']); 30 | }); 31 | it('should accept luminosity', () => { 32 | let colors = random({ luminosity: 'bright', count: 3, seed: 11100 }).map(n => n.toHexString()); 33 | expect(colors).toEqual(['#d916f2', '#f511da', '#f70ca5']); 34 | colors = random({ luminosity: 'dark', count: 3, seed: 9999923 }).map(n => n.toHexString()); 35 | expect(colors).toEqual(['#06377a', '#05197d', '#0d0580']); 36 | }); 37 | it('should accept luminosity', () => { 38 | let colors = random({ luminosity: 'bright', count: 3, seed: 11100 }).map(n => n.toHexString()); 39 | expect(colors).toEqual(['#d916f2', '#f511da', '#f70ca5']); 40 | colors = random({ luminosity: 'dark', count: 3, seed: 9999923 }).map(n => n.toHexString()); 41 | expect(colors).toEqual(['#06377a', '#05197d', '#0d0580']); 42 | colors = random({ luminosity: 'light', count: 3, seed: 9999923 }).map(n => n.toHexString()); 43 | expect(colors).toEqual(['#91baf2', '#8e9ff5', '#938cf5']); 44 | colors = random({ luminosity: 'bright', count: 3, seed: 9999923 }).map(n => n.toHexString()); 45 | expect(colors).toEqual(['#2568c4', '#223ec9', '#2b1fcf']); 46 | colors = random({ luminosity: 'random', count: 3, seed: 9999923 }).map(n => n.toHexString()); 47 | expect(colors).toEqual(['#3e6396', '#3b4ca1', '#4038ab']); 48 | }); 49 | it('should accept hue and luminosity', () => { 50 | let colors = random({ hue: 'red', luminosity: 'bright', count: 3, seed: 13378008 }).map(n => 51 | n.toHexString(), 52 | ); 53 | expect(colors).toEqual(['#db2a21', '#de2d1d', '#e33119']); 54 | colors = random({ hue: 'red', luminosity: 'dark', count: 3, seed: 13378008 }).map(n => 55 | n.toHexString(), 56 | ); 57 | expect(colors).toEqual(['#a60f07', '#a61205', '#a81805']); 58 | }); 59 | it('should accept alpha', () => { 60 | const colors = random({ alpha: 0.4, seed: 13378008 }); 61 | expect(colors.a).toBe(0.4); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "files": ["./src/public_api", "./src/umd_api"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "lib": ["ES2021"], 5 | "target": "ES2021", 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "declaration": true, 9 | "outDir": "dist", 10 | "strict": true, 11 | "sourceMap": false, 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "noImplicitAny": false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "declaration": false, 6 | "outDir": "dist/module", 7 | "skipLibCheck": true 8 | }, 9 | "files": ["./src/public_api", "./src/umd_api"] 10 | } 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | provider: 'v8', 7 | reporter: ['text', 'json', 'html'], 8 | }, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------