├── .gitignore ├── README.md ├── babel.config.json ├── benchmarks └── index.ts ├── docs ├── README.md ├── functions │ ├── alpha.md │ ├── blend.md │ ├── darken.md │ ├── format.md │ ├── formatHEX.md │ ├── formatHEXA.md │ ├── formatHSLA.md │ ├── formatHWBA.md │ ├── formatRGBA.md │ ├── from.md │ ├── getAlpha.md │ ├── getBlue.md │ ├── getGreen.md │ ├── getLuminance.md │ ├── getRed.md │ ├── lighten.md │ ├── newColor.md │ ├── parse.md │ ├── parseColor.md │ ├── parseHex.md │ ├── setAlpha.md │ ├── setBlue.md │ ├── setGreen.md │ ├── setRed.md │ ├── toHSLA.md │ ├── toHWBA.md │ ├── toNumber.md │ └── toRGBA.md ├── string │ ├── README.md │ └── functions │ │ ├── alpha.md │ │ ├── blend.md │ │ ├── darken.md │ │ ├── getLuminance.md │ │ └── lighten.md ├── type-aliases │ └── Color.md └── variables │ ├── OFFSET_A.md │ ├── OFFSET_B.md │ ├── OFFSET_G.md │ └── OFFSET_R.md ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── src ├── bit.ts ├── color.test.ts ├── convert.ts ├── core.ts ├── format.ts ├── functions.ts ├── index.ts ├── parse.ts └── string.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | .npmignore 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | color-bits 3 |

4 | 5 |

6 | High-performance color library 7 |

8 | 9 | This library represents RGBA colors as a single `int32` number and avoids allocating memory as much as possible while parsing, handling, and formatting colors, to provide the best possible memory and CPU efficiency. 10 | 11 |

12 | Benchmarks • 13 | Install • 14 | Technical details • 15 | Documentation • 16 | License 17 |

18 | 19 |

20 | 🔴🟠🟡🟢🔵🟣 21 |

22 | 23 | ### ⚡ Benchmarks 24 | 25 | | Library | Operations/sec | Relative speed | 26 | | --- | --: | --- | 27 | | **color-bits** | **22 966 299** | fastest | 28 | | colord | 4 308 547 | 81.24% slower | 29 | | tinycolor2 | 1 475 762 | 93.57% slower | 30 | | chroma-js | 846 924 | 96.31% slower | 31 | | color | 799 262 | 96.52% slower | 32 | 33 | ### 🛠️ Install 34 | 35 | ```sh 36 | pnpm install color-bits 37 | ``` 38 | 39 | ### 📑 Technical details 40 | 41 | Due to the compact representation, `color-bits` preserves **at most 8 bits of precision for each channel**, so an operation like `alpha(color, 0.000001)` would simply return the same color with no modification. 42 | 43 | `color-bits` supports the full **CSS Color Module Level 4** color spaces **in absolute representations only**, so: 44 | - Yes: `oklab(59.69% 0.1007 0.1191)` 45 | - No: `oklab(from green l a b / 0.5)` 46 | 47 | When parsing and converting non-sRGB color spaces, `color-bits` behaves the same as browsers do, which differs from the formal CSS spec! In technical terms: non-sRGB color spaces with a wider gamut are converted using clipping rather than gamut-mapping. 48 | 49 | For performance reasons, the representation is `int32`, not `uint32`. It is expected if you see negative numbers when you print the color value. 50 | 51 | Every function is tree-shakeable, so the bundle size cost should be from 1.5kb to 3kb, depending on which functions you use. 52 | 53 | ### 📚 Documentation 54 | 55 | [Docs for color-bits](https://github.com/romgrk/color-bits/tree/master/docs/README.md) 56 | [Docs for color-bits/string](https://github.com/romgrk/color-bits/tree/master/docs/string/README.md) 57 | 58 | If you're storing and manipulating colors frequently, you should use the `color-bits` exports directly, e.g. 59 | 60 | ```tsx 61 | import * as Color from 'color-bits' 62 | 63 | const background = Color.parse('#232323') 64 | const seeThrough = Color.alpha(background, 0.5) 65 | const output = Color.format(seeThrough) // #RRGGBBAA string 66 | ``` 67 | 68 | The `color-bits/string` module wraps some of the functions to accept string colors as input/output, which may be useful if you're not storing the colors but just transforming them on the fly. It can be faster than calling the functions separately in some cases. 69 | 70 | ```tsx 71 | import * as Color from 'color-bits/string' 72 | 73 | const output = Color.alpha('#232323', 0.5) // #RRGGBBAA string 74 | ``` 75 | 76 | ### 📜 License 77 | 78 | I release any of the code I wrote here to the public domain. Feel free to copy/paste in part or in full without attribution. 79 | 80 | Some parts of the codebase have been extracted from Chrome's devtools, MaterialUI, and stackoverflow, those contain a license notice or attribution in code comments, inline. Everything is MIT-compatible. 81 | 82 |

83 | 🔴🟠🟡🟢🔵🟣 84 |

