diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2494eb2..6e98150 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,13 +10,12 @@ jobs: fail-fast: false matrix: node-version: - - 17 - - 16 - - 14 - - 12 + - 22 + - 20 + - 18 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/README.md b/README.md index 1581f67..cb5a157 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ console.log(color.ansi256().object()); // { ansi256: 183, alpha: 0.5 } ``` ## Install -```console -$ npm install color +```shell +npm install color ``` ## Usage ```js -const Color = require('color'); +import Color from 'color'; ``` ### Constructors @@ -95,17 +95,17 @@ String constructors are handled by [color-string](https://www.npmjs.com/package/ ### Getters ```js -color.hsl(); +color.hsl() ``` Convert a color to a different space (`hsl()`, `cmyk()`, etc.). ```js -color.object(); // {r: 255, g: 255, b: 255} +color.object() // {r: 255, g: 255, b: 255} ``` Get a hash of the color value. Reflects the color's current model (see above). ```js -color.rgb().array() // [255, 255, 255] +color.rgb().array() // [255, 255, 255] ``` Get an array of the values with `array()`. Reflects the color's current model (see above). @@ -120,31 +120,31 @@ color.hex() // #ffffff Get the hex value. (**NOTE:** `.hex()` does not return alpha values; use `.hexa()` for an RGBA representation) ```js -color.red() // 255 +color.red() // 255 ``` Get the value for an individual channel. ### CSS Strings ```js -color.hsl().string() // 'hsl(320, 50%, 100%)' +color.hsl().string() // 'hsl(320, 50%, 100%)' ``` Calling `.string()` with a number rounds the numbers to that decimal place. It defaults to 1. ### Luminosity ```js -color.luminosity(); // 0.412 +color.luminosity(); // 0.412 ``` The [WCAG luminosity](http://www.w3.org/TR/WCAG20/#relativeluminancedef) of the color. 0 is black, 1 is white. ```js -color.contrast(Color("blue")) // 12 +color.contrast(Color("blue")) // 12 ``` The [WCAG contrast ratio](http://www.w3.org/TR/WCAG20/#contrast-ratiodef) to another color, from 1 (same color) to 21 (contrast b/w white and black). ```js -color.isLight(); // true -color.isDark(); // false +color.isLight() // true +color.isDark() // false ``` Get whether the color is "light" or "dark", useful for deciding text color. @@ -166,7 +166,7 @@ color.grayscale() // #5CBF54 -> #969696 color.whiten(0.5) // hwb(100, 50%, 50%) -> hwb(100, 75%, 50%) color.blacken(0.5) // hwb(100, 50%, 50%) -> hwb(100, 50%, 75%) -color.fade(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 0.4) +color.fade(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 0.4) color.opaquer(0.5) // rgba(10, 10, 10, 0.8) -> rgba(10, 10, 10, 1.0) color.rotate(180) // hsl(60, 20%, 20%) -> hsl(240, 20%, 20%) diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..eb3554e --- /dev/null +++ b/index.d.ts @@ -0,0 +1,135 @@ +import type convert from 'color-convert'; + +export type ColorLike = ColorInstance | string | ArrayLike | number | Record; +export type ColorJson = {model: string; color: number[]; valpha: number}; +export type ColorObject = {alpha?: number | undefined} & Record; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export interface ColorInstance { + toString(): string; + // eslint-disable-next-line @typescript-eslint/naming-convention + toJSON(): ColorJson; + string(places?: number): string; + percentString(places?: number): string; + array(): number[]; + object(): ColorObject; + unitArray(): number[]; + unitObject(): {r: number; g: number; b: number; alpha?: number | undefined}; + round(places?: number): ColorInstance; + alpha(): number; + alpha(value: number): ColorInstance; + red(): number; + red(value: number): ColorInstance; + green(): number; + green(value: number): ColorInstance; + blue(): number; + blue(value: number): ColorInstance; + hue(): number; + hue(value: number): ColorInstance; + saturationl(): number; + saturationl(value: number): ColorInstance; + lightness(): number; + lightness(value: number): ColorInstance; + saturationv(): number; + saturationv(value: number): ColorInstance; + value(): number; + value(value: number): ColorInstance; + chroma(): number; + chroma(value: number): ColorInstance; + gray(): number; + gray(value: number): ColorInstance; + white(): number; + white(value: number): ColorInstance; + wblack(): number; + wblack(value: number): ColorInstance; + cyan(): number; + cyan(value: number): ColorInstance; + magenta(): number; + magenta(value: number): ColorInstance; + yellow(): number; + yellow(value: number): ColorInstance; + black(): number; + black(value: number): ColorInstance; + x(): number; + x(value: number): ColorInstance; + y(): number; + y(value: number): ColorInstance; + z(): number; + z(value: number): ColorInstance; + l(): number; + l(value: number): ColorInstance; + a(): number; + a(value: number): ColorInstance; + b(): number; + b(value: number): ColorInstance; + keyword(): string; + keyword(value: V): ColorInstance; + hex(): string; + hex(value: V): ColorInstance; + hexa(): string; + hexa(value: V): ColorInstance; + rgbNumber(): number; + luminosity(): number; + contrast(color2: ColorInstance): number; + level(color2: ColorInstance): 'AAA' | 'AA' | ''; + isDark(): boolean; + isLight(): boolean; + negate(): ColorInstance; + lighten(ratio: number): ColorInstance; + darken(ratio: number): ColorInstance; + saturate(ratio: number): ColorInstance; + desaturate(ratio: number): ColorInstance; + whiten(ratio: number): ColorInstance; + blacken(ratio: number): ColorInstance; + grayscale(): ColorInstance; + fade(ratio: number): ColorInstance; + opaquer(ratio: number): ColorInstance; + rotate(degrees: number): ColorInstance; + mix(mixinColor: ColorInstance, weight?: number): ColorInstance; + rgb(...arguments_: number[]): ColorInstance; + hsl(...arguments_: number[]): ColorInstance; + hsv(...arguments_: number[]): ColorInstance; + hwb(...arguments_: number[]): ColorInstance; + cmyk(...arguments_: number[]): ColorInstance; + xyz(...arguments_: number[]): ColorInstance; + lab(...arguments_: number[]): ColorInstance; + lch(...arguments_: number[]): ColorInstance; + ansi16(...arguments_: number[]): ColorInstance; + ansi256(...arguments_: number[]): ColorInstance; + hcg(...arguments_: number[]): ColorInstance; + apple(...arguments_: number[]): ColorInstance; +} + +export type ColorConstructor = { + (object?: ColorLike, model?: keyof (typeof convert)): ColorInstance; + new(object?: ColorLike, model?: keyof (typeof convert)): ColorInstance; + rgb(...value: number[]): ColorInstance; + rgb(color: ColorLike): ColorInstance; + hsl(...value: number[]): ColorInstance; + hsl(color: ColorLike): ColorInstance; + hsv(...value: number[]): ColorInstance; + hsv(color: ColorLike): ColorInstance; + hwb(...value: number[]): ColorInstance; + hwb(color: ColorLike): ColorInstance; + cmyk(...value: number[]): ColorInstance; + cmyk(color: ColorLike): ColorInstance; + xyz(...value: number[]): ColorInstance; + xyz(color: ColorLike): ColorInstance; + lab(...value: number[]): ColorInstance; + lab(color: ColorLike): ColorInstance; + lch(...value: number[]): ColorInstance; + lch(color: ColorLike): ColorInstance; + ansi16(...value: number[]): ColorInstance; + ansi16(color: ColorLike): ColorInstance; + ansi256(...value: number[]): ColorInstance; + ansi256(color: ColorLike): ColorInstance; + hcg(...value: number[]): ColorInstance; + hcg(color: ColorLike): ColorInstance; + apple(...value: number[]): ColorInstance; + apple(color: ColorLike): ColorInstance; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +declare const Color: ColorConstructor; + +export default Color; diff --git a/index.js b/index.js index cb22fa7..5a3e268 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ -const colorString = require('color-string'); -const convert = require('color-convert'); +import colorString from 'color-string'; +import convert from 'color-convert'; const skippedModels = [ // To be honest, I don't really feel like keyword belongs in color convert, but eh. @@ -123,14 +123,14 @@ Color.prototype = { string(places) { let self = this.model in colorString.to ? this : this.rgb(); self = self.round(typeof places === 'number' ? places : 1); - const args = self.valpha === 1 ? self.color : [...self.color, this.valpha]; - return colorString.to[self.model](args); + const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha]; + return colorString.to[self.model](...arguments_); }, percentString(places) { const self = this.rgb().round(typeof places === 'number' ? places : 1); - const args = self.valpha === 1 ? self.color : [...self.color, this.valpha]; - return colorString.to.rgb.percent(args); + const arguments_ = self.valpha === 1 ? self.color : [...self.color, this.valpha]; + return colorString.to.rgb.percent(...arguments_); }, array() { @@ -237,7 +237,7 @@ Color.prototype = { return new Color(value); } - return colorString.to.hex(this.rgb().round().color); + return colorString.to.hex(...this.rgb().round().color); }, hexa(value) { @@ -252,7 +252,7 @@ Color.prototype = { alphaHex = '0' + alphaHex; } - return colorString.to.hex(rgbArray) + alphaHex; + return colorString.to.hex(...rgbArray) + alphaHex; }, rgbNumber() { @@ -409,23 +409,23 @@ for (const model of Object.keys(convert)) { const {channels} = convert[model]; // Conversion methods - Color.prototype[model] = function (...args) { + Color.prototype[model] = function (...arguments_) { if (this.model === model) { return new Color(this); } - if (args.length > 0) { - return new Color(args, model); + if (arguments_.length > 0) { + return new Color(arguments_, model); } return new Color([...assertArray(convert[this.model][model].raw(this.color)), this.valpha], model); }; // 'static' construction methods - Color[model] = function (...args) { - let color = args[0]; + Color[model] = function (...arguments_) { + let color = arguments_[0]; if (typeof color === 'number') { - color = zeroArray(args, channels); + color = zeroArray(arguments_, channels); } return new Color(color, model); @@ -446,7 +446,7 @@ function getset(model, channel, modifier) { model = Array.isArray(model) ? model : [model]; for (const m of model) { - (limiters[m] || (limiters[m] = []))[channel] = modifier; + (limiters[m] ||= [])[channel] = modifier; } model = model[0]; @@ -493,4 +493,4 @@ function zeroArray(array, length) { return array; } -module.exports = Color; +export default Color; diff --git a/index.test-d.ts b/index.test-d.ts new file mode 100644 index 0000000..30e5cc1 --- /dev/null +++ b/index.test-d.ts @@ -0,0 +1,111 @@ +import {expectType} from 'tsd'; +import Color, { + type ColorInstance, type ColorJson, type ColorObject, +} from './index.js'; + +// String constructor +expectType(Color('rgb(255, 255, 255)')); +expectType(Color('hsl(194, 53%, 79%)')); +expectType(Color('hsl(194, 53%, 79%, 0.5)')); +expectType(Color('#FF0000')); +expectType(Color('#FF000033')); +expectType(Color('lightblue')); +expectType(Color('purple')); +// RGB +expectType(Color({r: 255, g: 255, b: 255})); +expectType(Color({ + r: 255, g: 255, b: 255, alpha: 0.5, +})); +expectType(Color.rgb(255, 255, 255)); +expectType(Color.rgb(255, 255, 255, 0.5)); +expectType(Color.rgb(0xFF, 0x00, 0x00, 0.5)); +expectType(Color.rgb([255, 255, 255])); +expectType(Color.rgb([0xFF, 0x00, 0x00, 0.5])); +// HSL +expectType(Color({h: 194, s: 53, l: 79})); +expectType(Color({ + h: 194, s: 53, l: 79, alpha: 0.5, +})); +expectType(Color.hsl(194, 53, 79)); +// HSV +expectType(Color({h: 195, s: 25, v: 99})); +expectType(Color({ + h: 195, s: 25, v: 99, alpha: 0.5, +})); +expectType(Color.hsv(195, 25, 99)); +expectType(Color.hsv([195, 25, 99])); +// CMYK +expectType(Color({ + c: 0, m: 100, y: 100, k: 0, +})); +expectType(Color({ + c: 0, m: 100, y: 100, k: 0, alpha: 0.5, +})); +expectType(Color.cmyk(0, 100, 100, 0)); +expectType(Color.cmyk(0, 100, 100, 0, 0.5)); +// Hwb +expectType(Color({h: 180, w: 0, b: 0})); +expectType(Color.hwb(180, 0, 0)); +// Lch +expectType(Color({l: 53, c: 105, h: 40})); +expectType(Color.lch(53, 105, 40)); +// Lab +expectType(Color({l: 53, a: 80, b: 67})); +expectType(Color.lab(53, 80, 67)); +// Hcg +expectType(Color({h: 0, c: 100, g: 0})); +expectType(Color.hcg(0, 100, 0)); +// Ansi16 +expectType(Color.ansi16(91)); +expectType(Color.ansi16(91, 0.5)); +// Ansi256 +expectType(Color.ansi256(196)); +expectType(Color.ansi256(196, 0.5)); +// Apple +expectType(Color.apple(65535, 65535, 65535)); +expectType(Color.apple([65535, 65535, 65535])); + +// Getters +const color = Color('#00ccff'); +expectType(color.hsl()); +expectType(color.toJSON()); +expectType(color.object()); +expectType(color.rgb().array()); +expectType(color.rgbNumber()); +expectType(color.hex()); +expectType(color.hex('#00ccff')); +expectType(color.red()); +expectType(color.red(255)); + +// CSS strings +expectType(color.hsl().string()); + +// Luminosity +expectType(color.luminosity()); +expectType(color.contrast(Color('blue'))); +expectType(color.isLight()); +expectType(color.isDark()); + +// Manipulation +expectType(color.negate()); +expectType(color.lighten(0.5)); +expectType(color.lighten(0.5)); +expectType(color.darken(0.5)); +expectType(color.darken(0.5)); +expectType(color.lightness(50)); +expectType(color.saturate(0.5)); +expectType(color.desaturate(0.5)); +expectType(color.grayscale()); +expectType(color.whiten(0.5)); +expectType(color.blacken(0.5)); +expectType(color.fade(0.5)); +expectType(color.opaquer(0.5)); +expectType(color.rotate(180)); +expectType(color.rotate(-90)); +expectType(color.mix(Color('yellow'))); +expectType(color.mix(Color('yellow'), 0.3)); + +// Chaining +expectType(color.green(100).grayscale().lighten(0.6)); +expectType(color.hsl().rgb().hex()); +expectType(color.hsl().rgb().gray()); diff --git a/package.json b/package.json index 4cdb6e3..e618cc5 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "color", "version": "4.2.3", "description": "Color conversion and manipulation with CSS string support", + "type": "module", + "exports": "./index.js", + "types": "./index.d.ts", "sideEffects": false, "keywords": [ "color", @@ -17,6 +20,7 @@ "repository": "Qix-/color", "xo": { "rules": { + "no-bitwise": 0, "no-cond-assign": 0, "new-cap": 0, "unicorn/prefer-module": 0, @@ -27,21 +31,22 @@ }, "files": [ "LICENSE", - "index.js" + "index.js", + "index.d.ts" ], "scripts": { - "pretest": "xo", - "test": "mocha" + "test": "xo && tsd && mocha" }, "engines": { - "node": ">=12.5.0" + "node": ">=18" }, "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" + "color-convert": "^3.0.1", + "color-string": "^2.0.0" }, "devDependencies": { - "mocha": "9.0.2", - "xo": "0.42.0" + "mocha": "11.1.0", + "tsd": "0.31.2", + "xo": "0.60.0" } } diff --git a/test/index.js b/test/index.js index c640fa4..a024574 100644 --- a/test/index.js +++ b/test/index.js @@ -1,7 +1,7 @@ /* eslint-env node, mocha */ -const assert = require('assert'); -const Color = require('..'); +import assert from 'node:assert'; +import Color from '../index.js'; const {deepEqual} = assert; const {equal} = assert; @@ -30,6 +30,106 @@ it('Immutability', () => { ok(c != c.rgb()); // eslint-disable-line eqeqeq }); +it('Colors to JSON', () => { + deepEqual(Color('#0A1E19').rgb().toJSON(), { + color: [10, 30, 25], + model: 'rgb', + valpha: 1, + }); + deepEqual(Color('rgb(10, 30, 25)').rgb().toJSON(), { + color: [10, 30, 25], + model: 'rgb', + valpha: 1, + }); + deepEqual(Color('rgba(10, 30, 25, 0.4)').rgb().toJSON(), { + color: [10, 30, 25], + model: 'rgb', + valpha: 0.4, + }); + deepEqual(Color('rgb(4%, 12%, 10%)').rgb().toJSON(), { + color: [10, 31, 26], + model: 'rgb', + valpha: 1, + }); + deepEqual(Color('rgba(4%, 12%, 10%, 0.4)').rgb().toJSON(), { + color: [10, 31, 26], + model: 'rgb', + valpha: 0.4, + }); + deepEqual(Color('blue').rgb().toJSON(), { + color: [0, 0, 255], + model: 'rgb', + valpha: 1, + }); + deepEqual(Color('hsl(120, 50%, 60%)').hsl().toJSON(), { + color: [120, 50, 60], + model: 'hsl', + valpha: 1, + }); + deepEqual(Color('hsla(120, 50%, 60%, 0.4)').hsl().toJSON(), { + color: [120, 50, 60], + model: 'hsl', + valpha: 0.4, + }); + deepEqual(Color('hwb(120, 50%, 60%)').hwb().toJSON(), { + color: [120, 50, 60], + model: 'hwb', + valpha: 1, + }); + deepEqual(Color('hwb(120, 50%, 60%, 0.4)').hwb().toJSON(), { + color: [120, 50, 60], + model: 'hwb', + valpha: 0.4, + }); + + deepEqual(Color({ + r: 10, + g: 30, + b: 25, + }).rgb().toJSON(), { + color: [10, 30, 25], + model: 'rgb', + valpha: 1, + }); + deepEqual(Color({ + h: 10, + s: 30, + l: 25, + }).hsl().toJSON(), { + color: [10, 30, 25], + model: 'hsl', + valpha: 1, + }); + deepEqual(Color({ + h: 10, + s: 30, + v: 25, + }).hsv().toJSON(), { + color: [10, 30, 25], + model: 'hsv', + valpha: 1, + }); + deepEqual(Color({ + h: 10, + w: 30, + b: 25, + }).hwb().toJSON(), { + color: [10, 30, 25], + model: 'hwb', + valpha: 1, + }); + deepEqual(Color({ + c: 10, + m: 30, + y: 25, + k: 10, + }).cmyk().toJSON(), { + color: [10, 30, 25, 10], + model: 'cmyk', + valpha: 1, + }); +}); + it('Color() argument', () => { deepEqual(Color('#0A1E19').rgb().object(), { r: 10,