85 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "current" } }], 4 | "@babel/preset-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /benchmarks/index.ts: -------------------------------------------------------------------------------- 1 | import b from 'benny' 2 | import { alpha as colorBitsAlpha } from '../src/string' 3 | import { colord } from 'colord' 4 | import tinycolor2 from 'tinycolor2' 5 | import color from 'color' 6 | import chroma from 'chroma-js' 7 | 8 | const input = '#808080' 9 | 10 | b.suite( 11 | `Parse color, set to 0.5 opacity, and convert back to hexadecimal`, 12 | 13 | b.add('color-bits', () => { 14 | colorBitsAlpha(input, 0.5) 15 | }), 16 | 17 | b.add('colord', () => { 18 | colord(input).alpha(0.5).toHex() 19 | }), 20 | 21 | b.add('color', () => { 22 | color(input).alpha(0.5).hex() 23 | }), 24 | 25 | b.add('tinycolor2', () => { 26 | tinycolor2(input).setAlpha(0.5).toHexString() 27 | }), 28 | 29 | b.add('chroma-js', () => { 30 | chroma(input).alpha(0.5).hex() 31 | }), 32 | 33 | b.cycle(), 34 | b.complete() 35 | ) 36 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | **color-bits** • **Docs** 2 | 3 | *** 4 | 5 | # color-bits 6 | 7 | ## Type Aliases 8 | 9 | - [Color](type-aliases/Color.md) 10 | 11 | ## Variables 12 | 13 | - [OFFSET\_A](variables/OFFSET_A.md) 14 | - [OFFSET\_B](variables/OFFSET_B.md) 15 | - [OFFSET\_G](variables/OFFSET_G.md) 16 | - [OFFSET\_R](variables/OFFSET_R.md) 17 | 18 | ## Functions 19 | 20 | - [alpha](functions/alpha.md) 21 | - [blend](functions/blend.md) 22 | - [darken](functions/darken.md) 23 | - [format](functions/format.md) 24 | - [formatHEX](functions/formatHEX.md) 25 | - [formatHEXA](functions/formatHEXA.md) 26 | - [formatHSLA](functions/formatHSLA.md) 27 | - [formatHWBA](functions/formatHWBA.md) 28 | - [formatRGBA](functions/formatRGBA.md) 29 | - [from](functions/from.md) 30 | - [getAlpha](functions/getAlpha.md) 31 | - [getBlue](functions/getBlue.md) 32 | - [getGreen](functions/getGreen.md) 33 | - [getLuminance](functions/getLuminance.md) 34 | - [getRed](functions/getRed.md) 35 | - [lighten](functions/lighten.md) 36 | - [newColor](functions/newColor.md) 37 | - [parse](functions/parse.md) 38 | - [parseColor](functions/parseColor.md) 39 | - [parseHex](functions/parseHex.md) 40 | - [setAlpha](functions/setAlpha.md) 41 | - [setBlue](functions/setBlue.md) 42 | - [setGreen](functions/setGreen.md) 43 | - [setRed](functions/setRed.md) 44 | - [toHSLA](functions/toHSLA.md) 45 | - [toHWBA](functions/toHWBA.md) 46 | - [toNumber](functions/toNumber.md) 47 | - [toRGBA](functions/toRGBA.md) 48 | -------------------------------------------------------------------------------- /docs/functions/alpha.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / alpha 6 | 7 | # Function: alpha() 8 | 9 | > **alpha**(`color`, `value`): [`Color`](../type-aliases/Color.md) 10 | 11 | Modifies color alpha channel. 12 | 13 | ## Parameters 14 | 15 | • **color**: `number` 16 | 17 | Color 18 | 19 | • **value**: `number` 20 | 21 | Value in the range [0, 1] 22 | 23 | ## Returns 24 | 25 | [`Color`](../type-aliases/Color.md) 26 | 27 | ## Defined in 28 | 29 | [functions.ts:11](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/functions.ts#L11) 30 | -------------------------------------------------------------------------------- /docs/functions/blend.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / blend 6 | 7 | # Function: blend() 8 | 9 | > **blend**(`background`, `overlay`, `opacity`, `gamma`?): `number` 10 | 11 | Blend (aka mix) two colors together. 12 | 13 | ## Parameters 14 | 15 | • **background**: `number` 16 | 17 | The background color 18 | 19 | • **overlay**: `number` 20 | 21 | The overlay color that is affected by 22 | 23 | • **opacity**: `number` 24 | 25 | Opacity (alpha) for 26 | 27 | • **gamma?**: `number` = `1.0` 28 | 29 | Gamma correction coefficient. `1.0` to match browser behavior, `2.2` for gamma-corrected blending. 30 | 31 | ## Returns 32 | 33 | `number` 34 | 35 | ## Opacity 36 | 37 | ## Overlay 38 | 39 | ## Defined in 40 | 41 | [functions.ts:62](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/functions.ts#L62) 42 | -------------------------------------------------------------------------------- /docs/functions/darken.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / darken 6 | 7 | # Function: darken() 8 | 9 | > **darken**(`color`, `coefficient`): [`Color`](../type-aliases/Color.md) 10 | 11 | Darkens a color. 12 | 13 | ## Parameters 14 | 15 | • **color**: `number` 16 | 17 | Color 18 | 19 | • **coefficient**: `number` 20 | 21 | Multiplier in the range [0, 1] 22 | 23 | ## Returns 24 | 25 | [`Color`](../type-aliases/Color.md) 26 | 27 | ## Defined in 28 | 29 | [functions.ts:20](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/functions.ts#L20) 30 | -------------------------------------------------------------------------------- /docs/functions/format.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / format 6 | 7 | # Function: format() 8 | 9 | > **format**(`color`): `string` 10 | 11 | Format to a #RRGGBBAA string 12 | 13 | ## Parameters 14 | 15 | • **color**: `number` 16 | 17 | ## Returns 18 | 19 | `string` 20 | 21 | ## Defined in 22 | 23 | [format.ts:18](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L18) 24 | -------------------------------------------------------------------------------- /docs/functions/formatHEX.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / formatHEX 6 | 7 | # Function: formatHEX() 8 | 9 | > **formatHEX**(`color`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `string` 18 | 19 | ## Defined in 20 | 21 | [format.ts:31](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L31) 22 | -------------------------------------------------------------------------------- /docs/functions/formatHEXA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / formatHEXA 6 | 7 | # Function: formatHEXA() 8 | 9 | > **formatHEXA**(`color`): `string` 10 | 11 | Format to a #RRGGBBAA string 12 | 13 | ## Parameters 14 | 15 | • **color**: `number` 16 | 17 | ## Returns 18 | 19 | `string` 20 | 21 | ## Defined in 22 | 23 | [format.ts:21](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L21) 24 | -------------------------------------------------------------------------------- /docs/functions/formatHSLA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / formatHSLA 6 | 7 | # Function: formatHSLA() 8 | 9 | > **formatHSLA**(`color`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `string` 18 | 19 | ## Defined in 20 | 21 | [format.ts:53](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L53) 22 | -------------------------------------------------------------------------------- /docs/functions/formatHWBA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / formatHWBA 6 | 7 | # Function: formatHWBA() 8 | 9 | > **formatHWBA**(`color`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `string` 18 | 19 | ## Defined in 20 | 21 | [format.ts:81](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L81) 22 | -------------------------------------------------------------------------------- /docs/functions/formatRGBA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / formatRGBA 6 | 7 | # Function: formatRGBA() 8 | 9 | > **formatRGBA**(`color`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `string` 18 | 19 | ## Defined in 20 | 21 | [format.ts:40](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L40) 22 | -------------------------------------------------------------------------------- /docs/functions/from.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / from 6 | 7 | # Function: from() 8 | 9 | > **from**(`color`): `number` 10 | 11 | Creates a new color from the given number value, e.g. 0x599eff. 12 | 13 | ## Parameters 14 | 15 | • **color**: `number` 16 | 17 | ## Returns 18 | 19 | `number` 20 | 21 | ## Defined in 22 | 23 | [core.ts:28](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L28) 24 | -------------------------------------------------------------------------------- /docs/functions/getAlpha.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / getAlpha 6 | 7 | # Function: getAlpha() 8 | 9 | > **getAlpha**(`c`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | ## Returns 16 | 17 | `number` 18 | 19 | ## Defined in 20 | 21 | [core.ts:48](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L48) 22 | -------------------------------------------------------------------------------- /docs/functions/getBlue.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / getBlue 6 | 7 | # Function: getBlue() 8 | 9 | > **getBlue**(`c`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | ## Returns 16 | 17 | `number` 18 | 19 | ## Defined in 20 | 21 | [core.ts:47](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L47) 22 | -------------------------------------------------------------------------------- /docs/functions/getGreen.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / getGreen 6 | 7 | # Function: getGreen() 8 | 9 | > **getGreen**(`c`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | ## Returns 16 | 17 | `number` 18 | 19 | ## Defined in 20 | 21 | [core.ts:46](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L46) 22 | -------------------------------------------------------------------------------- /docs/functions/getLuminance.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / getLuminance 6 | 7 | # Function: getLuminance() 8 | 9 | > **getLuminance**(`color`): `number` 10 | 11 | The relative brightness of any point in a color space, normalized to 0 for 12 | darkest black and 1 for lightest white. 13 | 14 | ## Parameters 15 | 16 | • **color**: `number` 17 | 18 | ## Returns 19 | 20 | `number` 21 | 22 | The relative brightness of the color in the range 0 - 1, with 3 digits precision 23 | 24 | ## Defined in 25 | 26 | [functions.ts:78](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/functions.ts#L78) 27 | -------------------------------------------------------------------------------- /docs/functions/getRed.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / getRed 6 | 7 | # Function: getRed() 8 | 9 | > **getRed**(`c`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | ## Returns 16 | 17 | `number` 18 | 19 | ## Defined in 20 | 21 | [core.ts:45](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L45) 22 | -------------------------------------------------------------------------------- /docs/functions/lighten.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / lighten 6 | 7 | # Function: lighten() 8 | 9 | > **lighten**(`color`, `coefficient`): [`Color`](../type-aliases/Color.md) 10 | 11 | Lighten a color. 12 | 13 | ## Parameters 14 | 15 | • **color**: `number` 16 | 17 | Color 18 | 19 | • **coefficient**: `number` 20 | 21 | Multiplier in the range [0, 1] 22 | 23 | ## Returns 24 | 25 | [`Color`](../type-aliases/Color.md) 26 | 27 | ## Defined in 28 | 29 | [functions.ts:41](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/functions.ts#L41) 30 | -------------------------------------------------------------------------------- /docs/functions/newColor.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / newColor 6 | 7 | # Function: newColor() 8 | 9 | > **newColor**(`r`, `g`, `b`, `a`): `number` 10 | 11 | Creates a new color from the given RGBA components. 12 | Every component should be in the [0, 255] range. 13 | 14 | ## Parameters 15 | 16 | • **r**: `number` 17 | 18 | • **g**: `number` 19 | 20 | • **b**: `number` 21 | 22 | • **a**: `number` 23 | 24 | ## Returns 25 | 26 | `number` 27 | 28 | ## Defined in 29 | 30 | [core.ts:16](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L16) 31 | -------------------------------------------------------------------------------- /docs/functions/parse.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / parse 6 | 7 | # Function: parse() 8 | 9 | > **parse**(`color`): [`Color`](../type-aliases/Color.md) 10 | 11 | Parse CSS color 12 | 13 | ## Parameters 14 | 15 | • **color**: `string` 16 | 17 | CSS color string: #xxx, #xxxxxx, #xxxxxxxx, rgb(), rgba(), hsl(), hsla(), color() 18 | 19 | ## Returns 20 | 21 | [`Color`](../type-aliases/Color.md) 22 | 23 | ## Defined in 24 | 25 | [parse.ts:38](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/parse.ts#L38) 26 | -------------------------------------------------------------------------------- /docs/functions/parseColor.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / parseColor 6 | 7 | # Function: parseColor() 8 | 9 | > **parseColor**(`color`): [`Color`](../type-aliases/Color.md) 10 | 11 | Parse CSS color 12 | https://developer.mozilla.org/en-US/docs/Web/CSS/color_value 13 | 14 | ## Parameters 15 | 16 | • **color**: `string` 17 | 18 | CSS color string: rgb(), rgba(), hsl(), hsla(), color() 19 | 20 | ## Returns 21 | 22 | [`Color`](../type-aliases/Color.md) 23 | 24 | ## Defined in 25 | 26 | [parse.ts:98](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/parse.ts#L98) 27 | -------------------------------------------------------------------------------- /docs/functions/parseHex.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / parseHex 6 | 7 | # Function: parseHex() 8 | 9 | > **parseHex**(`color`): [`Color`](../type-aliases/Color.md) 10 | 11 | Parse hexadecimal CSS color 12 | 13 | ## Parameters 14 | 15 | • **color**: `string` 16 | 17 | Hex color string: #xxx, #xxxxxx, #xxxxxxxx 18 | 19 | ## Returns 20 | 21 | [`Color`](../type-aliases/Color.md) 22 | 23 | ## Defined in 24 | 25 | [parse.ts:50](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/parse.ts#L50) 26 | -------------------------------------------------------------------------------- /docs/functions/setAlpha.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / setAlpha 6 | 7 | # Function: setAlpha() 8 | 9 | > **setAlpha**(`c`, `value`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `number` 20 | 21 | ## Defined in 22 | 23 | [core.ts:53](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L53) 24 | -------------------------------------------------------------------------------- /docs/functions/setBlue.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / setBlue 6 | 7 | # Function: setBlue() 8 | 9 | > **setBlue**(`c`, `value`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `number` 20 | 21 | ## Defined in 22 | 23 | [core.ts:52](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L52) 24 | -------------------------------------------------------------------------------- /docs/functions/setGreen.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / setGreen 6 | 7 | # Function: setGreen() 8 | 9 | > **setGreen**(`c`, `value`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `number` 20 | 21 | ## Defined in 22 | 23 | [core.ts:51](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L51) 24 | -------------------------------------------------------------------------------- /docs/functions/setRed.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / setRed 6 | 7 | # Function: setRed() 8 | 9 | > **setRed**(`c`, `value`): `number` 10 | 11 | ## Parameters 12 | 13 | • **c**: `number` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `number` 20 | 21 | ## Defined in 22 | 23 | [core.ts:50](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L50) 24 | -------------------------------------------------------------------------------- /docs/functions/toHSLA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / toHSLA 6 | 7 | # Function: toHSLA() 8 | 9 | > **toHSLA**(`color`): `object` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `object` 18 | 19 | ### a 20 | 21 | > **a**: `number` 22 | 23 | ### h 24 | 25 | > **h**: `number` 26 | 27 | ### l 28 | 29 | > **l**: `number` 30 | 31 | ### s 32 | 33 | > **s**: `number` 34 | 35 | ## Defined in 36 | 37 | [format.ts:67](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L67) 38 | -------------------------------------------------------------------------------- /docs/functions/toHWBA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / toHWBA 6 | 7 | # Function: toHWBA() 8 | 9 | > **toHWBA**(`color`): `object` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `object` 18 | 19 | ### a 20 | 21 | > **a**: `number` 22 | 23 | ### b 24 | 25 | > **b**: `number` 26 | 27 | ### h 28 | 29 | > **h**: `number` 30 | 31 | ### w 32 | 33 | > **w**: `number` 34 | 35 | ## Defined in 36 | 37 | [format.ts:95](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L95) 38 | -------------------------------------------------------------------------------- /docs/functions/toNumber.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / toNumber 6 | 7 | # Function: toNumber() 8 | 9 | > **toNumber**(`color`): `number` 10 | 11 | Turns the color into its equivalent number representation. 12 | This is essentially a cast from int32 to uint32. 13 | 14 | ## Parameters 15 | 16 | • **color**: `number` 17 | 18 | ## Returns 19 | 20 | `number` 21 | 22 | ## Defined in 23 | 24 | [core.ts:41](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L41) 25 | -------------------------------------------------------------------------------- /docs/functions/toRGBA.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / toRGBA 6 | 7 | # Function: toRGBA() 8 | 9 | > **toRGBA**(`color`): `object` 10 | 11 | ## Parameters 12 | 13 | • **color**: `number` 14 | 15 | ## Returns 16 | 17 | `object` 18 | 19 | ### a 20 | 21 | > **a**: `number` 22 | 23 | ### b 24 | 25 | > **b**: `number` 26 | 27 | ### g 28 | 29 | > **g**: `number` 30 | 31 | ### r 32 | 33 | > **r**: `number` 34 | 35 | ## Defined in 36 | 37 | [format.ts:44](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/format.ts#L44) 38 | -------------------------------------------------------------------------------- /docs/string/README.md: -------------------------------------------------------------------------------- 1 | **color-bits** • **Docs** 2 | 3 | *** 4 | 5 | # color-bits 6 | 7 | ## Functions 8 | 9 | - [alpha](functions/alpha.md) 10 | - [blend](functions/blend.md) 11 | - [darken](functions/darken.md) 12 | - [getLuminance](functions/getLuminance.md) 13 | - [lighten](functions/lighten.md) 14 | -------------------------------------------------------------------------------- /docs/string/functions/alpha.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / alpha 6 | 7 | # Function: alpha() 8 | 9 | > **alpha**(`color`, `value`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `string` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `string` 20 | 21 | ## Defined in 22 | 23 | [string.ts:13](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/string.ts#L13) 24 | -------------------------------------------------------------------------------- /docs/string/functions/blend.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / blend 6 | 7 | # Function: blend() 8 | 9 | > **blend**(`background`, `overlay`, `opacity`, `gamma`): `string` 10 | 11 | ## Parameters 12 | 13 | • **background**: `string` 14 | 15 | • **overlay**: `string` 16 | 17 | • **opacity**: `number` 18 | 19 | • **gamma**: `number` 20 | 21 | ## Returns 22 | 23 | `string` 24 | 25 | ## Defined in 26 | 27 | [string.ts:14](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/string.ts#L14) 28 | -------------------------------------------------------------------------------- /docs/string/functions/darken.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / darken 6 | 7 | # Function: darken() 8 | 9 | > **darken**(`color`, `value`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `string` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `string` 20 | 21 | ## Defined in 22 | 23 | [string.ts:15](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/string.ts#L15) 24 | -------------------------------------------------------------------------------- /docs/string/functions/getLuminance.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / getLuminance 6 | 7 | # Function: getLuminance() 8 | 9 | > **getLuminance**(`color`): `number` 10 | 11 | ## Parameters 12 | 13 | • **color**: `string` 14 | 15 | ## Returns 16 | 17 | `number` 18 | 19 | ## Defined in 20 | 21 | [string.ts:17](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/string.ts#L17) 22 | -------------------------------------------------------------------------------- /docs/string/functions/lighten.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / lighten 6 | 7 | # Function: lighten() 8 | 9 | > **lighten**(`color`, `value`): `string` 10 | 11 | ## Parameters 12 | 13 | • **color**: `string` 14 | 15 | • **value**: `number` 16 | 17 | ## Returns 18 | 19 | `string` 20 | 21 | ## Defined in 22 | 23 | [string.ts:16](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/string.ts#L16) 24 | -------------------------------------------------------------------------------- /docs/type-aliases/Color.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / Color 6 | 7 | # Type Alias: Color 8 | 9 | > **Color**: `number` 10 | 11 | ## Defined in 12 | 13 | [core.ts:5](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L5) 14 | -------------------------------------------------------------------------------- /docs/variables/OFFSET_A.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / OFFSET\_A 6 | 7 | # Variable: OFFSET\_A 8 | 9 | > `const` **OFFSET\_A**: `0` = `0` 10 | 11 | ## Defined in 12 | 13 | [core.ts:10](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L10) 14 | -------------------------------------------------------------------------------- /docs/variables/OFFSET_B.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / OFFSET\_B 6 | 7 | # Variable: OFFSET\_B 8 | 9 | > `const` **OFFSET\_B**: `8` = `8` 10 | 11 | ## Defined in 12 | 13 | [core.ts:9](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L9) 14 | -------------------------------------------------------------------------------- /docs/variables/OFFSET_G.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / OFFSET\_G 6 | 7 | # Variable: OFFSET\_G 8 | 9 | > `const` **OFFSET\_G**: `16` = `16` 10 | 11 | ## Defined in 12 | 13 | [core.ts:8](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L8) 14 | -------------------------------------------------------------------------------- /docs/variables/OFFSET_R.md: -------------------------------------------------------------------------------- 1 | [**color-bits**](../README.md) • **Docs** 2 | 3 | *** 4 | 5 | [color-bits](../README.md) / OFFSET\_R 6 | 7 | # Variable: OFFSET\_R 8 | 9 | > `const` **OFFSET\_R**: `24` = `24` 10 | 11 | ## Defined in 12 | 13 | [core.ts:7](https://github.com/romgrk/color-bits/blob/e6e18569fa37645f22dd4f4c831dece10d0dd00b/src/core.ts#L7) 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | export default { 3 | extensionsToTreatAsEsm: ['.ts'], 4 | transform: { 5 | '\\.ts$': ['ts-jest', { 6 | useESM: true, 7 | }], 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-bits", 3 | "version": "1.1.0", 4 | "main": "./build/index.js", 5 | "sideEffects": false, 6 | "scripts": { 7 | "prepack": "pnpm run build && npmignore --auto", 8 | "build": "tsc", 9 | "start": "tsc --watch", 10 | "docs": "pnpm run docs:core && pnpm run docs:string", 11 | "docs:core": "typedoc --readme none --plugin typedoc-plugin-markdown --out ./docs ./src/index.ts", 12 | "docs:string": "typedoc --readme none --plugin typedoc-plugin-markdown --out ./docs/string ./src/string.ts", 13 | "benchmark": "tsx ./benchmarks/index.ts", 14 | "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --watch" 15 | }, 16 | "exports": { 17 | ".": "./build/index.js", 18 | "./*": "./build/*", 19 | "./string": "./build/string.js" 20 | }, 21 | "types": "./build/index.d.ts", 22 | "keywords": [ 23 | "color", 24 | "colors" 25 | ], 26 | "author": "", 27 | "license": "ISC", 28 | "description": "High performance color library", 29 | "publishConfig": { 30 | "ignore": [ 31 | "!build/", 32 | "docs/", 33 | "src/", 34 | "test/" 35 | ] 36 | }, 37 | "devDependencies": { 38 | "@babel/preset-env": "^7.25.4", 39 | "@babel/preset-typescript": "^7.24.7", 40 | "@jest/globals": "^29.7.0", 41 | "@types/chai": "^4.3.19", 42 | "@types/jest": "^29.5.12", 43 | "benny": "^3.7.1", 44 | "chai": "^5.1.1", 45 | "chroma-js": "^3.0.0", 46 | "color": "^4.2.3", 47 | "colord": "^2.9.3", 48 | "jest": "^29.7.0", 49 | "npmignore": "^0.3.1", 50 | "tinycolor2": "^1.6.0", 51 | "ts-jest": "^29.2.5", 52 | "tsdoc-markdown": "^0.6.0", 53 | "tsx": "^4.19.0", 54 | "typedoc": "^0.26.6", 55 | "typedoc-plugin-markdown": "^4.2.6", 56 | "typescript": "^5.5.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/bit.ts: -------------------------------------------------------------------------------- 1 | // Bitwise functions 2 | // 3 | // The color representation would ideally be 32-bits unsigned, but JS bitwise 4 | // operators only work as 32-bits signed. The range of Smi values on V8 is also 5 | // 32-bits signed. Those two factors make it that it's much more efficient to just 6 | // use signed integers to represent the data. 7 | // 8 | // Colors with a R channel >= 0x80 will be a negative number, but that's not really 9 | // an issue at any point because the bits for signed and unsigned integers are always 10 | // the same, only their interpretation changes. 11 | 12 | const INT32_TO_UINT32_OFFSET = 2 ** 32; 13 | 14 | export function cast(n: number) { 15 | if (n < 0) { 16 | return n + INT32_TO_UINT32_OFFSET; 17 | } 18 | return n; 19 | } 20 | 21 | export function get(n: number, offset: number) { 22 | return (n >> offset) & 0xff; 23 | } 24 | 25 | export function set(n: number, offset: number, byte: number) { 26 | return n ^ ((n ^ (byte << offset)) & (0xff << offset)); 27 | } 28 | -------------------------------------------------------------------------------- /src/color.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as Color from './index'; 3 | 4 | const c = Color.from; 5 | 6 | describe('Color', () => { 7 | it('can encode/decode the representation', () => { 8 | const color = Color.from(0x599eff80) 9 | expect(Color.getRed(color)).to.equal(0x59) 10 | expect(Color.getGreen(color)).to.equal(0x9e) 11 | expect(Color.getBlue(color)).to.equal(0xff) 12 | expect(Color.getAlpha(color)).to.equal(0x80) 13 | }); 14 | 15 | it('can set channels', () => { 16 | const color = Color.parse('#ffffff') 17 | expect(Color.setRed(color, 0)).to.equal(c(0x00ffffff)) 18 | expect(Color.setGreen(color, 0)).to.equal(c(0xff00ffff)) 19 | expect(Color.setBlue(color, 0)).to.equal(c(0xffff00ff)) 20 | expect(Color.setAlpha(color, 0)).to.equal(c(0xffffff00)) 21 | }); 22 | 23 | describe('.parse():', () => { 24 | it('parses CSS hexadecimal', () => { 25 | expect(Color.parse('#59f')).to.equal(c(0x5599ffff)); 26 | expect(Color.parse('#5599ff')).to.equal(c(0x5599ffff)); 27 | expect(Color.parse('#5599ffff')).to.equal(c(0x5599ffff)); 28 | }); 29 | 30 | it('parses CSS color spaces', () => { 31 | ['rgb', 'rgba'].forEach(type => { 32 | expect(Color.parse(`${type}(255 153 85)`)).to.equal(c(0xff9955ff)); 33 | expect(Color.parse(`${type}(255, 153, 85)`)).to.equal(c(0xff9955ff)); 34 | expect(Color.parse(`${type}(255 153 85 / 50%)`)).to.equal(c(0xff995580)); 35 | expect(Color.parse(`${type}(255 153 85 / .5)`)).to.equal(c(0xff995580)); 36 | expect(Color.parse(`${type}(255 153 85 / 0.5)`)).to.equal(c(0xff995580)); 37 | }); 38 | 39 | ['hsl', 'hsla'].forEach(type => { 40 | expect(Color.parse(`${type}(50deg 80% 40% / 50%)`)).to.equal(c(0xb89c1480)); 41 | expect(Color.parse(`${type}(50deg 80% 40% / 0.5)`)).to.equal(c(0xb89c1480)); 42 | expect(Color.parse(`${type}(0 80% 40% / 0.5)`)).to.equal(c(0xb8141480)); 43 | expect(Color.parse(`${type}(none 80% 40% / 0.5)`)).to.equal(c(0xb8141480)); 44 | expect(Color.parse(`${type}(1turn 80% 40% / 0.5)`)).to.equal(c(0xb8141480)); 45 | expect(Color.parse(`${type}(400grad 80% 40% / 0.5)`)).to.equal(c(0xb8141480)); 46 | expect(Color.parse(`${type}(0rad 80% 40% / 0.5)`)).to.equal(c(0xb8141480)); 47 | }); 48 | 49 | expect(Color.parse('hwb(12 50% 0%)')).to.equal(c(0xff9980ff)); 50 | expect(Color.parse('hwb(50deg 30% 40%)')).to.equal(c(0x998c4dff)); 51 | expect(Color.parse('hwb(0.5turn 10% 0% / .5)')).to.equal(c(0x1affff80)); 52 | 53 | expect(Color.parse('color(srgb 1 0.5 0 / 0.5)')).to.equal(c(0xff800080)) 54 | expect(Color.parse('color(srgb-linear 1 0.5 0 / 0.5)')).to.equal(c(0xffbc0080)) 55 | expect(Color.parse('color(display-p3 1 0.5 0 / 0.5)')).to.equal(c(0xff760080)) 56 | expect(Color.parse('color(a98-rgb 1 0.5 0 / 0.5)')).to.equal(c(0xff810080)) 57 | expect(Color.parse('color(prophoto-rgb 1 0.5 0 / 0.5)')).to.equal(c(0xff630080)) 58 | expect(Color.parse('color(rec2020 1 0.5 0 / 0.5)')).to.equal(c(0xff720080)) 59 | }); 60 | 61 | it('parses lab()', () => { 62 | expect(Color.format(Color.parse('lab(50% 40 59.5 / 0.5)'))).to.equal(Color.format(c(0xbf570080))) 63 | }); 64 | 65 | it('parses lch()', () => { 66 | expect(Color.format(Color.parse('lch(52.2% 72.2 50 / 0.5)'))).to.equal(Color.format(c(0xcd561a80))) 67 | }); 68 | 69 | it('parses oklab()', () => { 70 | expect(Color.format(Color.parse('oklab(40.1% 0.1143 0.045)'))).to.equal(Color.format(c(0x7d2429ff))) 71 | }); 72 | 73 | it('parses oklch()', () => { 74 | expect(Color.format(Color.parse('oklch(40.1% 0.123 21.57)'))).to.equal(Color.format(c(0x7d2429ff))) 75 | }); 76 | 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | // 5 | // Source: https://github.com/ChromeDevTools/devtools-frontend/blob/c51201e6ee70370f7f1ac8a1a49dca7d4561aeaa/front_end/core/common/ColorConverter.ts 6 | // License: https://github.com/ChromeDevTools/devtools-frontend/blob/c51201e6ee70370f7f1ac8a1a49dca7d4561aeaa/LICENSE 7 | 8 | /** 9 | * Implementation of this module and all the tests are heavily influenced by 10 | * https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/color_conversions.cc 11 | */ 12 | 13 | // https://en.wikipedia.org/wiki/CIELAB_color_space#Converting_between_CIELAB_and_CIEXYZ_coordinates 14 | const D50_X = 0.9642; 15 | const D50_Y = 1.0; 16 | const D50_Z = 0.8251; 17 | 18 | type Matrix3x3 = [ 19 | [number, number, number], 20 | [number, number, number], 21 | [number, number, number], 22 | ]; 23 | 24 | type Vector3 = [number, number, number]; 25 | 26 | function multiply(matrix: Matrix3x3, other: Vector3): Vector3 { 27 | const dst = [0, 0, 0] as Vector3; 28 | for (let row = 0; row < 3; ++row) { 29 | dst[row] = matrix[row][0] * other[0] + matrix[row][1] * other[1] + 30 | matrix[row][2] * other[2]; 31 | } 32 | return dst; 33 | } 34 | 35 | // A transfer function mapping encoded values to linear values, 36 | // represented by this 7-parameter piecewise function: 37 | // 38 | // linear = sign(encoded) * (c*|encoded| + f) , 0 <= |encoded| < d 39 | // = sign(encoded) * ((a*|encoded| + b)^g + e), d <= |encoded| 40 | // 41 | // (A simple gamma transfer function sets g to gamma and a to 1.) 42 | class TransferFunction { 43 | g: number; 44 | a: number; 45 | b: number; 46 | c: number; 47 | d: number; 48 | e: number; 49 | f: number; 50 | 51 | constructor(g: number, a: number, b: number = 0, c: number = 0, d: number = 0, e: number = 0, f: number = 0) { 52 | this.g = g; 53 | this.a = a; 54 | this.b = b; 55 | this.c = c; 56 | this.d = d; 57 | this.e = e; 58 | this.f = f; 59 | } 60 | 61 | eval(val: number): number { 62 | const sign = val < 0 ? -1.0 : 1.0; 63 | const abs = val * sign; 64 | 65 | // 0 <= |encoded| < d path 66 | if (abs < this.d) { 67 | return sign * (this.c * abs + this.f); 68 | } 69 | 70 | // d <= |encoded| path 71 | return sign * (Math.pow(this.a * abs + this.b, this.g) + this.e); 72 | } 73 | } 74 | 75 | const NAMED_TRANSFER_FN = { 76 | sRGB: new TransferFunction(2.4, (1 / 1.055), (0.055 / 1.055), (1 / 12.92), 0.04045, 0.0, 0.0), 77 | sRGB_INVERSE: new TransferFunction(0.416667, 1.13728, -0, 12.92, 0.0031308, -0.0549698, -0), 78 | 79 | proPhotoRGB: new TransferFunction(1.8, 1), 80 | proPhotoRGB_INVERSE: new TransferFunction(0.555556, 1, -0, 0, 0, 0, 0), 81 | 82 | k2Dot2: new TransferFunction(2.2, 1.0), 83 | k2Dot2_INVERSE: new TransferFunction(0.454545, 1), 84 | 85 | rec2020: new TransferFunction(2.22222, 0.909672, 0.0903276, 0.222222, 0.0812429, 0, 0), 86 | rec2020_INVERSE: new TransferFunction(0.45, 1.23439, -0, 4.5, 0.018054, -0.0993195, -0), 87 | }; 88 | 89 | const NAMED_GAMUTS = { 90 | sRGB: [ 91 | [0.436065674, 0.385147095, 0.143066406], 92 | [0.222488403, 0.716873169, 0.060607910], 93 | [0.013916016, 0.097076416, 0.714096069], 94 | ] as Matrix3x3, 95 | sRGB_INVERSE: [ 96 | [3.134112151374599, -1.6173924597114966, -0.4906334036481285], 97 | [-0.9787872938826594, 1.9162795854799963, 0.0334547139520088], 98 | [0.07198304248352326, -0.2289858493321844, 1.4053851325241447], 99 | ] as Matrix3x3, 100 | displayP3: [ 101 | [0.515102, 0.291965, 0.157153], 102 | [0.241182, 0.692236, 0.0665819], 103 | [-0.00104941, 0.0418818, 0.784378], 104 | ] as Matrix3x3, 105 | displayP3_INVERSE: [ 106 | [2.404045155982687, -0.9898986932663839, -0.3976317191366333], 107 | [-0.8422283799266768, 1.7988505115115485, 0.016048170293157416], 108 | [0.04818705979712955, -0.09737385156228891, 1.2735066448052303], 109 | ] as Matrix3x3, 110 | adobeRGB: [ 111 | [0.60974, 0.20528, 0.14919], 112 | [0.31111, 0.62567, 0.06322], 113 | [0.01947, 0.06087, 0.74457], 114 | ] as Matrix3x3, 115 | adobeRGB_INVERSE: [ 116 | [1.9625385510109137, -0.6106892546501431, -0.3413827467482388], 117 | [-0.9787580455521, 1.9161624707082339, 0.03341676594241408], 118 | [0.028696263137883395, -0.1406807819331586, 1.349252109991369], 119 | ] as Matrix3x3, 120 | rec2020: [ 121 | [0.673459, 0.165661, 0.125100], 122 | [0.279033, 0.675338, 0.0456288], 123 | [-0.00193139, 0.0299794, 0.797162], 124 | ] as Matrix3x3, 125 | rec2020_INVERSE: [ 126 | [1.647275201661012, -0.3936024771460771, -0.23598028884792507], 127 | [-0.6826176165196962, 1.647617775014935, 0.01281626807852422], 128 | [0.029662725298529837, -0.06291668721366285, 1.2533964313435522], 129 | ] as Matrix3x3, 130 | xyz: [ 131 | [1.0, 0.0, 0.0], 132 | [0.0, 1.0, 0.0], 133 | [0.0, 0.0, 1.0], 134 | ] as Matrix3x3, 135 | 136 | }; 137 | 138 | function degToRad(deg: number): number { 139 | return deg * (Math.PI / 180); 140 | } 141 | 142 | function radToDeg(rad: number): number { 143 | return rad * (180 / Math.PI); 144 | } 145 | 146 | function applyTransferFns(fn: TransferFunction, r: number, g: number, b: number): [number, number, number] { 147 | return [fn.eval(r), fn.eval(g), fn.eval(b)]; 148 | } 149 | 150 | const OKLAB_TO_LMS_MATRIX = [ 151 | [0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339], 152 | [1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402], 153 | [1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399], 154 | ] as Matrix3x3; 155 | 156 | // Inverse of the OKLAB_TO_LMS_MATRIX 157 | const LMS_TO_OKLAB_MATRIX = [ 158 | [0.2104542553, 0.7936177849999999, -0.0040720468], 159 | [1.9779984951000003, -2.4285922049999997, 0.4505937099000001], 160 | [0.025904037099999982, 0.7827717662, -0.8086757660000001], 161 | ] as Matrix3x3; 162 | 163 | const XYZ_TO_LMS_MATRIX = [ 164 | [0.8190224432164319, 0.3619062562801221, -0.12887378261216414], 165 | [0.0329836671980271, 0.9292868468965546, 0.03614466816999844], 166 | [0.048177199566046255, 0.26423952494422764, 0.6335478258136937], 167 | ] as Matrix3x3; 168 | // Inverse of XYZ_TO_LMS_MATRIX 169 | const LMS_TO_XYZ_MATRIX = [ 170 | [1.226879873374156, -0.5578149965554814, 0.2813910501772159], 171 | [-0.040575762624313734, 1.1122868293970596, -0.07171106666151703], 172 | [-0.07637294974672144, -0.4214933239627915, 1.586924024427242], 173 | ] as Matrix3x3; 174 | 175 | const PRO_PHOTO_TO_XYZD50_MATRIX = [ 176 | [0.7976700747153241, 0.13519395152800417, 0.03135596341127167], 177 | [0.28803902352472205, 0.7118744007923554, 0.00008661179538844252], 178 | [2.739876695467402e-7, -0.0000014405226518969991, 0.825211112593861], 179 | ] as Matrix3x3; 180 | // Inverse of PRO_PHOTO_TO_XYZD50_MATRIX 181 | const XYZD50_TO_PRO_PHOTO_MATRIX = [ 182 | [1.3459533710138858, -0.25561367037652133, -0.051116041522131374], 183 | [-0.544600415668951, 1.5081687311475767, 0.020535163968720935], 184 | [-0.0000013975622054109725, 0.000002717590904589903, 1.2118111696814942], 185 | ] as Matrix3x3; 186 | 187 | const XYZD65_TO_XYZD50_MATRIX = [ 188 | [1.0478573189120088, 0.022907374491829943, -0.050162247377152525], 189 | [0.029570500050499514, 0.9904755577034089, -0.017061518194840468], 190 | [-0.00924047197558879, 0.015052921526981566, 0.7519708530777581], 191 | ] as Matrix3x3; 192 | // Inverse of XYZD65_TO_XYZD50_MATRIX 193 | const XYZD50_TO_XYZD65_MATRIX = [ 194 | [0.9555366447632887, -0.02306009252137888, 0.06321844147263304], 195 | [-0.028315378228764922, 1.009951351591575, 0.021026001591792402], 196 | [0.012308773293784308, -0.02050053471777469, 1.3301947294775631], 197 | ] as Matrix3x3; 198 | 199 | const XYZD65_TO_SRGB_MATRIX = [ 200 | [3.2408089365140573, -1.5375788839307314, -0.4985609572551541], 201 | [-0.9692732213205414, 1.876110235238969, 0.041560501141251774], 202 | [0.05567030990267439, -0.2040007921971802, 1.0571046720577026], 203 | ] as Matrix3x3; 204 | 205 | export function labToXyzd50(l: number, a: number, b: number): [number, number, number] { 206 | let y = (l + 16.0) / 116.0; 207 | let x = y + a / 500.0; 208 | let z = y - b / 200.0; 209 | 210 | function labInverseTransferFunction(t: number): number { 211 | const delta = (24.0 / 116.0); 212 | 213 | if (t <= delta) { 214 | return (108.0 / 841.0) * (t - (16.0 / 116.0)); 215 | } 216 | 217 | return t * t * t; 218 | } 219 | 220 | x = labInverseTransferFunction(x) * D50_X; 221 | y = labInverseTransferFunction(y) * D50_Y; 222 | z = labInverseTransferFunction(z) * D50_Z; 223 | 224 | return [x, y, z]; 225 | } 226 | 227 | export function xyzd50ToLab(x: number, y: number, z: number): [number, number, number] { 228 | function labTransferFunction(t: number): number { 229 | const deltaLimit: number = (24.0 / 116.0) * (24.0 / 116.0) * (24.0 / 116.0); 230 | 231 | if (t <= deltaLimit) { 232 | return (841.0 / 108.0) * t + (16.0 / 116.0); 233 | } 234 | return Math.pow(t, 1.0 / 3.0); 235 | } 236 | 237 | x = labTransferFunction(x / D50_X); 238 | y = labTransferFunction(y / D50_Y); 239 | z = labTransferFunction(z / D50_Z); 240 | 241 | const l = 116.0 * y - 16.0; 242 | const a = 500.0 * (x - y); 243 | const b = 200.0 * (y - z); 244 | 245 | return [l, a, b]; 246 | } 247 | 248 | export function oklabToXyzd65(l: number, a: number, b: number): [number, number, number] { 249 | const labInput = [l, a, b] as Vector3; 250 | const lmsIntermediate = multiply(OKLAB_TO_LMS_MATRIX, labInput); 251 | lmsIntermediate[0] = lmsIntermediate[0] * lmsIntermediate[0] * lmsIntermediate[0]; 252 | lmsIntermediate[1] = lmsIntermediate[1] * lmsIntermediate[1] * lmsIntermediate[1]; 253 | lmsIntermediate[2] = lmsIntermediate[2] * lmsIntermediate[2] * lmsIntermediate[2]; 254 | const xyzOutput = multiply(LMS_TO_XYZ_MATRIX, lmsIntermediate); 255 | return xyzOutput; 256 | } 257 | 258 | export function xyzd65ToOklab(x: number, y: number, z: number): [number, number, number] { 259 | const xyzInput = [x, y, z] as Vector3; 260 | const lmsIntermediate = multiply(XYZ_TO_LMS_MATRIX, xyzInput); 261 | 262 | lmsIntermediate[0] = Math.pow(lmsIntermediate[0], 1.0 / 3.0); 263 | lmsIntermediate[1] = Math.pow(lmsIntermediate[1], 1.0 / 3.0); 264 | lmsIntermediate[2] = Math.pow(lmsIntermediate[2], 1.0 / 3.0); 265 | 266 | const labOutput = multiply(LMS_TO_OKLAB_MATRIX, lmsIntermediate); 267 | return [labOutput[0], labOutput[1], labOutput[2]]; 268 | } 269 | 270 | export function lchToLab(l: number, c: number, h: number|undefined): [number, number, number] { 271 | if (h === undefined) { 272 | return [l, 0, 0]; 273 | } 274 | 275 | return [l, c * Math.cos(degToRad(h)), c * Math.sin(degToRad(h))]; 276 | } 277 | 278 | export function labToLch(l: number, a: number, b: number): [number, number, number] { 279 | return [l, Math.sqrt(a * a + b * b), radToDeg(Math.atan2(b, a))]; 280 | } 281 | 282 | export function displayP3ToXyzd50(r: number, g: number, b: number): [number, number, number] { 283 | const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.sRGB, r, g, b); 284 | const rgbInput = [mappedR, mappedG, mappedB] as Vector3; 285 | const xyzOutput = multiply(NAMED_GAMUTS.displayP3, rgbInput); 286 | return xyzOutput; 287 | } 288 | 289 | export function xyzd50ToDisplayP3(x: number, y: number, z: number): [number, number, number] { 290 | const xyzInput = [x, y, z] as Vector3; 291 | const rgbOutput = multiply(NAMED_GAMUTS.displayP3_INVERSE, xyzInput); 292 | return applyTransferFns( 293 | NAMED_TRANSFER_FN.sRGB_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]); 294 | } 295 | 296 | export function proPhotoToXyzd50(r: number, g: number, b: number): [number, number, number] { 297 | const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.proPhotoRGB, r, g, b); 298 | const rgbInput = [mappedR, mappedG, mappedB] as Vector3; 299 | const xyzOutput = multiply(PRO_PHOTO_TO_XYZD50_MATRIX, rgbInput); 300 | return xyzOutput; 301 | } 302 | 303 | export function xyzd50ToProPhoto(x: number, y: number, z: number): [number, number, number] { 304 | const xyzInput = [x, y, z] as Vector3; 305 | const rgbOutput = multiply(XYZD50_TO_PRO_PHOTO_MATRIX, xyzInput); 306 | return applyTransferFns( 307 | NAMED_TRANSFER_FN.proPhotoRGB_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]); 308 | } 309 | 310 | export function adobeRGBToXyzd50(r: number, g: number, b: number): [number, number, number] { 311 | const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.k2Dot2, r, g, b); 312 | const rgbInput = [mappedR, mappedG, mappedB] as Vector3; 313 | const xyzOutput = multiply(NAMED_GAMUTS.adobeRGB, rgbInput); 314 | return xyzOutput; 315 | } 316 | 317 | export function xyzd50ToAdobeRGB(x: number, y: number, z: number): [number, number, number] { 318 | const xyzInput = [x, y, z] as Vector3; 319 | const rgbOutput = multiply(NAMED_GAMUTS.adobeRGB_INVERSE, xyzInput); 320 | return applyTransferFns( 321 | NAMED_TRANSFER_FN.k2Dot2_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]); 322 | } 323 | 324 | export function rec2020ToXyzd50(r: number, g: number, b: number): [number, number, number] { 325 | const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.rec2020, r, g, b); 326 | const rgbInput = [mappedR, mappedG, mappedB] as Vector3; 327 | const xyzOutput = multiply(NAMED_GAMUTS.rec2020, rgbInput); 328 | return xyzOutput; 329 | } 330 | 331 | export function xyzd50ToRec2020(x: number, y: number, z: number): [number, number, number] { 332 | const xyzInput = [x, y, z] as Vector3; 333 | const rgbOutput = multiply(NAMED_GAMUTS.rec2020_INVERSE, xyzInput); 334 | return applyTransferFns( 335 | NAMED_TRANSFER_FN.rec2020_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]); 336 | } 337 | 338 | export function xyzd50ToD65(x: number, y: number, z: number): [number, number, number] { 339 | const xyzInput = [x, y, z] as Vector3; 340 | const xyzOutput = multiply(XYZD50_TO_XYZD65_MATRIX, xyzInput); 341 | return xyzOutput; 342 | } 343 | 344 | export function xyzd65ToD50(x: number, y: number, z: number): [number, number, number] { 345 | const xyzInput = [x, y, z] as Vector3; 346 | const xyzOutput = multiply(XYZD65_TO_XYZD50_MATRIX, xyzInput); 347 | return xyzOutput; 348 | } 349 | 350 | export function xyzd65TosRGBLinear(x: number, y: number, z: number): [number, number, number] { 351 | const xyzInput = [x, y, z] as Vector3; 352 | const rgbResult = multiply(XYZD65_TO_SRGB_MATRIX, xyzInput); 353 | return rgbResult; 354 | } 355 | 356 | export function xyzd50TosRGBLinear(x: number, y: number, z: number): [number, number, number] { 357 | const xyzInput = [x, y, z] as Vector3; 358 | const rgbResult = multiply(NAMED_GAMUTS.sRGB_INVERSE, xyzInput); 359 | return rgbResult; 360 | } 361 | 362 | export function srgbLinearToXyzd50(r: number, g: number, b: number): [number, number, number] { 363 | const rgbInput = [r, g, b] as Vector3; 364 | const xyzOutput = multiply(NAMED_GAMUTS.sRGB, rgbInput); 365 | return xyzOutput; 366 | } 367 | 368 | export function srgbToXyzd50(r: number, g: number, b: number): [number, number, number] { 369 | const [mappedR, mappedG, mappedB] = applyTransferFns(NAMED_TRANSFER_FN.sRGB, r, g, b); 370 | const rgbInput = [mappedR, mappedG, mappedB] as Vector3; 371 | const xyzOutput = multiply(NAMED_GAMUTS.sRGB, rgbInput); 372 | return xyzOutput; 373 | } 374 | 375 | export function xyzd50ToSrgb(x: number, y: number, z: number): [number, number, number] { 376 | const xyzInput = [x, y, z] as Vector3; 377 | const rgbOutput = multiply(NAMED_GAMUTS.sRGB_INVERSE, xyzInput); 378 | return applyTransferFns( 379 | NAMED_TRANSFER_FN.sRGB_INVERSE, rgbOutput[0], rgbOutput[1], rgbOutput[2]); 380 | } 381 | 382 | export function oklchToXyzd50(lInput: number, c: number, h: number): [number, number, number] { 383 | const [l, a, b] = lchToLab(lInput, c, h); 384 | const [x65, y65, z65] = oklabToXyzd65(l, a, b); 385 | return xyzd65ToD50(x65, y65, z65); 386 | } 387 | 388 | export function xyzd50ToOklch(x: number, y: number, z: number): [number, number, number] { 389 | const [x65, y65, z65] = xyzd50ToD65(x, y, z); 390 | const [l, a, b] = xyzd65ToOklab(x65, y65, z65); 391 | return labToLch(l, a, b); 392 | } 393 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import * as bit from './bit' 2 | 3 | const { cast, get, set } = bit 4 | 5 | export type Color = number; 6 | 7 | export const OFFSET_R = 24; 8 | export const OFFSET_G = 16; 9 | export const OFFSET_B = 8; 10 | export const OFFSET_A = 0; 11 | 12 | /** 13 | * Creates a new color from the given RGBA components. 14 | * Every component should be in the [0, 255] range. 15 | */ 16 | export function newColor(r: number, g: number, b: number, a: number) { 17 | return ( 18 | (r << OFFSET_R) + 19 | (g << OFFSET_G) + 20 | (b << OFFSET_B) + 21 | (a << OFFSET_A) 22 | ); 23 | } 24 | 25 | /** 26 | * Creates a new color from the given number value, e.g. 0x599eff. 27 | */ 28 | export function from(color: number) { 29 | return newColor( 30 | get(color, OFFSET_R), 31 | get(color, OFFSET_G), 32 | get(color, OFFSET_B), 33 | get(color, OFFSET_A), 34 | ); 35 | } 36 | 37 | /** 38 | * Turns the color into its equivalent number representation. 39 | * This is essentially a cast from int32 to uint32. 40 | */ 41 | export function toNumber(color: Color) { 42 | return cast(color); 43 | } 44 | 45 | export function getRed(c: Color) { return get(c, OFFSET_R); } 46 | export function getGreen(c: Color) { return get(c, OFFSET_G); } 47 | export function getBlue(c: Color) { return get(c, OFFSET_B); } 48 | export function getAlpha(c: Color) { return get(c, OFFSET_A); } 49 | 50 | export function setRed(c: Color, value: number) { return set(c, OFFSET_R, value); } 51 | export function setGreen(c: Color, value: number) { return set(c, OFFSET_G, value); } 52 | export function setBlue(c: Color, value: number) { return set(c, OFFSET_B, value); } 53 | export function setAlpha(c: Color, value: number) { return set(c, OFFSET_A, value); } 54 | -------------------------------------------------------------------------------- /src/format.ts: -------------------------------------------------------------------------------- 1 | import type { Color } from './core'; 2 | import * as core from './core' 3 | 4 | const { getRed, getGreen, getBlue, getAlpha } = core 5 | 6 | // Return buffer, avoid allocations 7 | const buffer = [0, 0, 0] 8 | 9 | /** 10 | * Map 8-bits value to its hexadecimal representation 11 | * ['00', '01', '02', ..., 'fe', 'ff'] 12 | */ 13 | const FORMAT_HEX = 14 | Array.from({ length: 256 }) 15 | .map((_, byte) => byte.toString(16).padStart(2, '0')) 16 | 17 | /** Format to a #RRGGBBAA string */ 18 | export const format = formatHEXA; 19 | 20 | /** Format to a #RRGGBBAA string */ 21 | export function formatHEXA(color: Color): string { 22 | return ( 23 | '#' + 24 | FORMAT_HEX[getRed(color)] + 25 | FORMAT_HEX[getGreen(color)] + 26 | FORMAT_HEX[getBlue(color)] + 27 | FORMAT_HEX[getAlpha(color)] 28 | ) 29 | } 30 | 31 | export function formatHEX(color: Color): string { 32 | return ( 33 | '#' + 34 | FORMAT_HEX[getRed(color)] + 35 | FORMAT_HEX[getGreen(color)] + 36 | FORMAT_HEX[getBlue(color)] 37 | ) 38 | } 39 | 40 | export function formatRGBA(color: Color) { 41 | return `rgba(${getRed(color)} ${getGreen(color)} ${getBlue(color)} / ${getAlpha(color) / 255})` 42 | } 43 | 44 | export function toRGBA(color: Color) { 45 | return { 46 | r: getRed(color), 47 | g: getGreen(color), 48 | b: getBlue(color), 49 | a: getAlpha(color), 50 | } 51 | } 52 | 53 | export function formatHSLA(color: Color) { 54 | rgbToHSL( 55 | getRed(color), 56 | getGreen(color), 57 | getBlue(color), 58 | ) 59 | const h = buffer[0] 60 | const s = buffer[1] 61 | const l = buffer[2] 62 | const a = getAlpha(color) / 255 63 | 64 | return `hsla(${h} ${s}% ${l}% / ${a})` 65 | } 66 | 67 | export function toHSLA(color: Color) { 68 | rgbToHSL( 69 | getRed(color), 70 | getGreen(color), 71 | getBlue(color), 72 | ) 73 | const h = buffer[0] 74 | const s = buffer[1] 75 | const l = buffer[2] 76 | const a = getAlpha(color) / 255 77 | 78 | return { h, s, l, a } 79 | } 80 | 81 | export function formatHWBA(color: Color) { 82 | rgbToHWB( 83 | getRed(color), 84 | getGreen(color), 85 | getBlue(color), 86 | ) 87 | const h = buffer[0] 88 | const w = buffer[1] 89 | const b = buffer[2] 90 | const a = getAlpha(color) / 255 91 | 92 | return `hsla(${h} ${w}% ${b}% / ${a})` 93 | } 94 | 95 | export function toHWBA(color: Color) { 96 | rgbToHWB( 97 | getRed(color), 98 | getGreen(color), 99 | getBlue(color), 100 | ) 101 | const h = buffer[0] 102 | const w = buffer[1] 103 | const b = buffer[2] 104 | const a = getAlpha(color) / 255 105 | 106 | return { h, w, b, a } 107 | } 108 | 109 | // Conversion functions 110 | 111 | // https://www.30secondsofcode.org/js/s/rgb-hex-hsl-hsb-color-format-conversion/ 112 | function rgbToHSL(r: number, g: number, b: number) { 113 | r /= 255; 114 | g /= 255; 115 | b /= 255; 116 | 117 | const l = Math.max(r, g, b); 118 | const s = l - Math.min(r, g, b); 119 | const h = s 120 | ? l === r 121 | ? (g - b) / s 122 | : l === g 123 | ? 2 + (b - r) / s 124 | : 4 + (r - g) / s 125 | : 0; 126 | 127 | buffer[0] = 60 * h < 0 ? 60 * h + 360 : 60 * h 128 | buffer[1] = 100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0) 129 | buffer[2] = (100 * (2 * l - s)) / 2 130 | } 131 | 132 | // https://stackoverflow.com/a/29463581/3112706 133 | function rgbToHWB(r: number, g: number, b: number) { 134 | r /= 255 135 | g /= 255 136 | b /= 255 137 | 138 | const w = Math.min(r, g, b) 139 | const v = Math.max(r, g, b) 140 | const black = 1 - v 141 | 142 | if (v === w) { 143 | buffer[0] = 0 144 | buffer[1] = w 145 | buffer[2] = black 146 | return 147 | } 148 | 149 | let f = r === w ? g - b : (g === w ? b - r : r - g); 150 | let i = r === w ? 3 : (g === w ? 5 : 1); 151 | 152 | buffer[0] = (i - f / (v - w)) / 6 153 | buffer[1] = w 154 | buffer[2] = black 155 | } 156 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import type { Color } from './core'; 2 | import * as core from './core' 3 | 4 | const { getRed, getGreen, getBlue, getAlpha, setAlpha, newColor } = core 5 | 6 | /** 7 | * Modifies color alpha channel. 8 | * @param color - Color 9 | * @param value - Value in the range [0, 1] 10 | */ 11 | export function alpha(color: Color, value: number): Color { 12 | return setAlpha(color, Math.round(value * 255)) 13 | } 14 | 15 | /** 16 | * Darkens a color. 17 | * @param color - Color 18 | * @param coefficient - Multiplier in the range [0, 1] 19 | */ 20 | export function darken(color: Color, coefficient: number): Color { 21 | const r = getRed(color) 22 | const g = getGreen(color) 23 | const b = getBlue(color) 24 | const a = getAlpha(color) 25 | 26 | const factor = 1 - coefficient 27 | 28 | return newColor( 29 | r * factor, 30 | g * factor, 31 | b * factor, 32 | a, 33 | ) 34 | } 35 | 36 | /** 37 | * Lighten a color. 38 | * @param color - Color 39 | * @param coefficient - Multiplier in the range [0, 1] 40 | */ 41 | export function lighten(color: Color, coefficient: number): Color { 42 | const r = getRed(color) 43 | const g = getGreen(color) 44 | const b = getBlue(color) 45 | const a = getAlpha(color) 46 | 47 | return newColor( 48 | r + (255 - r) * coefficient, 49 | g + (255 - g) * coefficient, 50 | b + (255 - b) * coefficient, 51 | a, 52 | ) 53 | } 54 | 55 | /** 56 | * Blend (aka mix) two colors together. 57 | * @param background The background color 58 | * @param overlay The overlay color that is affected by @opacity 59 | * @param opacity Opacity (alpha) for @overlay 60 | * @param [gamma=1.0] Gamma correction coefficient. `1.0` to match browser behavior, `2.2` for gamma-corrected blending. 61 | */ 62 | export function blend(background: Color, overlay: Color, opacity: number, gamma = 1.0) { 63 | const blendChannel = (b: number, o: number) => 64 | Math.round((b ** (1 / gamma) * (1 - opacity) + o ** (1 / gamma) * opacity) ** gamma) 65 | 66 | const r = blendChannel(getRed(background), getRed(overlay)) 67 | const g = blendChannel(getGreen(background), getGreen(overlay)) 68 | const b = blendChannel(getBlue(background), getBlue(overlay)) 69 | 70 | return newColor(r, g, b, 255) 71 | } 72 | 73 | /** 74 | * The relative brightness of any point in a color space, normalized to 0 for 75 | * darkest black and 1 for lightest white. 76 | * @returns The relative brightness of the color in the range 0 - 1, with 3 digits precision 77 | */ 78 | export function getLuminance(color: Color) { 79 | const r = getRed(color) / 255 80 | const g = getGreen(color) / 255 81 | const b = getBlue(color) / 255 82 | 83 | const apply = (v: number) => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4 84 | 85 | const r1 = apply(r) 86 | const g1 = apply(g) 87 | const b1 = apply(b) 88 | 89 | return Math.round((0.2126 * r1 + 0.7152 * g1 + 0.0722 * b1) * 1000) / 1000 90 | } 91 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './parse'; 3 | export * from './format'; 4 | export * from './functions'; 5 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import { Color, newColor } from './core'; 2 | import * as convert from './convert' 3 | 4 | const HASH = '#'.charCodeAt(0); 5 | const PERCENT = '%'.charCodeAt(0); 6 | const G = 'g'.charCodeAt(0); 7 | const N = 'n'.charCodeAt(0); 8 | const D = 'd'.charCodeAt(0); 9 | const E = 'e'.charCodeAt(0); 10 | 11 | /** 12 | * Approximative CSS colorspace string pattern, e.g. rgb(), color() 13 | */ 14 | const PATTERN = (() => { 15 | const NAME = '(\\w+)' 16 | const SEPARATOR = '[\\s,\\/]' 17 | const VALUE = '([^\\s,\\/]+)' 18 | const SEPARATOR_THEN_VALUE = `(?:${SEPARATOR}+${VALUE})` 19 | 20 | return new RegExp( 21 | `${NAME}\\( 22 | ${SEPARATOR}* 23 | ${VALUE} 24 | ${SEPARATOR_THEN_VALUE} 25 | ${SEPARATOR_THEN_VALUE} 26 | ${SEPARATOR_THEN_VALUE}? 27 | ${SEPARATOR_THEN_VALUE}? 28 | ${SEPARATOR}* 29 | \\)`.replace(/\s/g, '') 30 | ) 31 | })(); 32 | 33 | 34 | /** 35 | * Parse CSS color 36 | * @param color CSS color string: #xxx, #xxxxxx, #xxxxxxxx, rgb(), rgba(), hsl(), hsla(), color() 37 | */ 38 | export function parse(color: string): Color { 39 | if (color.charCodeAt(0) === HASH) { 40 | return parseHex(color); 41 | } else { 42 | return parseColor(color); 43 | } 44 | } 45 | 46 | /** 47 | * Parse hexadecimal CSS color 48 | * @param color Hex color string: #xxx, #xxxxxx, #xxxxxxxx 49 | */ 50 | export function parseHex(color: string): Color { 51 | let r = 0x00; 52 | let g = 0x00; 53 | let b = 0x00; 54 | let a = 0xff; 55 | 56 | switch (color.length) { 57 | // #59f 58 | case 4: { 59 | r = (hexValue(color.charCodeAt(1)) << 4) + hexValue(color.charCodeAt(1)); 60 | g = (hexValue(color.charCodeAt(2)) << 4) + hexValue(color.charCodeAt(2)); 61 | b = (hexValue(color.charCodeAt(3)) << 4) + hexValue(color.charCodeAt(3)); 62 | break; 63 | } 64 | // #5599ff 65 | case 7: { 66 | r = (hexValue(color.charCodeAt(1)) << 4) + hexValue(color.charCodeAt(2)); 67 | g = (hexValue(color.charCodeAt(3)) << 4) + hexValue(color.charCodeAt(4)); 68 | b = (hexValue(color.charCodeAt(5)) << 4) + hexValue(color.charCodeAt(6)); 69 | break; 70 | } 71 | // #5599ff88 72 | case 9: { 73 | r = (hexValue(color.charCodeAt(1)) << 4) + hexValue(color.charCodeAt(2)); 74 | g = (hexValue(color.charCodeAt(3)) << 4) + hexValue(color.charCodeAt(4)); 75 | b = (hexValue(color.charCodeAt(5)) << 4) + hexValue(color.charCodeAt(6)); 76 | a = (hexValue(color.charCodeAt(7)) << 4) + hexValue(color.charCodeAt(8)); 77 | break; 78 | } 79 | default: { 80 | break; 81 | } 82 | } 83 | 84 | return newColor(r, g, b, a) 85 | } 86 | 87 | // https://lemire.me/blog/2019/04/17/parsing-short-hexadecimal-strings-efficiently/ 88 | function hexValue(c: number) { 89 | return (c & 0xF) + 9 * (c >> 6) 90 | } 91 | 92 | 93 | /** 94 | * Parse CSS color 95 | * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value 96 | * @param color CSS color string: rgb(), rgba(), hsl(), hsla(), color() 97 | */ 98 | export function parseColor(color: string): Color { 99 | const match = PATTERN.exec(color); 100 | if (match === null) { 101 | throw new Error(`Color.parse(): invalid CSS color: "${color}"`); 102 | } 103 | 104 | const format = match[1]; 105 | const p1 = match[2]; 106 | const p2 = match[3]; 107 | const p3 = match[4]; 108 | const p4 = match[5]; 109 | const p5 = match[6]; 110 | 111 | switch (format) { 112 | case 'rgb': 113 | case 'rgba': { 114 | const r = parseColorChannel(p1); 115 | const g = parseColorChannel(p2); 116 | const b = parseColorChannel(p3); 117 | const a = p4 ? parseAlphaChannel(p4) : 255; 118 | 119 | return newColor(r, g, b, a); 120 | } 121 | case 'hsl': 122 | case 'hsla': { 123 | const h = parseAngle(p1); 124 | const s = parsePercentage(p2); 125 | const l = parsePercentage(p3); 126 | const a = p4 ? parseAlphaChannel(p4) : 255; 127 | 128 | // https://stackoverflow.com/a/9493060/3112706 129 | let r, g, b; 130 | if (s === 0) { 131 | r = g = b = Math.round(l * 255); // achromatic 132 | } else { 133 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 134 | const p = 2 * l - q; 135 | r = Math.round(hueToRGB(p, q, h + 1 / 3) * 255); 136 | g = Math.round(hueToRGB(p, q, h) * 255); 137 | b = Math.round(hueToRGB(p, q, h - 1 / 3) * 255); 138 | } 139 | 140 | return newColor(r, g, b, a); 141 | } 142 | case 'hwb': { 143 | const h = parseAngle(p1); 144 | const w = parsePercentage(p2); 145 | const bl = parsePercentage(p3); 146 | const a = p4 ? parseAlphaChannel(p4) : 255; 147 | 148 | /* https://drafts.csswg.org/css-color/#hwb-to-rgb */ 149 | const s = 1.0; 150 | const l = 0.5; 151 | 152 | // Same as HSL to RGB 153 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 154 | const p = 2 * l - q; 155 | let r = Math.round(hueToRGB(p, q, h + 1 / 3) * 255); 156 | let g = Math.round(hueToRGB(p, q, h) * 255); 157 | let b = Math.round(hueToRGB(p, q, h - 1 / 3) * 255); 158 | 159 | // Then HWB 160 | r = hwbApply(r, w, bl); 161 | g = hwbApply(g, w, bl); 162 | b = hwbApply(b, w, bl); 163 | 164 | return newColor(r, g, b, a); 165 | } 166 | case 'lab': { 167 | const l = parsePercentageFor(p1, 100); 168 | const aa = parsePercentageFor(p2, 125); 169 | const b = parsePercentageFor(p3, 125); 170 | const a = p4 ? parseAlphaChannel(p4) : 255; 171 | return newColorFromArray(a, 172 | convert.xyzd50ToSrgb(...convert.labToXyzd50(l, aa, b)) 173 | ) 174 | } 175 | case 'lch': { 176 | const l = parsePercentageFor(p1, 100); 177 | const c = parsePercentageFor(p2, 150); 178 | const h = parseAngle(p3) * 360; 179 | const a = p4 ? parseAlphaChannel(p4) : 255; 180 | return newColorFromArray(a, 181 | convert.xyzd50ToSrgb(...convert.labToXyzd50(...convert.lchToLab(l, c, h))) 182 | ) 183 | } 184 | case 'oklab': { 185 | const l = parsePercentageFor(p1, 1); 186 | const aa = parsePercentageFor(p2, 0.4); 187 | const b = parsePercentageFor(p3, 0.4); 188 | const a = p4 ? parseAlphaChannel(p4) : 255; 189 | return newColorFromArray(a, 190 | convert.xyzd50ToSrgb(...convert.xyzd65ToD50(...convert.oklabToXyzd65(l, aa, b))) 191 | ) 192 | } 193 | case 'oklch': { 194 | const l = parsePercentageOrValue(p1); 195 | const c = parsePercentageOrValue(p2); 196 | const h = parsePercentageOrValue(p3); 197 | const a = p4 ? parseAlphaChannel(p4) : 255; 198 | return newColorFromArray(a, 199 | convert.xyzd50ToSrgb(...convert.oklchToXyzd50(l, c, h)) 200 | ) 201 | } 202 | case 'color': { 203 | // https://drafts.csswg.org/css-color-4/#color-function 204 | 205 | const colorspace = p1; 206 | const c1 = parsePercentageOrValue(p2); 207 | const c2 = parsePercentageOrValue(p3); 208 | const c3 = parsePercentageOrValue(p4); 209 | const a = p5 ? parseAlphaChannel(p5) : 255; 210 | 211 | switch (colorspace) { 212 | // RGB color spaces 213 | case 'srgb': { 214 | return newColorFromArray(a, 215 | [c1, c2, c3] 216 | ) 217 | } 218 | case 'srgb-linear': { 219 | return newColorFromArray(a, 220 | convert.xyzd50ToSrgb(...convert.srgbLinearToXyzd50(c1, c2, c3)) 221 | ) 222 | } 223 | case 'display-p3': { 224 | return newColorFromArray(a, 225 | convert.xyzd50ToSrgb(...convert.displayP3ToXyzd50(c1, c2, c3)) 226 | ) 227 | } 228 | case 'a98-rgb': { 229 | return newColorFromArray(a, 230 | convert.xyzd50ToSrgb(...convert.adobeRGBToXyzd50(c1, c2, c3)) 231 | ) 232 | } 233 | case 'prophoto-rgb': { 234 | return newColorFromArray(a, 235 | convert.xyzd50ToSrgb(...convert.proPhotoToXyzd50(c1, c2, c3)) 236 | ) 237 | } 238 | case 'rec2020': { 239 | return newColorFromArray(a, 240 | convert.xyzd50ToSrgb(...convert.rec2020ToXyzd50(c1, c2, c3)) 241 | ) 242 | } 243 | // XYZ color spaces 244 | case 'xyz': 245 | case 'xyz-d65': { 246 | return newColorFromArray(a, 247 | convert.xyzd50ToSrgb(...convert.xyzd65ToD50(c1, c2, c3)) 248 | ) 249 | } 250 | case 'xyz-d50': { 251 | return newColorFromArray(a, 252 | convert.xyzd50ToSrgb(c1, c2, c3) 253 | ) 254 | } 255 | default: 256 | } 257 | } 258 | default: 259 | } 260 | throw new Error(`Color.parse(): invalid CSS color: "${color}"`); 261 | } 262 | 263 | /** 264 | * Accepts: "50%", "128" 265 | * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#values 266 | * @returns a value in the 0 to 255 range 267 | */ 268 | function parseColorChannel(channel: string): number { 269 | if (channel.charCodeAt(channel.length - 1) === PERCENT) { 270 | return Math.round((parseFloat(channel) / 100) * 255); 271 | } 272 | return Math.round(parseFloat(channel)); 273 | } 274 | 275 | /** 276 | * Accepts: "50%", ".5", "0.5" 277 | * https://developer.mozilla.org/en-US/docs/Web/CSS/alpha-value 278 | * @returns a value in the [0, 255] range 279 | */ 280 | function parseAlphaChannel(channel: string): number { 281 | return Math.round(parseAlphaValue(channel) * 255); 282 | } 283 | 284 | /** 285 | * Accepts: "50%", ".5", "0.5" 286 | * https://developer.mozilla.org/en-US/docs/Web/CSS/alpha-value 287 | * @returns a value in the [0, 1] range 288 | */ 289 | function parseAlphaValue(channel: string): number { 290 | if (channel.charCodeAt(0) === N) { 291 | return 0; 292 | } 293 | if (channel.charCodeAt(channel.length - 1) === PERCENT) { 294 | return parseFloat(channel) / 100; 295 | } 296 | return parseFloat(channel); 297 | } 298 | 299 | /** 300 | * Accepts: "360", "360deg", "400grad", "6.28rad", "1turn", "none" 301 | * https://developer.mozilla.org/en-US/docs/Web/CSS/angle 302 | * @returns a value in the 0.0 to 1.0 range 303 | */ 304 | function parseAngle(angle: string): number { 305 | let factor = 1; 306 | switch (angle.charCodeAt(angle.length - 1)) { 307 | case E: { 308 | // 'none' 309 | return 0; 310 | } 311 | case D: { 312 | // 'rad', 'grad' 313 | if (angle.charCodeAt(Math.max(0, angle.length - 4)) === G) { 314 | // 'grad' 315 | factor = 400; 316 | } else { 317 | // 'rad' 318 | factor = 2 * Math.PI; // TAU 319 | } 320 | break; 321 | } 322 | case N: { 323 | // 'turn' 324 | factor = 1; 325 | break; 326 | } 327 | // case G: // 'deg', but no need to check as it's also the default 328 | default: { 329 | factor = 360; 330 | } 331 | } 332 | return parseFloat(angle) / factor; 333 | } 334 | 335 | /** 336 | * Accepts: "100%", "none" 337 | * @returns a value in the 0.0 to 1.0 range 338 | */ 339 | function parsePercentage(value: string): number { 340 | if (value.charCodeAt(0) === N) { 341 | return 0; 342 | } 343 | return parseFloat(value) / 100; 344 | } 345 | 346 | /** 347 | * Accepts: "1.0", "100%", "none" 348 | * @returns a value in the 0.0 to 1.0 range 349 | */ 350 | function parsePercentageOrValue(value: string): number { 351 | if (value.charCodeAt(0) === N) { 352 | return 0; 353 | } 354 | if (value.charCodeAt(value.length - 1) === PERCENT) { 355 | return parseFloat(value) / 100; 356 | } 357 | return parseFloat(value); 358 | } 359 | 360 | /** 361 | * Accepts: "100", "100%", "none" 362 | * @returns a value in the -@range to @range range 363 | */ 364 | function parsePercentageFor(value: string, range: number): number { 365 | if (value.charCodeAt(0) === N) { 366 | return 0; 367 | } 368 | if (value.charCodeAt(value.length - 1) === PERCENT) { 369 | return parseFloat(value) / 100 * range; 370 | } 371 | return parseFloat(value); 372 | } 373 | 374 | 375 | // HSL functions 376 | 377 | function hueToRGB(p: number, q: number, t: number) { 378 | if (t < 0) { t += 1 }; 379 | if (t > 1) { t -= 1 }; 380 | if (t < 1 / 6) { return p + (q - p) * 6 * t }; 381 | if (t < 1 / 2) { return q }; 382 | if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6 }; 383 | { return p }; 384 | } 385 | 386 | // HWB functions 387 | 388 | function hwbApply(channel: number, w: number, b: number) { 389 | let result = channel / 255 390 | 391 | result *= 1 - w - b 392 | result += w 393 | 394 | return Math.round(result * 255) 395 | } 396 | 397 | 398 | function clamp(value: number) { 399 | return Math.max(0, Math.min(255, value)) 400 | } 401 | 402 | function newColorFromArray(a: number, rgb: [number, number, number]) { 403 | const r = clamp(Math.round(rgb[0] * 255)) 404 | const g = clamp(Math.round(rgb[1] * 255)) 405 | const b = clamp(Math.round(rgb[2] * 255)) 406 | return newColor(r, g, b, a) 407 | } 408 | -------------------------------------------------------------------------------- /src/string.ts: -------------------------------------------------------------------------------- 1 | import * as Color from './' 2 | 3 | const { 4 | format, 5 | parse, 6 | alpha: alphaBase, 7 | blend: blendBase, 8 | darken: darkenBase, 9 | lighten: lightenBase, 10 | getLuminance: getLuminanceBase, 11 | } = Color 12 | 13 | export function alpha(color: string, value: number) { return format(alphaBase(parse(color), value)) } 14 | export function blend(background: string, overlay: string, opacity: number, gamma: number) { return format(blendBase(parse(background), parse(overlay), opacity, gamma)) } 15 | export function darken(color: string, value: number) { return format(darkenBase(parse(color), value)) } 16 | export function lighten(color: string, value: number) { return format(lightenBase(parse(color), value)) } 17 | export function getLuminance(color: string) { return getLuminanceBase(parse(color)) } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "lib": ["ES2017"], 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "esModuleInterop": true, 9 | "preserveConstEnums": true, 10 | "outDir": "./build", 11 | "sourceMap": true, 12 | "declaration": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["**/*.test.ts"] 16 | } 17 | --------------------------------------------------------------------------------