diff --git a/__tests__/colorConversion.test.ts b/__tests__/colorConversion.test.ts new file mode 100644 index 0000000..6182dd7 --- /dev/null +++ b/__tests__/colorConversion.test.ts @@ -0,0 +1,540 @@ +import { describe, expect, test } from '@jest/globals'; +import colorKit from '../src/colorKit/index'; + +const colors = [ + { + hex: ['#fff', '#ffff', '#ffffff', '#ffffffff', 0xffffffff], + rgb: [ + 'rgb(255, 255, 255)', + 'rgb(255 255 255)', + 'rgba(255, 255, 255, 1.0)', + 'rgba(255 255 255 / 1.0)', + { r: 255, g: 255, b: 255 }, + { r: 255, g: 255, b: 255, a: 1 }, + ], + hsl: [ + 'hsl(0, 0%, 100%)', + 'hsl(0deg, 0%, 100%)', + 'hsl(0 0% 100%)', + 'hsl(0deg 0% 100%)', + 'hsla(0, 0%, 100%, 1.0)', + 'hsla(0deg, 0%, 100%, 1.0)', + 'hsla(0 0% 100% / 1.0)', + 'hsla(0deg 0% 100% / 1.0)', + { h: 0, s: 0, l: 100 }, + { h: 0, s: 0, l: 100, a: 1 }, + ], + hsv: [ + 'hsv(0, 0%, 100%)', + 'hsv(0deg, 0%, 100%)', + 'hsv(0 0% 100%)', + 'hsv(0deg 0% 100%)', + 'hsva(0, 0%, 100%, 1.0)', + 'hsva(0deg, 0%, 100%, 1.0)', + 'hsva(0 0% 100% / 1.0)', + 'hsva(0deg 0% 100% / 1.0)', + { h: 0, s: 0, v: 100 }, + { h: 0, s: 0, v: 100, a: 1 }, + ], + hwb: [ + 'hwb(0, 100%, 0%)', + 'hwb(0deg, 100%, 0%)', + 'hwb(0 100% 0%)', + 'hwb(0deg 100% 0%)', + 'hwba(0, 100%, 0%, 1.0)', + 'hwba(0deg, 100%, 0%, 1.0)', + 'hwba(0 100% 0% / 1.0)', + 'hwba(0deg 100% 0% / 1.0)', + { h: 0, w: 100, b: 0 }, + { h: 0, w: 100, b: 0, a: 1 }, + ], + }, + { + hex: ['#000', '#000f', '#000000', '#000000ff', 0x000000ff], + rgb: [ + 'rgb(0, 0, 0)', + 'rgb(0 0 0)', + 'rgba(0, 0, 0, 1.0)', + 'rgba(0 0 0 / 1.0)', + { r: 0, g: 0, b: 0 }, + { r: 0, g: 0, b: 0, a: 1 }, + ], + hsl: [ + 'hsl(0, 0%, 0%)', + 'hsl(0deg, 0%, 0%)', + 'hsl(0 0% 0%)', + 'hsl(0deg 0% 0%)', + 'hsla(0, 0%, 0%, 1.0)', + 'hsla(0deg, 0%, 0%, 1.0)', + 'hsla(0 0% 0% / 1.0)', + 'hsla(0deg 0% 0% / 1.0)', + { h: 0, s: 0, l: 0 }, + { h: 0, s: 0, l: 0, a: 1 }, + ], + hsv: [ + 'hsv(0, 0%, 0%)', + 'hsv(0deg, 0%, 0%)', + 'hsv(0 0% 0%)', + 'hsv(0deg 0% 0%)', + 'hsva(0, 0%, 0%, 1.0)', + 'hsva(0deg, 0%, 0%, 1.0)', + 'hsva(0 0% 0% / 1.0)', + 'hsva(0deg 0% 0% / 1.0)', + { h: 0, s: 0, v: 0 }, + { h: 0, s: 0, v: 0, a: 1 }, + ], + hwb: [ + 'hwb(0, 0%, 100%)', + 'hwb(0deg, 0%, 100%)', + 'hwb(0 0% 100%)', + 'hwb(0deg 0% 100%)', + 'hwba(0, 0%, 100%, 1.0)', + 'hwba(0deg, 0%, 100%, 1.0)', + 'hwba(0 0% 100% / 1.0)', + 'hwba(0deg 0% 100% / 1.0)', + { h: 0, w: 0, b: 100 }, + { h: 0, w: 0, b: 100, a: 1 }, + ], + }, + { + hex: ['#f00', '#f00f', '#ff0000', '#ff0000ff', 0xff0000ff], + rgb: [ + 'rgb(255, 0, 0)', + 'rgb(255 0 0)', + 'rgba(255, 0, 0, 1.0)', + 'rgba(255 0 0 / 1.0)', + { r: 255, g: 0, b: 0 }, + { r: 255, g: 0, b: 0, a: 1 }, + ], + hsl: [ + 'hsl(0, 100%, 50%)', + 'hsl(0deg, 100%, 50%)', + 'hsl(0 100% 50%)', + 'hsl(0deg 100% 50%)', + 'hsla(0, 100%, 50%, 1.0)', + 'hsla(0deg, 100%, 50%, 1.0)', + 'hsla(0 100% 50% / 1.0)', + 'hsla(0deg 100% 50% / 1.0)', + { h: 0, s: 100, l: 50 }, + { h: 0, s: 100, l: 50, a: 1 }, + ], + hsv: [ + 'hsv(0, 100%, 100%)', + 'hsv(0deg, 100%, 100%)', + 'hsv(0 100% 100%)', + 'hsv(0deg 100% 100%)', + 'hsva(0, 100%, 100%, 1.0)', + 'hsva(0deg, 100%, 100%, 1.0)', + 'hsva(0 100% 100% / 1.0)', + 'hsva(0deg 100% 100% / 1.0)', + { h: 0, s: 100, v: 100 }, + { h: 0, s: 100, v: 100, a: 1 }, + ], + hwb: [ + 'hwb(0, 0%, 0%)', + 'hwb(0deg, 0%, 0%)', + 'hwb(0 0% 0%)', + 'hwb(0deg 0% 0%)', + 'hwba(0, 0%, 0%, 1.0)', + 'hwba(0deg, 0%, 0%, 1.0)', + 'hwba(0 0% 0% / 1.0)', + 'hwba(0deg 0% 0% / 1.0)', + { h: 0, w: 0, b: 0 }, + { h: 0, w: 0, b: 0, a: 1 }, + ], + }, + { + hex: ['#0f0', '#0f0f', '#00ff00', '#00ff00ff', 0x00ff00ff], + rgb: [ + 'rgb(0, 255, 0)', + 'rgb(0 255 0)', + 'rgba(0, 255, 0, 1.0)', + 'rgba(0 255 0 / 1.0)', + { r: 0, g: 255, b: 0 }, + { r: 0, g: 255, b: 0, a: 1 }, + ], + hsl: [ + 'hsl(120, 100%, 50%)', + 'hsl(120deg, 100%, 50%)', + 'hsl(120 100% 50%)', + 'hsl(120deg 100% 50%)', + 'hsla(120, 100%, 50%, 1.0)', + 'hsla(120deg, 100%, 50%, 1.0)', + 'hsla(120 100% 50% / 1.0)', + 'hsla(120deg 100% 50% / 1.0)', + { h: 120, s: 100, l: 50 }, + { h: 120, s: 100, l: 50, a: 1 }, + ], + hsv: [ + 'hsv(120, 100%, 100%)', + 'hsv(120deg, 100%, 100%)', + 'hsv(120 100% 100%)', + 'hsv(120deg 100% 100%)', + 'hsva(120, 100%, 100%, 1.0)', + 'hsva(120deg, 100%, 100%, 1.0)', + 'hsva(120 100% 100% / 1.0)', + 'hsva(120deg 100% 100% / 1.0)', + { h: 120, s: 100, v: 100 }, + { h: 120, s: 100, v: 100, a: 1 }, + ], + hwb: [ + 'hwb(120, 0%, 0%)', + 'hwb(120deg, 0%, 0%)', + 'hwb(120 0% 0%)', + 'hwb(120deg 0% 0%)', + 'hwba(120, 0%, 0%, 1.0)', + 'hwba(120deg, 0%, 0%, 1.0)', + 'hwba(120 0% 0% / 1.0)', + 'hwba(120deg 0% 0% / 1.0)', + { h: 120, w: 0, b: 0 }, + { h: 120, w: 0, b: 0, a: 1 }, + ], + }, + { + hex: ['#00f', '#00ff', '#0000ff', '#0000ffff', 0x0000ffff], + rgb: [ + 'rgb(0, 0, 255)', + 'rgb(0 0 255)', + 'rgba(0, 0, 255, 1.0)', + 'rgba(0 0 255 / 1.0)', + { r: 0, g: 0, b: 255 }, + { r: 0, g: 0, b: 255, a: 1 }, + ], + hsl: [ + 'hsl(240, 100%, 50%)', + 'hsl(240deg, 100%, 50%)', + 'hsl(240 100% 50%)', + 'hsl(240deg 100% 50%)', + 'hsla(240, 100%, 50%, 1.0)', + 'hsla(240deg, 100%, 50%, 1.0)', + 'hsla(240 100% 50% / 1.0)', + 'hsla(240deg 100% 50% / 1.0)', + { h: 240, s: 100, l: 50 }, + { h: 240, s: 100, l: 50, a: 1 }, + ], + hsv: [ + 'hsv(240, 100%, 100%)', + 'hsv(240deg, 100%, 100%)', + 'hsv(240 100% 100%)', + 'hsv(240deg 100% 100%)', + 'hsva(240, 100%, 100%, 1.0)', + 'hsva(240deg, 100%, 100%, 1.0)', + 'hsva(240 100% 100% / 1.0)', + 'hsva(240deg 100% 100% / 1.0)', + { h: 240, s: 100, v: 100 }, + { h: 240, s: 100, v: 100, a: 1 }, + ], + hwb: [ + 'hwb(240, 0%, 0%)', + 'hwb(240deg, 0%, 0%)', + 'hwb(240 0% 0%)', + 'hwb(240deg 0% 0%)', + 'hwba(240, 0%, 0%, 1.0)', + 'hwba(240deg, 0%, 0%, 1.0)', + 'hwba(240 0% 0% / 1.0)', + 'hwba(240deg 0% 0% / 1.0)', + { h: 240, w: 0, b: 0 }, + { h: 240, w: 0, b: 0, a: 1 }, + ], + }, +]; + +describe('RGB to Hex Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { rgb, hex } = colors[i]; + for (let r = 0; r < rgb.length; r++) { + test(`converts ${rgb[r]} to ${hex[2]}`, () => { + const color = colorKit.HEX(rgb[r]); + expect(color).toBe(hex[2]); + }); + } + } +}); + +describe('HSL to Hex Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsl, hex } = colors[i]; + for (let r = 0; r < hsl.length; r++) { + test(`converts ${hsl[r]} to ${hex[2]}`, () => { + const color = colorKit.HEX(hsl[r]); + expect(color).toBe(hex[2]); + }); + } + } +}); + +describe('HWB to Hex Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hwb, hex } = colors[i]; + for (let r = 0; r < hwb.length; r++) { + test(`converts ${hwb[r]} to ${hex[2]}`, () => { + const color = colorKit.HEX(hwb[r]); + expect(color).toBe(hex[2]); + }); + } + } +}); + +describe('HSV to Hex Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsv, hex } = colors[i]; + for (let r = 0; r < hsv.length; r++) { + test(`converts ${hsv[r]} to ${hex[2]}`, () => { + const color = colorKit.HEX(hsv[r]); + expect(color).toBe(hex[2]); + }); + } + } +}); + +describe('Hex to Hex Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hex } = colors[i]; + for (let r = 0; r < hex.length; r++) { + test(`converts ${hex[r]} to ${hex[3]}`, () => { + const color = colorKit.HEX(hex[r]); + expect(color).toBe(hex[3]); + }); + } + } +}); + +describe('HEX to RGB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hex, rgb } = colors[i]; + for (let r = 0; r < hex.length; r++) { + test(`converts ${hex[r]} to ${rgb[0]}`, () => { + const color = colorKit.RGB(hex[r]).string(); + expect(color).toBe(rgb[0]); + }); + } + } +}); + +describe('HSL to RGB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsl, rgb } = colors[i]; + for (let r = 0; r < hsl.length; r++) { + test(`converts ${hsl[r]} to ${rgb[0]}`, () => { + const color = colorKit.RGB(hsl[r]).string(); + expect(color).toBe(rgb[0]); + }); + } + } +}); + +describe('HSV to RGB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsv, rgb } = colors[i]; + for (let r = 0; r < hsv.length; r++) { + test(`converts ${hsv[r]} to ${rgb[0]}`, () => { + const color = colorKit.RGB(hsv[r]).string(); + expect(color).toBe(rgb[0]); + }); + } + } +}); + +describe('HWB to RGB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hwb, rgb } = colors[i]; + for (let r = 0; r < hwb.length; r++) { + test(`converts ${hwb[r]} to ${rgb[0]}`, () => { + const color = colorKit.RGB(hwb[r]).string(); + expect(color).toBe(rgb[0]); + }); + } + } +}); + +describe('RGB to RGB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { rgb } = colors[i]; + for (let r = 0; r < rgb.length; r++) { + test(`converts ${rgb[r]} to ${rgb[0]}`, () => { + const color = colorKit.RGB(rgb[r]).string(); + expect(color).toBe(rgb[0]); + }); + } + } +}); + +describe('HEX to HSL Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hex, hsl } = colors[i]; + for (let r = 0; r < hex.length; r++) { + test(`converts ${hex[r]} to ${hsl[0]}`, () => { + const color = colorKit.HSL(hex[r]).string(); + expect(color).toBe(hsl[0]); + }); + } + } +}); + +describe('RGB to HSL Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { rgb, hsl } = colors[i]; + for (let r = 0; r < rgb.length; r++) { + test(`converts ${rgb[r]} to ${hsl[0]}`, () => { + const color = colorKit.HSL(rgb[r]).string(); + expect(color).toBe(hsl[0]); + }); + } + } +}); + +describe('HWB to HSL Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hwb, hsl } = colors[i]; + for (let r = 0; r < hwb.length; r++) { + test(`converts ${hwb[r]} to ${hsl[0]}`, () => { + const color = colorKit.HSL(hwb[r]).string(); + expect(color).toBe(hsl[0]); + }); + } + } +}); + +describe('HSV to HSL Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsv, hsl } = colors[i]; + for (let r = 0; r < hsv.length; r++) { + test(`converts ${hsv[r]} to ${hsl[0]}`, () => { + const color = colorKit.HSL(hsv[r]).string(); + expect(color).toBe(hsl[0]); + }); + } + } +}); + +describe('HSL to HSL Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsl } = colors[i]; + for (let r = 0; r < hsl.length; r++) { + test(`converts ${hsl[r]} to ${hsl[0]}`, () => { + const color = colorKit.HSL(hsl[r]).string(); + expect(color).toBe(hsl[0]); + }); + } + } +}); + +describe('HEX to HWB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hex, hwb } = colors[i]; + for (let r = 0; r < hex.length; r++) { + test(`converts ${hex[r]} to ${hwb[0]}`, () => { + const color = colorKit.HWB(hex[r]).string(); + expect(color).toBe(hwb[0]); + }); + } + } +}); + +describe('RGB to HWB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { rgb, hwb } = colors[i]; + for (let r = 0; r < rgb.length; r++) { + test(`converts ${rgb[r]} to ${hwb[0]}`, () => { + const color = colorKit.HWB(rgb[r]).string(); + expect(color).toBe(hwb[0]); + }); + } + } +}); + +describe('HSL to HWB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsl, hwb } = colors[i]; + for (let r = 0; r < hsl.length; r++) { + test(`converts ${hsl[r]} to ${hwb[0]}`, () => { + const color = colorKit.HWB(hsl[r]).string(); + expect(color).toBe(hwb[0]); + }); + } + } +}); + +describe('HSV to HWB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsv, hwb } = colors[i]; + for (let r = 0; r < hsv.length; r++) { + test(`converts ${hsv[r]} to ${hwb[0]}`, () => { + const color = colorKit.HWB(hsv[r]).string(); + expect(color).toBe(hwb[0]); + }); + } + } +}); + +describe('HWB to HWB Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hwb } = colors[i]; + for (let r = 0; r < hwb.length; r++) { + test(`converts ${hwb[r]} to ${hwb[0]}`, () => { + const color = colorKit.HWB(hwb[r]).string(); + expect(color).toBe(hwb[0]); + }); + } + } +}); + +describe('HEX to HSV Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hex, hsv } = colors[i]; + for (let r = 0; r < hex.length; r++) { + test(`converts ${hex[r]} to ${hsv[0]}`, () => { + const color = colorKit.HSV(hex[r]).string(); + expect(color).toBe(hsv[0]); + }); + } + } +}); + +describe('RGB to HSV Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { rgb, hsv } = colors[i]; + for (let r = 0; r < rgb.length; r++) { + test(`converts ${rgb[r]} to ${hsv[0]}`, () => { + const color = colorKit.HSV(rgb[r]).string(); + expect(color).toBe(hsv[0]); + }); + } + } +}); + +describe('HSL to HSV Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsl, hsv } = colors[i]; + for (let r = 0; r < hsl.length; r++) { + test(`converts ${hsl[r]} to ${hsv[0]}`, () => { + const color = colorKit.HSV(hsl[r]).string(); + expect(color).toBe(hsv[0]); + }); + } + } +}); + +describe('HWB to HSV Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hwb, hsv } = colors[i]; + for (let r = 0; r < hwb.length; r++) { + test(`converts ${hwb[r]} to ${hsv[0]}`, () => { + const color = colorKit.HSV(hwb[r]).string(); + expect(color).toBe(hsv[0]); + }); + } + } +}); + +describe('HSV to HSV Conversion', () => { + for (let i = 0; i < colors.length; i++) { + const { hsv } = colors[i]; + for (let r = 0; r < hsv.length; r++) { + test(`converts ${hsv[r]} to ${hsv[0]}`, () => { + const color = colorKit.HSV(hsv[r]).string(); + expect(color).toBe(hsv[0]); + }); + } + } +}); diff --git a/__tests__/colorInformation.test.ts b/__tests__/colorInformation.test.ts new file mode 100644 index 0000000..410e6da --- /dev/null +++ b/__tests__/colorInformation.test.ts @@ -0,0 +1,186 @@ +import { describe, expect, test } from '@jest/globals'; +import colorKit from '../src/colorKit/index'; + +const colorData = [ + { + color: '#c992d2', + expected: { + format: 'hex6', + red: 201, + green: 146, + blue: 210, + alpha: 1, + hue: 292, + brightness: 82, + luminance: 70, + luminanceWCAG: 0.3762841918, + saturation: 42, + isDark: true, + isLight: false, + }, + }, + { + color: '#00ff00', + expected: { + format: 'hex6', + red: 0, + green: 255, + blue: 0, + alpha: 1, + hue: 120, + brightness: 100, + luminance: 50, + luminanceWCAG: 0.7152, + saturation: 100, + isDark: false, + isLight: true, + }, + }, + { + color: '#0000ff', + expected: { + format: 'hex6', + red: 0, + green: 0, + blue: 255, + alpha: 1, + hue: 240, + brightness: 100, + luminance: 50, + luminanceWCAG: 0.0722, + saturation: 100, + isDark: true, + isLight: false, + }, + }, + { + color: 'rgba(39, 98, 186, 0.5)', + expected: { + format: 'rgba', + red: 39, + green: 98, + blue: 186, + alpha: 0.5, + hue: 216, + brightness: 73, + luminance: 44, + luminanceWCAG: 0.1271187038, + saturation: 65, + isDark: true, + isLight: false, + }, + }, + { + color: 'hsl(310, 68%, 47%)', + expected: { + format: 'hsl', + red: 201, + green: 38, + blue: 174, + alpha: 1, + hue: 310, + brightness: 79, + luminance: 47, + luminanceWCAG: 0.1668271683, + saturation: 68, + isDark: true, + isLight: false, + }, + }, + { + color: 'hwb(182, 49%, 7%)', + expected: { + format: 'hwb', + red: 125, + green: 233, + blue: 237, + alpha: 1, + hue: 182, + brightness: 93, + luminance: 71, + luminanceWCAG: 0.6882787144, + saturation: 76, + isDark: false, + isLight: true, + }, + }, + { + color: 'hsv(54, 51%, 76%)', + expected: { + format: 'hsv', + red: 194, + green: 184, + blue: 95, + alpha: 1, + hue: 54, + brightness: 76, + luminance: 57, + luminanceWCAG: 0.4701178587, + saturation: 45, + isDark: true, + isLight: false, + }, + }, + { + color: 0xdeb3f5ff, + expected: { + format: 'hex8', + red: 222, + green: 179, + blue: 245, + alpha: 1, + hue: 279, + brightness: 96, + luminance: 83, + luminanceWCAG: 0.5436236679, + saturation: 77, + isDark: false, + isLight: true, + }, + }, +]; + +describe('get the color information', () => { + for (const data of colorData) { + test(`For the color "${data.color}"`, () => { + const type = colorKit.getFormat(data.color); + expect(type).toBe(data.expected.format); + + const red = colorKit.getRed(data.color); + expect(red).toBe(data.expected.red); + + const green = colorKit.getGreen(data.color); + expect(green).toBe(data.expected.green); + + const blue = colorKit.getBlue(data.color); + expect(blue).toBe(data.expected.blue); + + const alpha = colorKit.getAlpha(data.color); + expect(alpha).toBe(data.expected.alpha); + + const hue = colorKit.getHue(data.color); + expect(hue).toBe(data.expected.hue); + + const brightness = colorKit.getBrightness(data.color); + expect(brightness).toBe(data.expected.brightness); + + const luminance = colorKit.getLuminance(data.color); + expect(luminance).toBe(data.expected.luminance); + + const luminanceWCAG = colorKit.getLuminanceWCAG(data.color); + expect(luminanceWCAG).toBeCloseTo(data.expected.luminanceWCAG); + + const saturation = colorKit.getSaturation(data.color); + expect(saturation).toBe(data.expected.saturation); + + const isDark = colorKit.isDark(data.color); + expect(isDark).toBe(data.expected.isDark); + + const isLight = colorKit.isLight(data.color); + expect(isLight).toBe(data.expected.isLight); + + const areColorsEqual = colorKit.areColorsEqual(data.color, data.color); + expect(areColorsEqual).toBeTruthy(); + }); + } +}); diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..e6ffbd4 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], +}; diff --git a/docusaurus/docs/ColorKit.mdx b/docusaurus/docs/ColorKit.mdx index 508bbaa..ee57df3 100644 --- a/docusaurus/docs/ColorKit.mdx +++ b/docusaurus/docs/ColorKit.mdx @@ -63,10 +63,7 @@ sidebar_position: 6 #### Color ints -- `0xf0f` (0xrgb) -- `0xff00ff` (0xrrggbb) -- `0xf0ff` (0xrgba) -- `0xff00ff00` (0xrrggbbaa) +- `0xff00ffff` (0xrrggbbaa) #### Color keywords diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..be19c64 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,12 @@ +/** + * For a detailed explanation regarding each configuration property, visit: + * https://jestjs.io/docs/configuration + */ + +import type { Config } from 'jest'; + +const config: Config = { + testMatch: ['/__tests__/**/*.test.ts'], +}; + +export default config; diff --git a/package.json b/package.json index b3001c1..8f75c01 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "types": "lib/typescript/index.d.ts", "scripts": { "test": "tsc --noEmit && eslint src --max-warnings=0", + "test-colorKit": "jest --silent=false", "build": "node scripts/build.js", "prettier ": "prettier --ignore-unknown --write src --config-precedence prefer-file", - "prepublishOnly": "npm run test && npm run build" + "prepublishOnly": "npm run test && npm run test-colorKit && npm run build" }, "keywords": [ "react-native", @@ -41,17 +42,22 @@ "homepage": "https://github.com/alabsi91/reanimated-color-picker", "devDependencies": { "@babel/cli": "^7.18.6", - "@babel/core": "^7.12.9", - "@babel/preset-env": "^7.20.2", + "@babel/core": "^7.23.2", + "@babel/preset-env": "^7.23.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", - "@react-native-community/eslint-config": "^3.1.0", + "@react-native-community/eslint-config": "3.1.0", "@tsconfig/react-native": "^2.0.2", "@types/react-native": "^0.71.5", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.0", + "jest": "^29.7.0", "prettier": "2.8.4", + "react-native": "^0.70.14", "react-native-reanimated": "^2.8.0", "resolve-tspaths": "^0.8.8", + "ts-node": "^10.9.1", "typescript": "^4.8.4" }, "peerDependencies": { @@ -65,7 +71,10 @@ } }, "eslintConfig": { - "extends": "@react-native-community", + "extends": [ + "@react-native-community", + "plugin:@typescript-eslint/recommended" + ], "ignorePatterns": [ "scripts/*", "Example/**/*", @@ -75,6 +84,9 @@ "docusaurus/**/*" ], "rules": { + "prefer-const": 1, + "@typescript-eslint/consistent-type-imports": 1, + "@typescript-eslint/no-non-null-assertion": 0, "react-native/no-inline-styles": 0, "react-native/no-single-element-style-arrays": 1, "react-native/no-unused-styles": 1, diff --git a/src/ColorPicker.tsx b/src/ColorPicker.tsx index 619390a..8d3de8c 100644 --- a/src/ColorPicker.tsx +++ b/src/ColorPicker.tsx @@ -11,9 +11,10 @@ import type { ColorPickerContext, ColorPickerProps, ColorPickerRef } from '@type import type { SupportedColorFormats } from './colorKit/types'; if (isWeb) { - // @ts-ignore + // @ts-expect-error no global if (!global.setImmediate) global.setImmediate = setTimeout; try { + // eslint-disable-next-line @typescript-eslint/no-var-requires const { enableExperimentalWebImplementation } = require('react-native-gesture-handler'); enableExperimentalWebImplementation(true); } catch (error) { diff --git a/src/colorKit/PrivateMethods.ts b/src/colorKit/PrivateMethods.ts deleted file mode 100644 index 8482ea9..0000000 --- a/src/colorKit/PrivateMethods.ts +++ /dev/null @@ -1,603 +0,0 @@ -import colorsRegex from './colorsRegex'; -import { clamp100, clampAlpha, clampHue, clampRGB } from './utilities'; - -import type { SupportedColorFormats, ColorFormats, hslaT, hslT, hsvaT, hsvT, hwbaT, hwbT, rgbaT, rgbT } from './types'; - -export default class PrivateMethods { - /** - Identify the color format of a given `string` or `object` */ - detectColorFormat(color: SupportedColorFormats): ColorFormats | null { - // color int - if (typeof color === 'number') color = '#' + color.toString(16); - - // color string - if (typeof color === 'string') { - color = color.trim().toLowerCase(); - for (const key in colorsRegex) { - const format = key as ColorFormats; - const entry = colorsRegex[format]; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) return format; - continue; - } - if (entry.test(color)) return format; - } - } - - // color object - if (typeof color === 'object') { - const rgbaKeys = ['r', 'g', 'b', 'a'] as (keyof rgbaT)[]; - const isRgbaOb = rgbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbaT)[k] === 'number'); - if (isRgbaOb) return 'rgba'; - - const rgbKeys = ['r', 'g', 'b'] as (keyof rgbT)[]; - const isRgbOb = rgbKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbT)[k] === 'number'); - if (isRgbOb) return 'rgb'; - - const hslaKeys = ['h', 's', 'l', 'a'] as (keyof hslaT)[]; - const isHslaOb = hslaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslaT)[k] === 'number'); - if (isHslaOb) return 'hsla'; - - const hslKeys = ['h', 's', 'l'] as (keyof hslT)[]; - const isHslOb = hslKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslT)[k] === 'number'); - if (isHslOb) return 'hsl'; - - const hsvaKeys = ['h', 's', 'v', 'a'] as (keyof hsvaT)[]; - const isHsvaOb = hsvaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvaT)[k] === 'number'); - if (isHsvaOb) return 'hsva'; - - const hsvKeys = ['h', 's', 'v'] as (keyof hsvT)[]; - const isHsvOb = hsvKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvT)[k] === 'number'); - if (isHsvOb) return 'hsv'; - - const hwbaKeys = ['h', 'w', 'b', 'a'] as (keyof hwbaT)[]; - const isHwbaOb = hwbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbaT)[k] === 'number'); - if (isHwbaOb) return 'hwba'; - - const hwbKeys = ['h', 'w', 'b'] as (keyof hwbT)[]; - const isHwbOb = hwbKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbT)[k] === 'number'); - if (isHwbOb) return 'hwb'; - } - - return null; - } - - // * RGB - /** - Get `RGB` or `RGBA` color values as an `object` */ - getRgbObject(color: string): rgbaT { - color = color.trim().toLowerCase(); - const colorType = this.detectColorFormat(color); - - if (!colorType?.includes('rgb')) { - console.error( - '[colorKit] is unable to parse the string into an `RGB` or `RGBA` object. As a result, the color "black" will be returned instead.' - ); - return { r: 0, g: 0, b: 0, a: 1 }; - } - - let match: RegExpMatchArray | null = null; - const entry = colorsRegex[colorType]; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) match = color.match(entry[i]); - } else match = color.match(entry); - - if (!match) - console.error('[colorKit] An error occurred while attempting to destructuring `RGB` values from the given string!!'); - - return { - r: clampRGB(parseInt(match?.[1] ?? '0', 10)), - g: clampRGB(parseInt(match?.[2] ?? '0', 10)), - b: clampRGB(parseInt(match?.[3] ?? '0', 10)), - a: clampAlpha(parseFloat(match?.[4] ?? '1')), - }; - } - /** - Convert an `RGB` or `RGBA` color to its corresponding `Hex` color */ - RGB_HEX(color: string | rgbaT | rgbT): string { - const { r, g, b, a } = - typeof color === 'string' - ? this.getRgbObject(color.trim().toLowerCase()) - : { r: color.r, g: color.g, b: color.b, a: (color as rgbaT).a ?? 1 }; - - const toHex = (c: number): string => { - c = clampRGB(c); - const hex = c.toString(16); - return hex.length === 1 ? '0' + hex : hex; - }; - - const red = toHex(r), - green = toHex(g), - blue = toHex(b), - alpha = a === 1 ? '' : toHex(a * 255); - return `#${red + green + blue + alpha}`; - } - /** - Convert an `RGB` or `RGBA` color to an `HSLA` object representation */ - RGB_HSLA(color: string | rgbaT | rgbT): hslaT { - const rgb = typeof color === 'string' ? this.getRgbObject(color.trim().toLowerCase()) : color, - r = rgb.r / 255, - g = rgb.g / 255, - b = rgb.b / 255, - a = (rgb as rgbaT).a ?? 1; - - let max = Math.max(r, g, b), - min = Math.min(r, g, b), - h = 0, - s, - l = (max + min) / 2; - - if (max === min) { - h = s = 0; - } else { - let d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - case g: - h = (b - r) / d + 2; - break; - case b: - h = (r - g) / d + 4; - break; - } - h /= 6; - } - - h = clampHue(h * 360); - s = clamp100(s * 100); - l = clamp100(l * 100); - - return { h, s, l, a: clampAlpha(a) }; - } - /** - Convert `RGB` or `RGBA` color to an `HSVA` object representation */ - RGB_HSVA(color: string | rgbaT | rgbT): hsvaT { - const rgb = typeof color === 'string' ? this.getRgbObject(color.trim().toLowerCase()) : color, - r = rgb.r / 255, - g = rgb.g / 255, - b = rgb.b / 255, - a = (rgb as rgbaT).a ?? 1; - - const max = Math.max(r, g, b), - min = Math.min(r, g, b), - d = max - min; - - let v = max, - h = 0, - s = max === 0 ? 0 : d / max; - - if (max === min) { - h = 0; - } else { - if (max === r) { - h = (g - b) / d + (g < b ? 6 : 0); - } else if (max === g) { - h = (b - r) / d + 2; - } else if (max === b) { - h = (r - g) / d + 4; - } - h = h / 6; - } - - return { - h: clampHue(h * 360), - s: clamp100(s * 100), - v: clamp100(v * 100), - a: clampAlpha(a), - }; - } - /** - Convert `RGB` or `RGBA` color to an `HWBA` object representation */ - RGB_HWBA(color: string | rgbaT | rgbT): hwbaT { - const rgb = typeof color === 'string' ? this.getRgbObject(color.trim().toLowerCase()) : color, - red = rgb.r / 255, - green = rgb.g / 255, - blue = rgb.b / 255, - a = (rgb as rgbaT).a ?? 1; - - const { h } = this.RGB_HSLA(color); - - const white = Math.min(red, green, blue) * 100; - const black = (1 - Math.max(red, green, blue)) * 100; - - return { - h: clampHue(h), - w: clamp100(white), - b: clamp100(black), - a: clampAlpha(a), - }; - } - - // * HEX * 100 - /** - Convert any `HEX` color to 8-digit `HEX` color (#rrggbbaa) */ - normalizeHexColor(color: string | number): string { - if (typeof color === 'number') color = '#' + color.toString(16); // color int - - const colorType = this.detectColorFormat(color.trim().toLowerCase()); - - if (!colorType?.includes('hex')) { - console.error( - '[colorKit] is unable to normalize the `HEX` string provided. As a result, the color "black" will be returned instead.' - ); - return '#000000ff'; - } - - const hex = color.replace(/^#/, '').split(''); - - if (hex.length === 3) return `#${hex.map(x => x + x).join('')}ff`; - if (hex.length === 4) return `#${hex.map(x => x + x).join('')}`; - if (hex.length === 6) return `#${hex.join('')}ff`; - - return color; - } - /** - Convert any `HEX` color to an `RGBA` object representation */ - HEX_RGBA(color: string | number): rgbaT { - if (typeof color === 'number') color = '#' + color.toString(16); // color int - - const hex = this.normalizeHexColor(color.trim().toLowerCase()); - - let match: RegExpMatchArray | null = null; - const entry = colorsRegex.hex8; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(hex)) match = hex.match(entry[i]); - } else match = hex.match(entry); - - if (!match) - console.error('[colorKit] An error occurred while attempting to destructuring `HEX` values from the given string!!'); - - return { - r: clampRGB(parseInt(match?.[1] ?? '0', 16)), - g: clampRGB(parseInt(match?.[2] ?? '0', 16)), - b: clampRGB(parseInt(match?.[3] ?? '0', 16)), - a: clampAlpha(parseInt(match?.[4] ?? '0', 16) / 255), - }; - } - /** - Convert any `HEX` color to an `HSVA` object representation */ - HEX_HSVA(color: string | number): hsvaT { - const rgb = this.HEX_RGBA(color); - const hsva = this.RGB_HSVA(rgb); - return hsva; - } - /** - Convert any `HEX` color to an `HSLA` object representation */ - HEX_HSLA(color: string): hslaT { - const rgb = this.HEX_RGBA(color); - return this.RGB_HSLA(rgb); - } - /** - Convert any `HEX` color to an `HWBA` object representation */ - HEX_HWBA(color: string): hwbaT { - const rgba = this.HEX_RGBA(color); - return this.RGB_HWBA(rgba); - } - - // * HSL - /** - Get `HSL` or `HSLA` color values as an `object` */ - getHslObject(color: string): hslaT { - color = color.trim().toLowerCase(); - const colorType = this.detectColorFormat(color); - - if (!colorType?.includes('hsl')) { - console.error( - '[colorKit] is unable to parse the string into an `HSL` or `HSLA` object. As a result, the color "black" will be returned instead.' - ); - return { h: 0, s: 0, l: 0, a: 1 }; - } - - let match: RegExpMatchArray | null = null; - const entry = colorsRegex[colorType]; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) match = color.match(entry[i]); - } else match = color.match(entry); - - if (!match) - console.error('[colorKit] An error occurred while attempting to destructuring `HSL` values from the given string!!'); - - return { - h: clampHue(parseInt(match?.[1] ?? '0', 10)), - s: clamp100(parseInt(match?.[2] ?? '0', 10)), - l: clamp100(parseInt(match?.[3] ?? '0', 10)), - a: clampAlpha(parseFloat(match?.[4] ?? '1')), - }; - } - /** - Convert `HSL` or `HSLA` color to an `RGBA` object representation */ - HSL_RGBA(color: string | hslaT | hslT): rgbaT { - const hsla = typeof color === 'string' ? this.getHslObject(color.trim().toLowerCase()) : color; - - const hue2rgb = (p: number, q: number, t: number) => { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - }; - - const h = hsla.h / 360, - s = hsla.s / 100, - l = hsla.l / 100, - a = (hsla as hslaT).a ?? 1; - - const q = l < 0.5 ? l * (1 + s) : l + s - l * s, - p = 2 * l - q; - - const r = hue2rgb(p, q, h + 1 / 3), - g = hue2rgb(p, q, h), - b = hue2rgb(p, q, h - 1 / 3); - - return { - r: clampRGB(r * 255), - g: clampRGB(g * 255), - b: clampRGB(b * 255), - a: clampAlpha(a), - }; - } - /** - Convert `HSL` or `HSLA` color to `HEX` color */ - HSL_HEX(color: string | hslaT | hslT): string { - const hsla = - typeof color === 'string' - ? this.getHslObject(color.trim().toLowerCase()) - : { h: color.h, s: color.s, l: color.l, a: (color as hslaT).a ?? 1 }; - - const toHex = (c: number): string => { - c = clampRGB(c); - const hex = c.toString(16); - return hex.length === 1 ? '0' + hex : hex; - }; - - const { r, g, b, a } = this.HSL_RGBA(hsla), - red = toHex(r), - green = toHex(g), - blue = toHex(b), - alpha = a === 1 ? '' : toHex(a * 255); - - return `#${red + green + blue + alpha}`; - } - /** - Convert `HSL` or `HSLA` color to an `HSVA` object representation */ - HSL_HSVA(color: string | hslaT | hslT): hsvaT { - const hsla = typeof color === 'string' ? this.getHslObject(color.trim().toLowerCase()) : color; - const h = hsla.h; - - const s = hsla.s / 100, - l = hsla.l / 100, - a = (hsla as hslaT).a ?? 1, - v = l + s * Math.min(l, 1 - l), - sNew = v === 0 ? 0 : 2 - (2 * l) / v; - - return { - h: clampHue(h), - s: clamp100(sNew * 100), - v: clamp100(v * 100), - a: clampAlpha(a), - }; - } - /** - Convert `HSL` or `HSLA` color to an `HWBA` object representation */ - HSL_HWBA(color: string | hslaT | hslT): hwbaT { - const hsva = this.HSL_HSVA(color); - return this.HSV_HWBA(hsva); - } - - // * HSV - /** - Get `HSV` or `HSVA` color values as an `object` */ - getHsvObject(color: string): hsvaT { - color = color.trim().toLowerCase(); - const colorType = this.detectColorFormat(color); - - if (!colorType?.includes('hsv')) { - console.error( - '[colorKit] is unable to parse the string into an `HSV` or `HSVA` object. As a result, the color "black" will be returned instead.' - ); - return { h: 0, s: 0, v: 0, a: 1 }; - } - - let match: RegExpMatchArray | null = null; - const entry = colorsRegex[colorType]; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) match = color.match(entry[i]); - } else match = color.match(entry); - - if (!match) - console.error('[colorKit] An error occurred while attempting to destructuring `HSL` values from the given string!!'); - - return { - h: clampHue(parseInt(match?.[1] ?? '0', 10)), - s: clamp100(parseInt(match?.[2] ?? '0', 10)), - v: clamp100(parseInt(match?.[3] ?? '0', 10)), - a: clampAlpha(parseFloat(match?.[4] ?? '1')), - }; - } - /** - Convert `HSV` color to an `RGBA` object representation */ - HSV_RGBA(color: hsvaT | hsvT | string): rgbaT { - const hsva = typeof color === 'string' ? this.getHsvObject(color.trim().toLowerCase()) : color; - - const h = hsva.h / 360, - s = hsva.s / 100, - v = hsva.v / 100, - a = (hsva as hsvaT).a ?? 1; - - const i = Math.floor(h * 6), - f = h * 6 - i, - p = v * (1 - s), - q = v * (1 - f * s), - t = v * (1 - (1 - f) * s); - - let r = 0, - g = 0, - b = 0; - switch (i % 6) { - case 0: - r = v; - g = t; - b = p; - break; - case 1: - r = q; - g = v; - b = p; - break; - case 2: - r = p; - g = v; - b = t; - break; - case 3: - r = p; - g = q; - b = v; - break; - case 4: - r = t; - g = p; - b = v; - break; - case 5: - r = v; - g = p; - b = q; - break; - } - - return { - r: clampRGB(r * 255), - g: clampRGB(g * 255), - b: clampRGB(b * 255), - a: clampAlpha(a), - }; - } - /** - Convert `HSV` color to an `HSLA` object representation */ - HSV_HSLA(color: hsvaT | hsvT | string): hslaT { - const hsva = typeof color === 'string' ? this.getHsvObject(color.trim().toLowerCase()) : color; - - const h = hsva.h, - s = hsva.s / 100, - v = hsva.v / 100, - a = (hsva as hsvaT).a ?? 1; - - const l = ((2 - s) * v) / 2, - sl = s * v, - sln = l !== 0 && l !== 1 ? sl / (l < 0.5 ? l * 2 : 2 - l * 2) : sl; - - return { - h: clampHue(h), - s: clamp100(sln * 100), - l: clamp100(l * 100), - a: clampAlpha(a), - }; - } - /** - Convert `HSV` color to an `Hex` color */ - HSV_HEX(color: hsvaT | hsvT | string): string { - const rgba = this.HSV_RGBA(color); - const hex = this.RGB_HEX(rgba); - return hex; - } - /** - Convert `HSV` color to an `HWBA` object representation */ - HSV_HWBA(color: hsvaT | hsvT | string): hwbaT { - const hsva = typeof color === 'string' ? this.getHsvObject(color.trim().toLowerCase()) : color; - - const { h, s, v } = hsva, - a = (hsva as hsvaT).a ?? 1, - w = (1 - s / 100) * v, - b = (1 - v / 100) * 100; - - return { - h: clampHue(h), - w: clamp100(w), - b: clamp100(b), - a: clampAlpha(a), - }; - } - - // * HWB - /** - Get `HWB` or `HWBA` color values as an `object` */ - getHwbObject(color: string): hwbaT { - color = color.trim().toLowerCase(); - const colorType = this.detectColorFormat(color); - - if (!colorType?.includes('hwb')) { - console.error( - '[colorKit] is unable to parse the string into an `HWB` or `HWBA` object. As a result, the color "black" will be returned instead.' - ); - return { h: 0, w: 0, b: 0, a: 1 }; - } - - let match: RegExpMatchArray | null = null; - const entry = colorsRegex[colorType]; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) match = color.match(entry[i]); - } else match = color.match(entry); - - if (!match) - console.error('[colorKit] An error occurred while attempting to destructuring `HWB` values from the given string!!'); - - return { - h: clampHue(parseInt(match?.[1] ?? '0', 10)), - w: clamp100(parseInt(match?.[2] ?? '0', 10)), - b: clamp100(parseInt(match?.[3] ?? '100', 10)), - a: clampAlpha(parseFloat(match?.[4] ?? '1')), - }; - } - /** - Convert `HWB` or `HWBA` color to an `RGBA` object representation */ - HWB_RGBA(color: hwbaT | hwbT | string): rgbaT { - const hwba = typeof color === 'string' ? this.getHwbObject(color.trim().toLowerCase()) : color; - - const h = hwba.h / 360, - w = hwba.w / 100, - b = hwba.b / 100, - a = (hwba as hwbaT)?.a ?? 1; - - if (w + b >= 1) { - const gray = clampRGB((w * 255) / (w + b)); - return { - r: gray, - g: gray, - b: gray, - a, - }; - } - - const calcHue = (p: number, q: number, t: number) => { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - }; - - const red = calcHue(0, 1, h + 1 / 3) * (1 - w - b) + w, - green = calcHue(0, 1, h) * (1 - w - b) + w, - blue = calcHue(0, 1, h - 1 / 3) * (1 - w - b) + w; - - return { - r: clampRGB(red * 255), - g: clampRGB(green * 255), - b: clampRGB(blue * 255), - a: clampAlpha(a), - }; - } - /** - Convert `HWB` or `HWBA` color to an `Hex` color */ - HWB_HEX(color: hwbaT | hwbT | string): string { - const rgba = this.HWB_RGBA(color); - return this.RGB_HEX(rgba); - } - /** - Convert `HWB` or `HWBA` color to an `HSVA` object representation */ - HWB_HSVA(color: hwbaT | hwbT | string): hsvaT { - const hwba = typeof color === 'string' ? this.getHwbObject(color.trim().toLowerCase()) : color; - - const h = hwba.h % 360, - w = hwba.w / 100, - b = hwba.b / 100, - a = (hwba as hwbaT)?.a ?? 1; - - const v = (1 - b) * 100; - let s = (1 - w / (v / 100)) * 100; - s = isNaN(s) ? 0 : s; - - return { - h: clampHue(h), - s: clamp100(s), - v: clamp100(v), - a: clampAlpha(a), - }; - } - /** - Convert `HWB` or `HWBA` color to an `HSLA` object representation */ - HWB_HSLA(color: hwbaT | hwbT | string): hslaT { - const hsva = this.HWB_HSVA(color); - return this.HSV_HSLA(hsva); - } -} diff --git a/src/colorKit/colorConversion.ts b/src/colorKit/colorConversion.ts new file mode 100644 index 0000000..3be08f8 --- /dev/null +++ b/src/colorKit/colorConversion.ts @@ -0,0 +1,1061 @@ +import colorsRegex from './colorsRegex'; +import namedColors from './namedColors'; +import { calculateHueValue, clamp100, clampAlpha, clampHue, clampRGB, numberToHexString } from './utilities'; + +import type { ColorFormats, hslaT, hslT, hsvaT, hsvT, hwbaT, hwbT, rgbaT, rgbT, SupportedColorFormats } from './types'; + +// ! Caution: The order of functions is crucial, +// ! as the react-native-reanimated Babel plugin will transform worklet functions +// ! and store them in a constant variables. + +/** - Identify the color format of a given `string` or `object` */ +function detectColorFormat(color: SupportedColorFormats): ColorFormats | null { + 'worklet'; + // color int + if (typeof color === 'number') { + if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) return 'hex8'; + return null; + } + + // color string + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + for (const key in colorsRegex) { + const format = key as ColorFormats; + const entry = colorsRegex[format]; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) { + if (entry[i].test(color)) return format; + } + continue; + } + if (entry.test(color)) return format; + } + } + + // color object + if (typeof color === 'object') { + const rgbaKeys = ['r', 'g', 'b', 'a'] as (keyof rgbaT)[]; + const isRgbaOb = rgbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbaT)[k] === 'number'); + if (isRgbaOb) return 'rgba'; + + const rgbKeys = ['r', 'g', 'b'] as (keyof rgbT)[]; + const isRgbOb = rgbKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbT)[k] === 'number'); + if (isRgbOb) return 'rgb'; + + const hslaKeys = ['h', 's', 'l', 'a'] as (keyof hslaT)[]; + const isHslaOb = hslaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslaT)[k] === 'number'); + if (isHslaOb) return 'hsla'; + + const hslKeys = ['h', 's', 'l'] as (keyof hslT)[]; + const isHslOb = hslKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslT)[k] === 'number'); + if (isHslOb) return 'hsl'; + + const hsvaKeys = ['h', 's', 'v', 'a'] as (keyof hsvaT)[]; + const isHsvaOb = hsvaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvaT)[k] === 'number'); + if (isHsvaOb) return 'hsva'; + + const hsvKeys = ['h', 's', 'v'] as (keyof hsvT)[]; + const isHsvOb = hsvKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvT)[k] === 'number'); + if (isHsvOb) return 'hsv'; + + const hwbaKeys = ['h', 'w', 'b', 'a'] as (keyof hwbaT)[]; + const isHwbaOb = hwbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbaT)[k] === 'number'); + if (isHwbaOb) return 'hwba'; + + const hwbKeys = ['h', 'w', 'b'] as (keyof hwbT)[]; + const isHwbOb = hwbKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbT)[k] === 'number'); + if (isHwbOb) return 'hwb'; + } + + return null; +} + +// * RGB +/** - Parse `RGB` or `RGBA` color string to an `object` */ +function RGB_Parse_String(color: string): rgbaT { + 'worklet'; + color = color.trim().toLowerCase(); + const colorType = detectColorFormat(color); + + if (!colorType || !colorType.includes('rgb')) { + console.error( + '[colorKit.getRgbObject] is unable to parse the string into an `RGB` object. As a result, the color "black" will be returned instead.' + ); + return { r: 0, g: 0, b: 0, a: 1 }; + } + + let matches: RegExpMatchArray | null = null; + const entry = colorsRegex[colorType]; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) { + if (entry[i].test(color)) matches = color.match(entry[i]); + } + } else { + matches = color.match(entry); + } + + if (!matches || matches.length < 4) { + console.error( + '[colorKit.getRgbObject] An error occurred while attempting to destructuring `RGB` values from the given string. As a result, the color "black" will be returned instead.' + ); + return { r: 0, g: 0, b: 0, a: 1 }; + } + + const r = parseInt(matches[1], 10), + g = parseInt(matches[2], 10), + b = parseInt(matches[3], 10), + a = parseFloat(matches[4] ?? '1'); + + return { + r: clampRGB(r), + g: clampRGB(g), + b: clampRGB(b), + a: clampAlpha(a), + }; +} +/** - Ensure that the `RGB` object values are within the correct range and that it has the alpha channel */ +function RGB_Normalize_Object(color: rgbaT | rgbT): rgbaT { + 'worklet'; + return { + r: clampRGB(color.r), + g: clampRGB(color.g), + b: clampRGB(color.b), + a: clampAlpha((color as rgbaT).a ?? 1), + }; +} +/** - Convert an `RGB` or `RGBA` color to its corresponding `Hex` color */ +function RGB_HEX(color: string | rgbaT | rgbT): string { + 'worklet'; + const { r, g, b, a } = typeof color === 'string' ? RGB_Parse_String(color) : RGB_Normalize_Object(color); + + const red = numberToHexString(r), + green = numberToHexString(g), + blue = numberToHexString(b), + alpha = a === 1 ? '' : numberToHexString(a * 255); + + return `#${red + green + blue + alpha}`; +} +/** - Convert an `RGB` or `RGBA` color to an `HSLA` object representation */ +function RGB_HSLA(color: string | rgbaT | rgbT): hslaT { + 'worklet'; + const rgb = typeof color === 'string' ? RGB_Parse_String(color) : RGB_Normalize_Object(color), + r = rgb.r / 255, + g = rgb.g / 255, + b = rgb.b / 255, + a = rgb.a; + + const max = Math.max(r, g, b), + min = Math.min(r, g, b); + + let h = 0, + s, + l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + if (max === r) { + h = (g - b) / d + (g < b ? 6 : 0); + } else if (max === g) { + h = (b - r) / d + 2; + } else if (max === b) { + h = (r - g) / d + 4; + } + + h /= 6; + } + + h = clampHue(h * 360); + s = clamp100(s * 100); + l = clamp100(l * 100); + + return { h, s, l, a: clampAlpha(a) }; +} +/** - Convert `RGB` or `RGBA` color to an `HSVA` object representation */ +function RGB_HSVA(color: string | rgbaT | rgbT): hsvaT { + 'worklet'; + const rgb = typeof color === 'string' ? RGB_Parse_String(color) : RGB_Normalize_Object(color), + r = rgb.r / 255, + g = rgb.g / 255, + b = rgb.b / 255, + a = rgb.a; + + const max = Math.max(r, g, b), + min = Math.min(r, g, b), + d = max - min, + v = max, + s = max === 0 ? 0 : d / max; + + let h = 0; + + if (max === min) { + h = 0; + } else { + if (max === r) { + h = (g - b) / d + (g < b ? 6 : 0); + } else if (max === g) { + h = (b - r) / d + 2; + } else if (max === b) { + h = (r - g) / d + 4; + } + + h = h / 6; + } + + return { + h: clampHue(h * 360), + s: clamp100(s * 100), + v: clamp100(v * 100), + a: clampAlpha(a), + }; +} +/** - Convert `RGB` or `RGBA` color to an `HWBA` object representation */ +function RGB_HWBA(color: string | rgbaT | rgbT): hwbaT { + 'worklet'; + const rgb = typeof color === 'string' ? RGB_Parse_String(color) : RGB_Normalize_Object(color), + red = rgb.r / 255, + green = rgb.g / 255, + blue = rgb.b / 255, + a = rgb.a; + + const { h } = RGB_HSLA(color); + + const white = Math.min(red, green, blue) * 100; + const black = (1 - Math.max(red, green, blue)) * 100; + + return { + h: clampHue(h), + w: clamp100(white), + b: clamp100(black), + a: clampAlpha(a), + }; +} +/** - Return the `RGB` color as a string, an array, or an object */ +function RGB_Return_Types({ r, g, b, a }: rgbaT) { + 'worklet'; + return { + string: (forceAlpha?: boolean) => { + 'worklet'; + // auto + if (typeof forceAlpha === 'undefined') { + if (typeof a === 'number' && a !== 1) return `rgba(${r}, ${g}, ${b}, ${a})`; + return `rgb(${r}, ${g}, ${b})`; + } + + if (forceAlpha) return `rgba(${r}, ${g}, ${b}, ${a ?? 1})`; + + return `rgb(${r}, ${g}, ${b})`; + }, + array: () => { + 'worklet'; + return [r, g, b, a]; + }, + object: () => { + 'worklet'; + return { r, g, b, a }; + }, + }; +} + +// * HEX +/** - Convert any `HEX` color to 8-digit `HEX` color (#rrggbbaa) */ +function HEX_Normalize(color: string | number): string { + 'worklet'; + if (typeof color === 'number') { + return '#' + color.toString(16).padStart(8, '0'); + } + + color = color.trim().toLowerCase(); + const colorType = detectColorFormat(color); + + if (!colorType || !colorType.includes('hex')) { + console.error( + '[colorKit.normalizeHexColor] is unable to normalize the `HEX` string provided. As a result, the color "black" will be returned instead.' + ); + return '#000000ff'; + } + + const hex = color.replace(/^#/, '').split(''); + + if (hex.length === 3) return `#${hex.map(x => x + x).join('')}ff`; + if (hex.length === 4) return `#${hex.map(x => x + x).join('')}`; + if (hex.length === 6) return `#${hex.join('')}ff`; + + return color; +} +/** - Convert any `HEX` color to an `RGBA` object representation */ +function HEX_RGBA(color: string | number): rgbaT { + 'worklet'; + + const hex = HEX_Normalize(color); + + let matches: RegExpMatchArray | null = null; + const entry = colorsRegex.hex8; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) { + if (entry[i].test(hex)) matches = hex.match(entry[i]); + } + } else { + matches = hex.match(entry); + } + + if (!matches || matches.length < 4) { + console.error( + '[colorKit.HEX_RGBA] An error occurred while attempting to destructuring `HEX` values from the given string. As a result, the color "black" will be returned instead.' + ); + return { r: 0, g: 0, b: 0, a: 1 }; + } + + const r = parseInt(matches[1], 16), + g = parseInt(matches[2], 16), + b = parseInt(matches[3], 16), + a = parseInt(matches[4], 16) / 255; + + return { + r: clampRGB(r), + g: clampRGB(g), + b: clampRGB(b), + a: clampAlpha(a), + }; +} +/** - Convert any `HEX` color to an `HSVA` object representation */ +function HEX_HSVA(color: string | number): hsvaT { + 'worklet'; + const rgb = HEX_RGBA(color); + const hsva = RGB_HSVA(rgb); + return hsva; +} +/** - Convert any `HEX` color to an `HSLA` object representation */ +function HEX_HSLA(color: string): hslaT { + 'worklet'; + const rgb = HEX_RGBA(color); + return RGB_HSLA(rgb); +} +/** - Convert any `HEX` color to an `HWBA` object representation */ +function HEX_HWBA(color: string): hwbaT { + 'worklet'; + const rgba = HEX_RGBA(color); + return RGB_HWBA(rgba); +} + +// * HSV +/** - Parse `HSV` or `HSVA` color string to an `object` */ +function HSV_Parse_String(color: string): hsvaT { + 'worklet'; + color = color.trim().toLowerCase(); + const colorType = detectColorFormat(color); + + if (!colorType || !colorType.includes('hsv')) { + console.error( + '[colorKit.getHsvObject] is unable to parse the string into an `HSV` object. As a result, the color "black" will be returned instead.' + ); + return { h: 0, s: 0, v: 0, a: 1 }; + } + + let matches: RegExpMatchArray | null = null; + const entry = colorsRegex[colorType as 'hsv' | 'hsva']; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) { + if (entry[i].test(color)) matches = color.match(entry[i]); + } + } else { + matches = color.match(entry); + } + + if (!matches || matches.length < 4) { + console.error( + '[colorKit.getHsvObject] An error occurred while attempting to destructuring `HSV` values from the given string. As a result, the color "black" will be returned instead.' + ); + return { h: 0, s: 0, v: 0, a: 1 }; + } + + const h = parseInt(matches[1], 10), + s = parseInt(matches[2], 10), + v = parseInt(matches[3], 10), + a = parseFloat(matches[4] ?? '1'); + + return { + h: clampHue(h), + s: clamp100(s), + v: clamp100(v), + a: clampAlpha(a), + }; +} +/** - Ensure that the `HSV` object values are within the correct range and that it has the alpha channel */ +function HSV_Normalize_Object(color: hsvaT | hsvT): hsvaT { + 'worklet'; + return { + h: clampHue(color.h), + s: clamp100(color.s), + v: clamp100(color.v), + a: clampAlpha((color as hsvaT).a ?? 1), + }; +} +/** - Convert `HSV` color to an `RGBA` object representation */ +function HSV_RGBA(color: hsvaT | hsvT | string): rgbaT { + 'worklet'; + const hsva = typeof color === 'string' ? HSV_Parse_String(color) : HSV_Normalize_Object(color); + + const h = hsva.h / 360, + s = hsva.s / 100, + v = hsva.v / 100, + a = hsva.a; + + const i = Math.floor(h * 6), + f = h * 6 - i, + p = v * (1 - s), + q = v * (1 - f * s), + t = v * (1 - (1 - f) * s); + + let r = 0, + g = 0, + b = 0; + + if (i % 6 === 0) { + r = v; + g = t; + b = p; + } else if (i % 6 === 1) { + r = q; + g = v; + b = p; + } else if (i % 6 === 2) { + r = p; + g = v; + b = t; + } else if (i % 6 === 3) { + r = p; + g = q; + b = v; + } else if (i % 6 === 4) { + r = t; + g = p; + b = v; + } else if (i % 6 === 5) { + r = v; + g = p; + b = q; + } + + return { + r: clampRGB(r * 255), + g: clampRGB(g * 255), + b: clampRGB(b * 255), + a: clampAlpha(a), + }; +} +/** - Convert `HSV` color to an `HSLA` object representation */ +function HSV_HSLA(color: hsvaT | hsvT | string): hslaT { + 'worklet'; + const hsva = typeof color === 'string' ? HSV_Parse_String(color) : HSV_Normalize_Object(color); + + const h = hsva.h, + s = hsva.s / 100, + v = hsva.v / 100, + a = hsva.a; + + const l = ((2 - s) * v) / 2, + sl = s * v, + sln = l !== 0 && l !== 1 ? sl / (l < 0.5 ? l * 2 : 2 - l * 2) : sl; + + return { + h: clampHue(h), + s: clamp100(sln * 100), + l: clamp100(l * 100), + a: clampAlpha(a), + }; +} +/** - Convert `HSV` color to an `Hex` color */ +function HSV_HEX(color: hsvaT | hsvT | string): string { + 'worklet'; + const rgba = HSV_RGBA(color); + const hex = RGB_HEX(rgba); + return hex; +} +/** - Convert `HSV` color to an `HWBA` object representation */ +function HSV_HWBA(color: hsvaT | hsvT | string): hwbaT { + 'worklet'; + const { h, s, v, a } = typeof color === 'string' ? HSV_Parse_String(color) : HSV_Normalize_Object(color); + + const w = (1 - s / 100) * v, + b = (1 - v / 100) * 100; + + return { + h: clampHue(h), + w: clamp100(w), + b: clamp100(b), + a: clampAlpha(a), + }; +} +/** - Return the `HSV` color as a string, an array, or an object */ +function HSV_Return_Types({ h, s, v, a }: hsvaT) { + 'worklet'; + return { + string: (forceAlpha?: boolean) => { + 'worklet'; + // auto + if (typeof forceAlpha === 'undefined') { + if (typeof a === 'number' && a !== 1) return `hsva(${h}, ${s}%, ${v}%, ${a})`; + return `hsv(${h}, ${s}%, ${v}%)`; + } + + if (forceAlpha) return `hsva(${h}, ${s}%, ${v}%, ${a ?? 1})`; + + return `hsv(${h}, ${s}%, ${v}%)`; + }, + array: () => { + 'worklet'; + return [h, s, v, a]; + }, + object: () => { + 'worklet'; + return { h, s, v, a }; + }, + }; +} + +// * HWB +/** - Parse `HWB` or `HWBA` color strong to an `object` */ +function HWB_Parse_String(color: string): hwbaT { + 'worklet'; + color = color.trim().toLowerCase(); + const colorType = detectColorFormat(color); + + if (!colorType || !colorType.includes('hwb')) { + console.error( + '[colorKit.getHwbObject] is unable to parse the string into an `HWB` object. As a result, the color "black" will be returned instead.' + ); + return { h: 0, w: 0, b: 0, a: 1 }; + } + + let matches: RegExpMatchArray | null = null; + const entry = colorsRegex[colorType as 'hwb' | 'hwba']; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) { + if (entry[i].test(color)) matches = color.match(entry[i]); + } + } else { + matches = color.match(entry); + } + + if (!matches || matches.length < 4) { + console.error( + '[colorKit.getHwbObject] An error occurred while attempting to destructuring `HWB` values from the given string. As a result, the color "black" will be returned instead.' + ); + return { h: 0, w: 0, b: 0, a: 1 }; + } + + const h = parseInt(matches[1], 10), + w = parseInt(matches[2], 10), + b = parseInt(matches[3], 10), + a = parseFloat(matches[4] ?? '1'); + + return { + h: clampHue(h), + w: clamp100(w), + b: clamp100(b), + a: clampAlpha(a), + }; +} +/** - Ensure that the `HWB` object values are within the correct range and that it has the alpha channel */ +function HWB_Normalize_Object(color: hwbaT | hwbT): hwbaT { + 'worklet'; + return { + h: clampHue(color.h), + w: clamp100(color.w), + b: clamp100(color.b), + a: clampAlpha((color as hwbaT).a ?? 1), + }; +} +/** - Convert `HWB` or `HWBA` color to an `RGBA` object representation */ +function HWB_RGBA(color: hwbaT | hwbT | string): rgbaT { + 'worklet'; + const hwba = typeof color === 'string' ? HWB_Parse_String(color) : HWB_Normalize_Object(color); + + const h = hwba.h / 360, + w = hwba.w / 100, + b = hwba.b / 100, + a = hwba.a; + + if (w + b >= 1) { + const gray = clampRGB((w * 255) / (w + b)); + return { + r: gray, + g: gray, + b: gray, + a, + }; + } + + const red = calculateHueValue(0, 1, h + 1 / 3) * (1 - w - b) + w, + green = calculateHueValue(0, 1, h) * (1 - w - b) + w, + blue = calculateHueValue(0, 1, h - 1 / 3) * (1 - w - b) + w; + + return { + r: clampRGB(red * 255), + g: clampRGB(green * 255), + b: clampRGB(blue * 255), + a: clampAlpha(a), + }; +} +/** - Convert `HWB` or `HWBA` color to an `Hex` color */ +function HWB_HEX(color: hwbaT | hwbT | string): string { + 'worklet'; + const rgba = HWB_RGBA(color); + return RGB_HEX(rgba); +} +/** - Convert `HWB` or `HWBA` color to an `HSVA` object representation */ +function HWB_HSVA(color: hwbaT | hwbT | string): hsvaT { + 'worklet'; + const hwba = typeof color === 'string' ? HWB_Parse_String(color) : HWB_Normalize_Object(color); + + const h = hwba.h % 360, + w = hwba.w / 100, + b = hwba.b / 100, + a = hwba.a; + + const v = (1 - b) * 100; + let s = (1 - w / (v / 100)) * 100; + s = isNaN(s) ? 0 : s; + + return { + h: clampHue(h), + s: clamp100(s), + v: clamp100(v), + a: clampAlpha(a), + }; +} +/** - Convert `HWB` or `HWBA` color to an `HSLA` object representation */ +function HWB_HSLA(color: hwbaT | hwbT | string): hslaT { + 'worklet'; + const hsva = HWB_HSVA(color); + return HSV_HSLA(hsva); +} +/** - Return the `HWB` color as a string, an array, or an object */ +function HWB_Return_Types({ h, w, b, a }: hwbaT) { + 'worklet'; + return { + string: (forceAlpha?: boolean) => { + 'worklet'; + // auto + if (typeof forceAlpha === 'undefined') { + if (typeof a === 'number' && a !== 1) return `hwba(${h}, ${w}%, ${b}%, ${a})`; + return `hwb(${h}, ${w}%, ${b}%)`; + } + + if (forceAlpha) return `hwba(${h}, ${w}%, ${b}%, ${a ?? 1})`; + + return `hwb(${h}, ${w}%, ${b}%)`; + }, + array: () => { + 'worklet'; + return [h, w, b, a]; + }, + object: () => { + 'worklet'; + return { h, w, b, a }; + }, + }; +} + +// * HSL +/** - Parse `HSL` or `HSLA` color string to an `object` */ +function HSL_Parse_String(color: string): hslaT { + 'worklet'; + color = color.trim().toLowerCase(); + const colorType = detectColorFormat(color); + + if (!colorType || !colorType.includes('hsl')) { + console.error( + '[colorKit.getHslObject] is unable to parse the string into an `HSL` object. As a result, the color "black" will be returned instead.' + ); + return { h: 0, s: 0, l: 0, a: 1 }; + } + + let matches: RegExpMatchArray | null = null; + const entry = colorsRegex[colorType as 'hsl' | 'hsla']; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) { + if (entry[i].test(color)) matches = color.match(entry[i]); + } + } else { + matches = color.match(entry); + } + + if (!matches || matches.length < 3) { + console.error( + '[colorKit.getHslObject] An error occurred while attempting to destructuring `HSL` values from the given string. As a result, the color "black" will be returned instead.' + ); + return { h: 0, s: 0, l: 0, a: 1 }; + } + + const h = parseInt(matches[1], 10), + s = parseInt(matches[2], 10), + l = parseInt(matches[3], 10), + a = parseFloat(matches[4] ?? '1'); + + return { + h: clampHue(h), + s: clamp100(s), + l: clamp100(l), + a: clampAlpha(a), + }; +} +/** - Ensure that the `HSL` object values are within the correct range and that it has the alpha channel */ +function HSL_Normalize_Object(color: hslaT | hslT): hslaT { + 'worklet'; + return { + h: clampHue(color.h), + s: clamp100(color.s), + l: clamp100(color.l), + a: clampAlpha((color as hslaT).a ?? 1), + }; +} +/** - Convert `HSL` or `HSLA` color to an `RGBA` object representation */ +function HSL_RGBA(color: string | hslaT | hslT): rgbaT { + 'worklet'; + const hsla = typeof color === 'string' ? HSL_Parse_String(color) : HSL_Normalize_Object(color); + + const h = hsla.h / 360, + s = hsla.s / 100, + l = hsla.l / 100, + a = hsla.a; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s, + p = 2 * l - q; + + const r = calculateHueValue(p, q, h + 1 / 3), + g = calculateHueValue(p, q, h), + b = calculateHueValue(p, q, h - 1 / 3); + + return { + r: clampRGB(r * 255), + g: clampRGB(g * 255), + b: clampRGB(b * 255), + a: clampAlpha(a), + }; +} +/** - Convert `HSL` or `HSLA` color to `HEX` color */ +function HSL_HEX(color: string | hslaT | hslT): string { + 'worklet'; + + const hsla = typeof color === 'string' ? HSL_Parse_String(color) : HSL_Normalize_Object(color); + const rgb = HSL_RGBA(hsla); + + const r = numberToHexString(rgb.r), + g = numberToHexString(rgb.g), + b = numberToHexString(rgb.b), + a = rgb.a === 1 ? '' : numberToHexString(rgb.a * 255); + + return `#${r + g + b + a}`; +} +/** - Convert `HSL` or `HSLA` color to an `HSVA` object representation */ +function HSL_HSVA(color: string | hslaT | hslT): hsvaT { + 'worklet'; + const hsla = typeof color === 'string' ? HSL_Parse_String(color) : HSL_Normalize_Object(color); + const h = hsla.h; + + const s = hsla.s / 100, + l = hsla.l / 100, + a = (hsla as hslaT).a ?? 1, + v = l + s * Math.min(l, 1 - l), + sNew = v === 0 ? 0 : 2 - (2 * l) / v; + + return { + h: clampHue(h), + s: clamp100(sNew * 100), + v: clamp100(v * 100), + a: clampAlpha(a), + }; +} +/** - Convert `HSL` or `HSLA` color to an `HWBA` object representation */ +function HSL_HWBA(color: string | hslaT | hslT): hwbaT { + 'worklet'; + const hsva = HSL_HSVA(color); + return HSV_HWBA(hsva); +} +/** - Return the `HSL` color as a string, an array, or an object */ +function HSL_Return_Types({ h, s, l, a }: hslaT) { + 'worklet'; + return { + string: (forceAlpha?: boolean) => { + 'worklet'; + // auto + if (typeof forceAlpha === 'undefined') { + if (typeof a === 'number' && a !== 1) return `hsla(${h}, ${s}%, ${l}%, ${a})`; + return `hsl(${h}, ${s}%, ${l}%)`; + } + + if (forceAlpha) return `hsla(${h}, ${s}%, ${l}%, ${a ?? 1})`; + + return `hsl(${h}, ${s}%, ${l}%)`; + }, + array: () => { + 'worklet'; + return [h, s, l, a]; + }, + object: () => { + 'worklet'; + return { h, s, l, a }; + }, + }; +} + +/** - Convert `HSL`, `HSV`, `HWB`, or `RGB` color to the `HEX` color format. */ +export function HEX(color: SupportedColorFormats): string { + 'worklet'; + // named color + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + + if (namedColors.hasOwnProperty(color)) { + color = namedColors[color as keyof typeof namedColors] as string; + } + } + + const colorType = detectColorFormat(color); + + // RGB to HEX + if (colorType === 'rgb' || colorType === 'rgba') { + return RGB_HEX(color as string | rgbaT); + } + + // HSL to HEX + if (colorType === 'hsl' || colorType === 'hsla') { + return HSL_HEX(color as string | hslaT | hslT); + } + + // HSV to HEX + if (colorType === 'hsv' || colorType === 'hsva') { + return HSV_HEX(color as hsvaT | hsvT); + } + + // HWB to HEX + if (colorType === 'hwb' || colorType === 'hwba') { + return HWB_HEX(color as hwbaT | hwbT); + } + + // HEX + if (colorType?.includes('hex')) { + return HEX_Normalize(color as string | number); + } + + // ! error + console.error( + '[colorKit.HEX] An error occurred while attempting to convert the provided parameter into an `HEX` color. As a result, the default color "black" will be used instead.' + ); + + return '#000000'; +} + +/** - Convert `HSL`, `HSV`, `HWB`, or `HEX` color to the `RGB` color format. */ +export function RGB(color: SupportedColorFormats) { + 'worklet'; + // named color + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + + if (namedColors.hasOwnProperty(color)) { + color = namedColors[color as keyof typeof namedColors] as string; + } + } + + const colorType = detectColorFormat(color); + + // HEX to RGB + if (colorType?.includes('hex')) { + const rgb = HEX_RGBA(color as string | number); + return RGB_Return_Types(rgb); + } + + // HSL to RGB + if (colorType === 'hsl' || colorType === 'hsla') { + const rgb = HSL_RGBA(color as hslaT | hslT); + return RGB_Return_Types(rgb); + } + + // HSV to RGB + if (colorType === 'hsv' || colorType === 'hsva') { + const rgb = HSV_RGBA(color as hsvaT | hsvT); + return RGB_Return_Types(rgb); + } + + // HWB to RGB + if (colorType === 'hwb' || colorType === 'hwba') { + const rgb = HWB_RGBA(color as hwbaT | hwbT); + return RGB_Return_Types(rgb); + } + + // RGB to normalized RGB + if (colorType === 'rgb' || colorType === 'rgba') { + const rgba = typeof color === 'string' ? RGB_Parse_String(color) : RGB_Normalize_Object(color as rgbaT | rgbT); + return RGB_Return_Types(rgba); + } + + // ! error + console.error( + '[colorKit.RGB] An error occurred while attempting to convert the provided parameter into an `RGB` color. As a result, the default color "black" will be used instead.' + ); + + return RGB_Return_Types({ r: 0, g: 0, b: 0, a: 1 }); +} + +/** - Convert `HEX`, `HSV`, `HWB`, or `RGB` color to the `HSL` color format. */ +export function HSL(color: SupportedColorFormats) { + 'worklet'; + // named color + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + + if (namedColors.hasOwnProperty(color)) { + color = namedColors[color as keyof typeof namedColors] as string; + } + } + + const colorType = detectColorFormat(color); + + // HEX to HSL + if (colorType?.includes('hex')) { + const hsla = HEX_HSLA(color as string); + return HSL_Return_Types(hsla); + } + + // RGB to HSL + if (colorType === 'rgb' || colorType === 'rgba') { + const hsla = RGB_HSLA(color as rgbaT | rgbT); + return HSL_Return_Types(hsla); + } + + // HSV to HSL + if (colorType === 'hsv' || colorType === 'hsva') { + const hsla = HSV_HSLA(color as hsvaT | hsvT); + return HSL_Return_Types(hsla); + } + + // HWB to HSL + if (colorType === 'hwb' || colorType === 'hwba') { + const hsla = HWB_HSLA(color as hwbaT | hwbT); + return HSL_Return_Types(hsla); + } + + // HSL to normalized HSL + if (colorType === 'hsl' || colorType === 'hsla') { + const hsla = typeof color === 'string' ? HSL_Parse_String(color) : HSL_Normalize_Object(color as hslaT | hslT); + return HSL_Return_Types(hsla); + } + + // ! error + console.error( + '[colorKit.HSL] An error occurred while attempting to convert the provided parameter into an `HSL` color. As a result, the default color "black" will be used instead.' + ); + + return HSL_Return_Types({ h: 0, s: 0, l: 0, a: 1 }); +} + +/** - Convert `HSL`, `HEX`, `HSV`, or `RGB` color to the `HWB` color format. */ +export function HWB(color: SupportedColorFormats) { + 'worklet'; + // named color + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + + if (namedColors.hasOwnProperty(color)) { + color = namedColors[color as keyof typeof namedColors] as string; + } + } + + const colorType = detectColorFormat(color); + + // HEX to HWB + if (colorType?.includes('hex')) { + const hwba = HEX_HWBA(color as string); + return HWB_Return_Types(hwba); + } + + // RGB to HWB + if (colorType === 'rgb' || colorType === 'rgba') { + const hwba = RGB_HWBA(color as rgbaT | rgbT); + return HWB_Return_Types(hwba); + } + + // HSL to HWB + if (colorType === 'hsl' || colorType === 'hsla') { + const hwba = HSL_HWBA(color as hslaT | hslT); + return HWB_Return_Types(hwba); + } + + // HSV to HWB + if (colorType === 'hsv' || colorType === 'hsva') { + const hwba = HSV_HWBA(color as hsvaT | hsvT); + return HWB_Return_Types(hwba); + } + + // HWB to normalized HWB + if (colorType === 'hwb' || colorType === 'hwba') { + const hwba = typeof color === 'string' ? HWB_Parse_String(color) : HWB_Normalize_Object(color as hwbaT | hwbT); + return HWB_Return_Types(hwba); + } + + // ! error + console.error( + '[colorKit.HWB] An error occurred while attempting to convert the provided parameter into an `HWB` color. As a result, the default color "black" will be used instead.' + ); + + return HWB_Return_Types({ h: 0, w: 0, b: 100, a: 1 }); +} + +/** - Convert `HSL`, `HEX`, `HWB`, or `RGB` color to the `HSV` color format. */ +export function HSV(color: SupportedColorFormats) { + 'worklet'; + // named color + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + + if (namedColors.hasOwnProperty(color)) { + color = namedColors[color as keyof typeof namedColors] as string; + } + } + + const colorType = detectColorFormat(color); + + // HEX to HSV + if (colorType?.includes('hex')) { + const hsva = HEX_HSVA(color as string); + return HSV_Return_Types(hsva); + } + + // RGB to HSV + if (colorType === 'rgb' || colorType === 'rgba') { + const hsva = RGB_HSVA(color as rgbaT | rgbT); + return HSV_Return_Types(hsva); + } + + // HSL to HSV + if (colorType === 'hsl' || colorType === 'hsla') { + const hsva = HSL_HSVA(color as hslaT | hslT); + return HSV_Return_Types(hsva); + } + + // HWB to HSV + if (colorType === 'hwb' || colorType === 'hwba') { + const hsva = HWB_HSVA(color as hwbaT | hwbT); + return HSV_Return_Types(hsva); + } + + // HSV to normalized HSV + if (colorType === 'hsv' || colorType === 'hsva') { + const hsva = typeof color === 'string' ? HSV_Parse_String(color) : HSV_Normalize_Object(color as hsvaT | hsvT); + return HSV_Return_Types(hsva); + } + + // ! error + console.error( + '[colorKit.HSV] An error occurred while attempting to convert the provided parameter into an `HSV` color. As a result, the default color "black" will be used instead.' + ); + + return HSV_Return_Types({ h: 0, s: 0, v: 0, a: 1 }); +} diff --git a/src/colorKit/colorInformation.ts b/src/colorKit/colorInformation.ts new file mode 100644 index 0000000..407453b --- /dev/null +++ b/src/colorKit/colorInformation.ts @@ -0,0 +1,170 @@ +import { HSL, HSV, RGB } from './colorConversion'; +import colorsRegex from './colorsRegex'; +import namedColors from './namedColors'; + +import type { ColorFormats, hslaT, hslT, hsvaT, hsvT, hwbaT, hwbT, rgbaT, rgbT, SupportedColorFormats } from './types'; + +/** - Identify the color format of a given `string` or `object`, and return `null` for invalid colors. */ +export function getFormat(color: SupportedColorFormats): ColorFormats | 'named' | null { + 'worklet'; + // color int + if (typeof color === 'number') { + if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) return 'hex8'; + return null; + } + + // color string + if (typeof color === 'string') { + color = color.trim().toLowerCase(); + if (namedColors.hasOwnProperty(color)) return 'named'; + + for (const key in colorsRegex) { + const format = key as ColorFormats; + const entry = colorsRegex[format]; + if (Array.isArray(entry)) { + for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) return format; + continue; + } + if (entry.test(color)) return format; + } + } + + // color object + if (typeof color === 'object') { + const rgbaKeys = ['r', 'g', 'b', 'a'] as (keyof rgbaT)[]; + const isRgbaOb = rgbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbaT)[k] === 'number'); + if (isRgbaOb) return 'rgba'; + + const rgbKeys = ['r', 'g', 'b'] as (keyof rgbT)[]; + const isRgbOb = rgbKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbT)[k] === 'number'); + if (isRgbOb) return 'rgb'; + + const hslaKeys = ['h', 's', 'l', 'a'] as (keyof hslaT)[]; + const isHslaOb = hslaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslaT)[k] === 'number'); + if (isHslaOb) return 'hsla'; + + const hslKeys = ['h', 's', 'l'] as (keyof hslT)[]; + const isHslOb = hslKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslT)[k] === 'number'); + if (isHslOb) return 'hsl'; + + const hsvaKeys = ['h', 's', 'v', 'a'] as (keyof hsvaT)[]; + const isHsvaOb = hsvaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvaT)[k] === 'number'); + if (isHsvaOb) return 'hsva'; + + const hsvKeys = ['h', 's', 'v'] as (keyof hsvT)[]; + const isHsvOb = hsvKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvT)[k] === 'number'); + if (isHsvOb) return 'hsv'; + + const hwbaKeys = ['h', 'w', 'b', 'a'] as (keyof hwbaT)[]; + const isHwbaOb = hwbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbaT)[k] === 'number'); + if (isHwbaOb) return 'hwba'; + + const hwbKeys = ['h', 'w', 'b'] as (keyof hwbT)[]; + const isHwbOb = hwbKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbT)[k] === 'number'); + if (isHwbOb) return 'hwb'; + } + + return null; +} + +/** - Get the `red` channel value of a given color. */ +export function getRed(color: SupportedColorFormats): number { + 'worklet'; + const { r } = RGB(color).object(); + return r; +} + +/** - Get the `green` channel value of a given color. */ +export function getGreen(color: SupportedColorFormats): number { + 'worklet'; + const { g } = RGB(color).object(); + return g; +} + +/** - Get the `blue` channel value of a given color. */ +export function getBlue(color: SupportedColorFormats): number { + 'worklet'; + const { b } = RGB(color).object(); + return b; +} + +/** - Get the `hue` channel value of a given color. */ +export function getHue(color: SupportedColorFormats): number { + 'worklet'; + const { h } = HSL(color).object(); + return h; +} + +/** - Get the `saturation` value of a given color. */ +export function getSaturation(color: SupportedColorFormats): number { + 'worklet'; + const { s } = HSL(color).object(); + return s; +} + +/** + * - Get color's HSL `luminosity` channel value. + * - If you want the overall `luminosity` of a color use `getLuminanceWCAG` method. + */ +export function getLuminance(color: SupportedColorFormats): number { + 'worklet'; + const { l } = HSL(color).object(); + return l; +} + +/** - Get the HSV's `value` (brightness) channel value of a given color. */ +export function getBrightness(color: SupportedColorFormats): number { + 'worklet'; + const { v } = HSV(color).object(); + return v; +} + +/** - Returns the perceived `luminance` of a color, from `0-1` as defined by Web Content Accessibility Guidelines (Version 2.0). */ +export function getLuminanceWCAG(color: SupportedColorFormats): number { + 'worklet'; + const { r, g, b } = RGB(color).object(); + const a = [r, g, b].map(v => (v / 255 <= 0.03928 ? v / 255 / 12.92 : Math.pow((v / 255 + 0.055) / 1.055, 2.4))); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; +} + +/** - Returns a boolean indicating whether the color is considered "dark" or not */ +export function isDark(color: SupportedColorFormats): boolean { + 'worklet'; + const luminance = getLuminanceWCAG(color); + return luminance < 0.5; +} + +/** - Returns a boolean indicating whether the color is considered "light" or not */ +export function isLight(color: SupportedColorFormats): boolean { + 'worklet'; + const luminance = getLuminanceWCAG(color); + return luminance >= 0.5; +} + +/** + * - Check if two colors are similar within a specified tolerance. + * @example + * const tolerance = 0; + * const isEqual = colorKit.areColorsEqual("#f00", "red", tolerance); // true + */ +export function areColorsEqual(color1: SupportedColorFormats, color2: SupportedColorFormats, tolerance = 0): boolean { + 'worklet'; + const rgb1 = RGB(color1).object(); + const rgb2 = RGB(color2).object(); + + const deltaR = rgb1.r - rgb2.r; + const deltaG = rgb1.g - rgb2.g; + const deltaB = rgb1.b - rgb2.b; + const difference = Math.sqrt(deltaR * deltaR + deltaG * deltaG + deltaB * deltaB); + + return difference <= tolerance; +} + +/** - Calculates the contrast ratio between two colors, useful for ensuring accessibility and readability. */ +export function contrastRatio(color1: SupportedColorFormats, color2: SupportedColorFormats): number { + 'worklet'; + const luminance1 = getLuminanceWCAG(color1); + const luminance2 = getLuminanceWCAG(color2); + const contrast = (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05); + return Math.round(contrast * 100) / 100; +} diff --git a/src/colorKit/colorKit.ts b/src/colorKit/colorKit.ts deleted file mode 100644 index 1fe53fe..0000000 --- a/src/colorKit/colorKit.ts +++ /dev/null @@ -1,1194 +0,0 @@ -import colorsRegex from './colorsRegex'; -import namedColors from './namedColors'; -import PrivateMethods from './PrivateMethods'; -import { clamp100, clampAlpha, clampHue, clampRGB, randomNumber } from './utilities'; - -import type { - ColorFormats, - ConversionMethods, - hslaT, - hslT, - hsvaT, - hsvT, - hwbaT, - hwbT, - rgbaT, - rgbT, - SupportedColorFormats, -} from './types'; - -class Colors { - private _: InstanceType; - private returnColorObject: (color: SupportedColorFormats) => ConversionMethods; - - constructor() { - this._ = new PrivateMethods(); - - this.returnColorObject = (color: SupportedColorFormats) => { - return { - hex: () => this.HEX(color), - rgb: () => this.RGB(color), - hsl: () => this.HSL(color), - hsv: () => this.HSV(color), - hwb: () => this.HWB(color), - }; - }; - } - - /** - Identify the color format of a given `string` or `object`, and return `null` for invalid colors. */ - getFormat(color: SupportedColorFormats): ColorFormats | 'named' | null { - // color int - if (typeof color === 'number') color = '#' + color.toString(16); - - // color string - if (typeof color === 'string') { - color = color.trim().toLowerCase(); - if (namedColors.hasOwnProperty(color)) return 'named'; - - for (const key in colorsRegex) { - const format = key as ColorFormats; - const entry = colorsRegex[format]; - if (Array.isArray(entry)) { - for (let i = 0; i < entry.length; i++) if (entry[i].test(color)) return format; - continue; - } - if (entry.test(color)) return format; - } - } - - // color object - if (typeof color === 'object') { - const rgbaKeys = ['r', 'g', 'b', 'a'] as (keyof rgbaT)[]; - const isRgbaOb = rgbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbaT)[k] === 'number'); - if (isRgbaOb) return 'rgba'; - - const rgbKeys = ['r', 'g', 'b'] as (keyof rgbT)[]; - const isRgbOb = rgbKeys.every(k => color.hasOwnProperty(k) && typeof (color as rgbT)[k] === 'number'); - if (isRgbOb) return 'rgb'; - - const hslaKeys = ['h', 's', 'l', 'a'] as (keyof hslaT)[]; - const isHslaOb = hslaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslaT)[k] === 'number'); - if (isHslaOb) return 'hsla'; - - const hslKeys = ['h', 's', 'l'] as (keyof hslT)[]; - const isHslOb = hslKeys.every(k => color.hasOwnProperty(k) && typeof (color as hslT)[k] === 'number'); - if (isHslOb) return 'hsl'; - - const hsvaKeys = ['h', 's', 'v', 'a'] as (keyof hsvaT)[]; - const isHsvaOb = hsvaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvaT)[k] === 'number'); - if (isHsvaOb) return 'hsva'; - - const hsvKeys = ['h', 's', 'v'] as (keyof hsvT)[]; - const isHsvOb = hsvKeys.every(k => color.hasOwnProperty(k) && typeof (color as hsvT)[k] === 'number'); - if (isHsvOb) return 'hsv'; - - const hwbaKeys = ['h', 'w', 'b', 'a'] as (keyof hwbaT)[]; - const isHwbaOb = hwbaKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbaT)[k] === 'number'); - if (isHwbaOb) return 'hwba'; - - const hwbKeys = ['h', 'w', 'b'] as (keyof hwbT)[]; - const isHwbOb = hwbKeys.every(k => color.hasOwnProperty(k) && typeof (color as hwbT)[k] === 'number'); - if (isHwbOb) return 'hwb'; - } - - return null; - } - - // * RGB - /** - Convert `HSL`, `HSLA`, `HSV`, `HSVA` or `HEX` color to `RGBA` color format. */ - RGB(color: SupportedColorFormats) { - if (typeof color === 'string') color = color.trim().toLowerCase(); - - color = - typeof color === 'string' && namedColors.hasOwnProperty(color) - ? (namedColors[color as keyof typeof namedColors] as string) - : color; - - const colorType = this._.detectColorFormat(color); - - if (colorType?.includes('hex')) { - return { - string: (alpha?: boolean) => { - const { r, g, b, a } = this._.HEX_RGBA(color as string); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - - if (alpha) return `rgba(${r}, ${g}, ${b}, ${a ?? 1})`; - - return `rgb(${r}, ${g}, ${b})`; - }, - array: () => { - const { r, g, b, a } = this._.HEX_RGBA(color as string); - return [r, g, b, a]; - }, - object: () => this._.HEX_RGBA(color as string), - }; - } - - if (colorType === 'hsl' || colorType === 'hsla') { - return { - string: (alpha?: boolean) => { - const { r, g, b, a } = this._.HSL_RGBA(color as hslaT | hslT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - - if (alpha) return `rgba(${r}, ${g}, ${b}, ${a ?? 1})`; - - return `rgb(${r}, ${g}, ${b})`; - }, - array: () => { - const { r, g, b, a } = this._.HSL_RGBA(color as hslaT | hslT); - return [r, g, b, a]; - }, - object: () => this._.HSL_RGBA(color as hslaT | hslT), - }; - } - - if (colorType === 'hsv' || colorType === 'hsva') { - return { - string: (alpha?: boolean) => { - const { r, g, b, a } = this._.HSV_RGBA(color as hsvaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - - if (alpha) return `rgba(${r}, ${g}, ${b}, ${a ?? 1})`; - - return `rgb(${r}, ${g}, ${b})`; - }, - array: () => { - const { r, g, b, a } = this._.HSV_RGBA(color as hsvaT | hsvT); - return [r, g, b, a]; - }, - object: () => this._.HSV_RGBA(color as hsvaT | hsvT), - }; - } - - if (colorType === 'hwb' || colorType === 'hwba') { - return { - string: (alpha?: boolean) => { - const { r, g, b, a } = this._.HWB_RGBA(color as hwbaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - - if (alpha) return `rgba(${r}, ${g}, ${b}, ${a ?? 1})`; - - return `rgb(${r}, ${g}, ${b})`; - }, - array: () => { - const { r, g, b, a } = this._.HWB_RGBA(color as hwbaT | hwbT); - return [r, g, b, a]; - }, - object: () => this._.HWB_RGBA(color as hwbaT | hwbT), - }; - } - - if (colorType === 'rgb' || colorType === 'rgba') { - return { - string: (alpha?: boolean) => { - const rgba = typeof color === 'string' ? this._.getRgbObject(color) : (color as rgbaT), - r = clampRGB(rgba.r), - g = clampRGB(rgba.g), - b = clampRGB(rgba.b), - a = clampAlpha(rgba.a ?? 1); - - if (typeof alpha === 'undefined') return a !== 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - - if (alpha) return `rgba(${r}, ${g}, ${b}, ${a})`; - - return `rgb(${r}, ${g}, ${b})`; - }, - array: () => { - const rgba = typeof color === 'string' ? this._.getRgbObject(color) : (color as rgbaT), - r = clampRGB(rgba.r), - g = clampRGB(rgba.g), - b = clampRGB(rgba.b), - a = clampAlpha(rgba.a ?? 1); - - return [r, g, b, a]; - }, - object: () => { - const rgba = typeof color === 'string' ? this._.getRgbObject(color) : (color as rgbaT), - r = clampRGB(rgba.r), - g = clampRGB(rgba.g), - b = clampRGB(rgba.b), - a = clampAlpha(rgba.a ?? 1); - - return { r, g, b, a }; - }, - }; - } - - // ! error - console.error( - '[colorKit] An error occurred while attempting to convert the provided parameter into an `RGB` color. As a result, the default color "black" will be used instead.' - ); - - return { - string: (alpha?: boolean) => (alpha ? 'rgba(0, 0, 0, 1)' : 'rgb(0, 0, 0)'), - array: () => [0, 0, 0, 1], - object: () => ({ r: 0, g: 0, b: 0, a: 1 }), - }; - } - - // * HEX - /** - Convert `HSL`, `HSLA`, `HSV`, `HSVA`, `RGB` or `RGBA` color to `HEX` color format. */ - HEX(color: SupportedColorFormats): string { - if (typeof color === 'string') color = color.trim().toLowerCase(); - - color = - typeof color === 'string' && namedColors.hasOwnProperty(color) - ? (namedColors[color as keyof typeof namedColors] as string) - : color; - - const colorType = this._.detectColorFormat(color); - - if (colorType === 'rgb' || colorType === 'rgba') { - return this._.RGB_HEX(color as string | rgbaT); - } - - if (colorType === 'hsl' || colorType === 'hsla') { - return this._.HSL_HEX(color as string | hslaT | hslT); - } - - if (colorType === 'hsv' || colorType === 'hsva') { - return this._.HSV_HEX(color as hsvaT | hsvT); - } - - if (colorType === 'hwb' || colorType === 'hwba') { - return this._.HWB_HEX(color as hwbaT | hwbT); - } - - if (colorType?.includes('hex')) return color as string; - - // ! error - console.error( - '[colorKit] An error occurred while attempting to convert the provided parameter into an `HEX` color. As a result, the default color "black" will be used instead.' - ); - - return '#000000'; - } - - // * HSL - /** - Convert `HEX`, `HSV`, `HSVA`, `RGB` or `RGBA` color to `HSLA` color format. */ - HSL(color: SupportedColorFormats) { - if (typeof color === 'string') color = color.trim().toLowerCase(); - - color = - typeof color === 'string' && namedColors.hasOwnProperty(color) - ? (namedColors[color as keyof typeof namedColors] as string) - : color; - - const colorType = this._.detectColorFormat(color); - - if (colorType?.includes('hex')) { - return { - string: (alpha?: boolean) => { - const { h, s, l, a } = this._.HEX_HSLA(color as string); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsla(${h}, ${s}%, ${l}%, ${a})` : `hsl(${h}, ${s}%, ${l}%)`; - - if (alpha) return `hsla(${h}, ${s}%, ${l}%, ${a ?? 1})`; - - return `hsl(${h}, ${s}%, ${l}%)`; - }, - array: () => { - const { h, s, l, a } = this._.HEX_HSLA(color as string); - return [h, s, l, a]; - }, - object: () => this._.HEX_HSLA(color as string), - }; - } - - if (colorType === 'rgb' || colorType === 'rgba') { - return { - string: (alpha?: boolean) => { - const { h, s, l, a } = this._.RGB_HSLA(color as rgbaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsla(${h}, ${s}%, ${l}%, ${a})` : `hsl(${h}, ${s}%, ${l}%)`; - - if (alpha) return `hsla(${h}, ${s}%, ${l}%, ${a ?? 1})`; - - return `hsl(${h}, ${s}%, ${l}%)`; - }, - array: () => { - const { h, s, l, a } = this._.RGB_HSLA(color as rgbaT); - return [h, s, l, a]; - }, - object: () => this._.RGB_HSLA(color as rgbaT | rgbT), - }; - } - - if (colorType === 'hsv' || colorType === 'hsva') { - return { - string: (alpha?: boolean) => { - const { h, s, l, a } = this._.HSV_HSLA(color as hsvaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsla(${h}, ${s}%, ${l}%, ${a})` : `hsl(${h}, ${s}%, ${l}%)`; - - if (alpha) return `hsla(${h}, ${s}%, ${l}%, ${a ?? 1})`; - - return `hsl(${h}, ${s}%, ${l}%)`; - }, - array: () => { - const { h, s, l, a } = this._.HSV_HSLA(color as hsvaT | hsvT); - return [h, s, l, a]; - }, - object: () => this._.HSV_HSLA(color as hsvaT | hsvT), - }; - } - - if (colorType === 'hwb' || colorType === 'hwba') { - return { - string: (alpha?: boolean) => { - const { h, s, l, a } = this._.HWB_HSLA(color as hwbaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsla(${h}, ${s}%, ${l}%, ${a})` : `hsl(${h}, ${s}%, ${l}%)`; - - if (alpha) return `hsla(${h}, ${s}%, ${l}%, ${a ?? 1})`; - - return `hsl(${h}, ${s}%, ${l}%)`; - }, - array: () => { - const { h, s, l, a } = this._.HWB_HSLA(color as hwbaT | hwbT); - return [h, s, l, a]; - }, - object: () => this._.HWB_HSLA(color as hwbaT | hwbT), - }; - } - - if (colorType === 'hsl' || colorType === 'hsla') { - return { - string: (alpha?: boolean) => { - const hsla = typeof color === 'string' ? this._.getHslObject(color) : (color as hslaT), - h = clampHue(hsla.h), - s = clamp100(hsla.s), - l = clamp100(hsla.l), - a = clampAlpha(hsla.a ?? 1); - - if (typeof alpha === 'undefined') return a !== 1 ? `hsla(${h}, ${s}%, ${l}%, ${a})` : `hsl(${h}, ${s}%, ${l}%)`; - - if (alpha) return `hsla(${h}, ${s}%, ${l}%, ${a})`; - - return `hsl(${h}, ${s}%, ${l}%)`; - }, - array: () => { - const hsla = typeof color === 'string' ? this._.getHslObject(color) : (color as hslaT), - h = clampHue(hsla.h), - s = clamp100(hsla.s), - l = clamp100(hsla.l), - a = clampAlpha(hsla.a ?? 1); - - return [h, s, l, a]; - }, - object: () => { - const hsla = typeof color === 'string' ? this._.getHslObject(color) : (color as hslaT), - h = clampHue(hsla.h), - s = clamp100(hsla.s), - l = clamp100(hsla.l), - a = clampAlpha(hsla.a ?? 1); - - return { h, s, l, a }; - }, - }; - } - - // ! error - console.error( - '[colorKit] An error occurred while attempting to convert the provided parameter into an `HSL` color. As a result, the default color "black" will be used instead.' - ); - - return { - string: (alpha?: boolean) => (alpha ? 'hsla(0, 0%, 0%, 1)' : 'hsl(0, 0%, 0%)'), - array: () => [0, 0, 0, 1], - object: () => ({ h: 0, s: 0, l: 0, a: 1 }), - }; - } - - // * HSV - /** - Convert `HSL`, `HSLA`, `HEX`, `RGB` or `RGBA` color to `HSVA` color format. */ - HSV(color: SupportedColorFormats) { - if (typeof color === 'string') color = color.trim().toLowerCase(); - - color = - typeof color === 'string' && namedColors.hasOwnProperty(color) - ? (namedColors[color as keyof typeof namedColors] as string) - : color; - - const colorType = this._.detectColorFormat(color); - - if (colorType?.includes('hex')) { - return { - string: (alpha?: boolean) => { - const { h, s, v, a } = this._.HEX_HSVA(color as string); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsva(${h}, ${s}%, ${v}%, ${a})` : `hsv(${h}, ${s}%, ${v}%)`; - - if (alpha) return `hsva(${h}, ${s}%, ${v}%, ${a ?? 1})`; - - return `hsv(${h}, ${s}%, ${v}%)`; - }, - array: () => { - const { h, s, v, a } = this._.HEX_HSVA(color as string); - return [h, s, v, a]; - }, - object: () => this._.HEX_HSVA(color as string), - }; - } - - if (colorType === 'rgb' || colorType === 'rgba') { - return { - string: (alpha?: boolean) => { - const { h, s, v, a } = this._.RGB_HSVA(color as rgbaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsva(${h}, ${s}%, ${v}%, ${a})` : `hsv(${h}, ${s}%, ${v}%)`; - - if (alpha) return `hsva(${h}, ${s}%, ${v}%, ${a ?? 1})`; - - return `hsv(${h}, ${s}%, ${v}%)`; - }, - array: () => { - const { h, s, v, a } = this._.RGB_HSVA(color as rgbaT); - return [h, s, v, a]; - }, - object: () => this._.RGB_HSVA(color as rgbaT | rgbT), - }; - } - - if (colorType === 'hsl' || colorType === 'hsla') { - return { - string: (alpha?: boolean) => { - const { h, s, v, a } = this._.HSL_HSVA(color as hslaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsva(${h}, ${s}%, ${v}%, ${a})` : `hsv(${h}, ${s}%, ${v}%)`; - - if (alpha) return `hsva(${h}, ${s}%, ${v}%, ${a ?? 1})`; - - return `hsv(${h}, ${s}%, ${v}%)`; - }, - array: () => { - const { h, s, v, a } = this._.HSL_HSVA(color as hslaT | hslT); - return [h, s, v, a]; - }, - object: () => this._.HSL_HSVA(color as hslaT | hslT), - }; - } - - if (colorType === 'hwb' || colorType === 'hwba') { - return { - string: (alpha?: boolean) => { - const { h, s, v, a } = this._.HWB_HSVA(color as hwbaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hsva(${h}, ${s}%, ${v}%, ${a})` : `hsv(${h}, ${s}%, ${v}%)`; - - if (alpha) return `hsva(${h}, ${s}%, ${v}%, ${a ?? 1})`; - - return `hsv(${h}, ${s}%, ${v}%)`; - }, - array: () => { - const { h, s, v, a } = this._.HWB_HSVA(color as hwbaT | hwbT); - return [h, s, v, a]; - }, - object: () => this._.HWB_HSVA(color as hwbaT | hwbT), - }; - } - - if (colorType === 'hsv' || colorType === 'hsva') { - return { - string: (alpha?: boolean) => { - const hsva = typeof color === 'string' ? this._.getHsvObject(color) : (color as hsvaT), - h = clampHue(hsva.h), - s = clamp100(hsva.s), - v = clamp100(hsva.v), - a = clampAlpha(hsva.a ?? 1); - - if (typeof alpha === 'undefined') return a !== 1 ? `hsva(${h}, ${s}%, ${v}%, ${a})` : `hsv(${h}, ${s}%, ${v}%)`; - - if (alpha) return `hsva(${h}, ${s}%, ${v}%, ${a})`; - - return `hsv(${h}, ${s}%, ${v}%)`; - }, - array: () => { - const hsva = typeof color === 'string' ? this._.getHsvObject(color) : (color as hsvaT), - h = clampHue(hsva.h), - s = clamp100(hsva.s), - v = clamp100(hsva.v), - a = clampAlpha(hsva.a ?? 1); - - return [h, s, v, a]; - }, - object: () => { - const hsva = typeof color === 'string' ? this._.getHsvObject(color) : (color as hsvaT), - h = clampHue(hsva.h), - s = clamp100(hsva.s), - v = clamp100(hsva.v), - a = clampAlpha(hsva.a ?? 1); - - return { h, s, v, a }; - }, - }; - } - - // ! error - console.error( - '[colorKit] An error occurred while attempting to convert the provided parameter into an `HSV` color. As a result, the default color "black" will be used instead.' - ); - - return { - string: (alpha?: boolean) => (alpha ? 'hsva(0, 0%, 0%, 1)' : 'hsv(0, 0%, 0%)'), - array: () => [0, 0, 0, 1], - object: () => ({ h: 0, s: 0, v: 0, a: 1 }), - }; - } - - // * HWB - /** - Convert `HSL`, `HSLA`, `HEX`, `RGB` or `RGBA` color to `HWBA` color format. */ - HWB(color: SupportedColorFormats) { - if (typeof color === 'string') color = color.trim().toLowerCase(); - - color = - typeof color === 'string' && namedColors.hasOwnProperty(color) - ? (namedColors[color as keyof typeof namedColors] as string) - : color; - - const colorType = this._.detectColorFormat(color); - - if (colorType?.includes('hex')) { - return { - string: (alpha?: boolean) => { - const { h, w, b, a } = this._.HEX_HWBA(color as string); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hwba(${h}, ${w}%, ${b}%, ${a})` : `hwb(${h}, ${w}%, ${b}%)`; - - if (alpha) return `hwba(${h}, ${w}%, ${b}%, ${a ?? 1})`; - - return `hwb(${h}, ${w}%, ${b}%)`; - }, - array: () => { - const { h, w, b, a } = this._.HEX_HWBA(color as string); - return [h, w, b, a]; - }, - object: () => this._.HEX_HWBA(color as string), - }; - } - - if (colorType === 'rgb' || colorType === 'rgba') { - return { - string: (alpha?: boolean) => { - const { h, w, b, a } = this._.RGB_HWBA(color as rgbaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hwba(${h}, ${w}%, ${b}%, ${a})` : `hwb(${h}, ${w}%, ${b}%)`; - - if (alpha) return `hwba(${h}, ${w}%, ${b}%, ${a ?? 1})`; - - return `hwb(${h}, ${w}%, ${b}%)`; - }, - array: () => { - const { h, w, b, a } = this._.RGB_HWBA(color as rgbaT); - return [h, w, b, a]; - }, - object: () => this._.RGB_HWBA(color as rgbaT | rgbT), - }; - } - - if (colorType === 'hsl' || colorType === 'hsla') { - return { - string: (alpha?: boolean) => { - const { h, w, b, a } = this._.HSL_HWBA(color as hslaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hwba(${h}, ${w}%, ${b}%, ${a})` : `hwb(${h}, ${w}%, ${b}%)`; - - if (alpha) return `hwba(${h}, ${w}%, ${b}%, ${a ?? 1})`; - - return `hwb(${h}, ${w}%, ${b}%)`; - }, - array: () => { - const { h, w, b, a } = this._.HSL_HWBA(color as hslaT | hslT); - return [h, w, b, a]; - }, - object: () => this._.HSL_HWBA(color as hslaT | hslT), - }; - } - - if (colorType === 'hsv' || colorType === 'hsva') { - return { - string: (alpha?: boolean) => { - const { h, w, b, a } = this._.HSV_HWBA(color as hsvaT); - - if (typeof alpha === 'undefined') - return typeof a === 'number' && a !== 1 ? `hwba(${h}, ${w}%, ${b}%, ${a})` : `hwb(${h}, ${w}%, ${b}%)`; - - if (alpha) return `hwba(${h}, ${w}%, ${b}%, ${a ?? 1})`; - - return `hwb(${h}, ${w}%, ${b}%)`; - }, - array: () => { - const { h, w, b, a } = this._.HSV_HWBA(color as hsvaT | hsvT); - return [h, w, b, a]; - }, - object: () => this._.HSV_HWBA(color as hsvaT | hsvT), - }; - } - - if (colorType === 'hwb' || colorType === 'hwba') { - return { - string: (alpha?: boolean) => { - const hwba = typeof color === 'string' ? this._.getHwbObject(color) : (color as hwbaT), - h = clampHue(hwba.h), - w = clamp100(hwba.w), - b = clamp100(hwba.b), - a = clampAlpha(hwba.a ?? 1); - - if (typeof alpha === 'undefined') return a !== 1 ? `hwba(${h}, ${w}%, ${b}%, ${a})` : `hwb(${h}, ${w}%, ${b}%)`; - - if (alpha) return `hwba(${h}, ${w}%, ${b}%, ${a})`; - - return `hwb(${h}, ${w}%, ${b}%)`; - }, - array: () => { - const hwba = typeof color === 'string' ? this._.getHwbObject(color) : (color as hwbaT), - h = clampHue(hwba.h), - w = clamp100(hwba.w), - b = clamp100(hwba.b), - a = clampAlpha(hwba.a ?? 1); - - return [h, w, b, a]; - }, - object: () => { - const hwba = typeof color === 'string' ? this._.getHwbObject(color) : (color as hwbaT), - h = clampHue(hwba.h), - w = clamp100(hwba.w), - b = clamp100(hwba.b), - a = clampAlpha(hwba.a ?? 1); - - return { h, w, b, a }; - }, - }; - } - - // ! error - console.error( - '[colorKit] An error occurred while attempting to convert the provided parameter into an `HWB` color. As a result, the default color "black" will be used instead.' - ); - - return { - string: (alpha?: boolean) => (alpha ? 'hwba(0, 0%, 100%, 1)' : 'hwb(0, 0%, 100%)'), - array: () => [0, 0, 100, 1], - object: () => ({ h: 0, w: 0, b: 100, a: 1 }), - }; - } - - // * color's channels - /** - Get the `red` channel value of a given color. */ - getRed(color: SupportedColorFormats): number { - const { r } = this.RGB(color).object(); - return r; - } - /** Set the `red` value of a color to a specific amount.*/ - setRed(color: SupportedColorFormats, amount: number): ConversionMethods { - const { g, b, a } = this.RGB(color).object(); - const newR = clampRGB(amount); - const newColor = { r: newR, g, b, a }; - - return this.returnColorObject(newColor); - } - /** Increase the `red` value of a color by the given percentage/amount. - * @example - * increaseRed(''rgb(100, 100, 100)', 20).hex(); - * increaseRed('rgb(100, 100, 100)', '20%').rgb().string(); - */ - increaseRed(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const red = typeof amount === 'string' ? r + r * (parseFloat(amount) / 100) : r + amount; - const newR = clampRGB(red); - const newColor = { r: newR, g, b, a }; - - return this.returnColorObject(newColor); - } - /** Decrease the `red` value of a color by the given percentage/amount - * @example - * decreaseRed('rgb(100, 100, 100)', 20).hex(); - * decreaseRed('rgb(100, 100, 100)', '20%').rgb().string(); - */ - decreaseRed(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const red = typeof amount === 'string' ? r - r * (parseFloat(amount) / 100) : r - amount; - const newR = clampRGB(red); - const newColor = { r: newR, g, b, a }; - - return this.returnColorObject(newColor); - } - - /** - Get the `green` channel value of a given color. */ - getGreen(color: SupportedColorFormats): number { - const { g } = this.RGB(color).object(); - return g; - } - /** - Set the `green` value of a color to a specific amount.*/ - setGreen(color: SupportedColorFormats, amount: number): ConversionMethods { - const { r, b, a } = this.RGB(color).object(); - const newG = clampRGB(amount); - const newColor = { r, g: newG, b, a }; - - return this.returnColorObject(newColor); - } - /** Increase the `green` value of a color by the given percentage. - * @example - * increaseGreen('rgb(100, 100, 100)', 20).hex(); - * increaseGreen('rgb(100, 100, 100)', '20%').rgb().string(); - */ - increaseGreen(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const green = typeof amount === 'string' ? g + g * (parseFloat(amount) / 100) : g + amount; - const newG = clampRGB(green); - const newColor = { r, g: newG, b, a }; - - return this.returnColorObject(newColor); - } - /** Decrease the `green` value of a color by the given percentage. - * @example - * decreaseGreen('rgb(100, 100, 100)', 20).hex(); - * decreaseGreen('rgb(100, 100, 100)', '20%').rgb().string(); - */ - decreaseGreen(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const green = typeof amount === 'string' ? g - g * (parseFloat(amount) / 100) : g - amount; - const newG = clampRGB(green); - const newColor = { r, g: newG, b, a }; - - return this.returnColorObject(newColor); - } - - /** - Get the `blue` channel value of a given color. */ - getBlue(color: SupportedColorFormats): number { - const { b } = this.RGB(color).object(); - return b; - } - /** - Set the `blue` value of a color to a specific amount.*/ - setBlur(color: SupportedColorFormats, amount: number): ConversionMethods { - const { r, g, a } = this.RGB(color).object(); - const newB = clampRGB(amount); - const newColor = { r, g, b: newB, a }; - - return this.returnColorObject(newColor); - } - /** Increase the `blue` value of a color by the given percentage. - * @example - * increaseBlue('rgb(100, 100, 100)', 20).hex(); - * increaseBlue('rgb(100, 100, 100)', '20%').rgb().string(); - */ - increaseBlue(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const blue = typeof amount === 'string' ? b + b * (parseFloat(amount) / 100) : b + amount; - const newB = clampRGB(blue); - const newColor = { r, g, b: newB, a }; - - return this.returnColorObject(newColor); - } - /** Decrease the `blue` value of a color by the given percentage. - * @example - * decreaseBlue('rgb(100, 100, 100)', 20).hex(); - * decreaseBlue('rgb(100, 100, 100)', '20%').rgb().string(); - */ - decreaseBlue(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const blue = typeof amount === 'string' ? b - b * (parseFloat(amount) / 100) : b - amount; - const newB = clampRGB(blue); - const newColor = { r, g, b: newB, a }; - - return this.returnColorObject(newColor); - } - - //* hue - /** - Get the `hue` channel value of a given color. */ - getHue(color: SupportedColorFormats): number { - const { h } = this.HSL(color).object(); - return h; - } - /** - Set the `hue` value of a color to a specific amount.*/ - setHue(color: SupportedColorFormats, amount: number): ConversionMethods { - const { s, l, a } = this.HSL(color).object(); - const newH = clampHue(amount); - const newColor = { h: newH, s, l, a }; - - return this.returnColorObject(newColor); - } - /** Increase the `hue` value of a color by the given percentage/amount. - * @example - * increaseHue('rgb(100, 100, 100)', 20).hex(); - * increaseHue('rgb(100, 100, 100)', '20%').rgb().string(); - */ - increaseHue(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const hue = typeof amount === 'string' ? h + h * (parseFloat(amount) / 100) : h + amount; - const newH = clampHue(hue); - const newColor = { h: newH, s, l, a }; - - return this.returnColorObject(newColor); - } - /** Decrease the `hue` value of a color by the given percentage/amount. - * @example - * decreaseHue('rgb(100, 100, 100)', 20).hex(); - * decreaseHue('rgb(100, 100, 100)', '20%').rgb().string(); - */ - decreaseHue(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const hue = typeof amount === 'string' ? h - h * (parseFloat(amount) / 100) : h - amount; - const newH = clampHue(hue); - const newColor = { h: newH, s, l, a }; - - return this.returnColorObject(newColor); - } - /** - * - Spin the `hue` channel by a certain percentage/amount. - * @example - * spin('red', 20).hex(); - * spin('rgb(255, 0, 0)', '20%').rgb().string(); - */ - spin(color: SupportedColorFormats, degree: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const spin = typeof degree === 'string' ? s * (parseFloat(degree) / 100) : degree; - const newColor = { h: Math.round((h + spin) % 360), s, l, a }; - - return this.returnColorObject(newColor); - } - - //* saturation - /** - Get the `saturation` value of a given color. */ - getSaturation(color: SupportedColorFormats): number { - const { s } = this.HSL(color).object(); - return s; - } - /** - Set the `saturation` value of a color to a specific amount.*/ - setSaturation(color: SupportedColorFormats, amount: number): ConversionMethods { - const { h, l, a } = this.HSL(color).object(); - const newS = clamp100(amount); - const saturatedColor = { h, s: newS, l, a }; - - return this.returnColorObject(saturatedColor); - } - /** - * - Increase the saturation of the given color by a certain percentage/amount. - * @example - * saturate('red', 20).hex(); - * saturate('rgb(255, 0, 0)', '20%').rgb().string(); - */ - saturate(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const saturation = typeof amount === 'string' ? s + s * (parseFloat(amount) / 100) : s + amount; - const newS = clamp100(saturation); - const saturatedColor = { h, s: newS, l, a }; - - return this.returnColorObject(saturatedColor); - } - /** - * - Decrease the saturation of the given color by a certain percentage/amount. - * @example - * saturate('red', 20).hex(); - * saturate('rgb(255, 0, 0)', '20%').rgb().string(); - */ - desaturate(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const saturation = typeof amount === 'string' ? s - s * (parseFloat(amount) / 100) : s - amount; - const newS = clamp100(saturation); - const desaturatedColor = { h, s: newS, l, a }; - - return this.returnColorObject(desaturatedColor); - } - - //* brightness - /** - Get color's HSL `luminosity` channel value. - * - If you want the overall `luminosity` of a color use `getLuminanceWCAG` method. - */ - getLuminance(color: SupportedColorFormats): number { - const { l } = this.HSL(color).object(); - return l; - } - /** - Set HSL's `luminosity` channel for a given color to a specific amount.*/ - setLuminance(color: SupportedColorFormats, amount: number): ConversionMethods { - const { h, s, a } = this.HSL(color).object(); - const newL = clamp100(amount); - const newColor = { h, s, l: newL, a }; - - return this.returnColorObject(newColor); - } - /** - * - Increase the brightness of the given color by a certain percentage/amount. - * @example - * brighten('red', 20).hex(); - * brighten('rgb(255, 0, 0)', '20%').rgb().string(); - */ - brighten(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const lum = typeof amount === 'string' ? l + l * (parseFloat(amount) / 100) : l + amount; - const newL = clamp100(lum); - const brightenedColor = { h, s, l: newL, a }; - - return this.returnColorObject(brightenedColor); - } - /** - * - Decrease the brightness of the given color by a certain percentage/amount. - * @example - * darken('red', 20).hex(); - * darken('rgb(255, 0, 0)', '20%').rgb().string(); - */ - darken(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, l, a } = this.HSL(color).object(); - const lum = typeof amount === 'string' ? l - l * (parseFloat(amount) / 100) : l - amount; - const newL = clamp100(lum); - const darkenedColor = { h, s, l: newL, a }; - - return this.returnColorObject(darkenedColor); - } - - /** - Get the HSV's `value` (brightness) channel value of a given color. */ - getBrightness(color: SupportedColorFormats): number { - const { v } = this.HSV(color).object(); - return v; - } - /** - Set HSV's `value` (brightness) channel for a given color to a specific amount.*/ - setBrightness(color: SupportedColorFormats, amount: number): ConversionMethods { - const { h, s, a } = this.HSV(color).object(); - const newV = clamp100(amount); - const newColor = { h, s, v: newV, a }; - - return this.returnColorObject(newColor); - } - /** Increase HSV's `value` (brightness) channel value of a color by the given percentage/amount.*/ - increaseBrightness(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, v, a } = this.HSV(color).object(); - const value = typeof amount === 'string' ? v + v * (parseFloat(amount) / 100) : v + amount; - const newV = clamp100(value); - const newColor = { h, s, v: newV, a }; - - return this.returnColorObject(newColor); - } - /** Decrease HSV's `value` (brightness) channel value of a color by the given percentage/amount.*/ - decreaseBrightness(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { h, s, v, a } = this.HSV(color).object(); - const value = typeof amount === 'string' ? v - v * (parseFloat(amount) / 100) : v - amount; - const newV = clamp100(value); - const newColor = { h, s, v: newV, a }; - - return this.returnColorObject(newColor); - } - - //* alpha - /** - Get the `alpha` value of a given color. */ - getAlpha(color: SupportedColorFormats): number { - const { a } = this.RGB(color).object(); - return a; - } - /** - Set the `alpha` value of a color to a specific amount.*/ - setAlpha(color: SupportedColorFormats, amount: number): ConversionMethods { - const { r, g, b } = this.RGB(color).object(); - const newA = clampAlpha(amount); - const newColor = { r, g, b, a: newA }; - - return this.returnColorObject(newColor); - } - /** Increase the `alpha` value of a color by the given percentage.*/ - increaseAlpha(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const alpha = typeof amount === 'string' ? a + a * (parseFloat(amount) / 100) : a + amount; - const newA = clampAlpha(alpha); - const newColor = { r, g, b, a: newA }; - - return this.returnColorObject(newColor); - } - /** Decrease the `alpha` value of a color by the given percentage.*/ - decreaseAlpha(color: SupportedColorFormats, amount: number | string): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const alpha = typeof amount === 'string' ? a - a * (parseFloat(amount) / 100) : a - amount; - const newA = clampAlpha(alpha); - const newColor = { r, g, b, a: newA }; - - return this.returnColorObject(newColor); - } - - // * tools - /** - Returns the perceived `luminance` of a color, from `0-1` as defined by Web Content Accessibility Guidelines (Version 2.0). */ - getLuminanceWCAG(color: SupportedColorFormats): number { - const { r, g, b } = this.RGB(color).object(); - const a = [r, g, b].map(v => (v / 255 <= 0.03928 ? v / 255 / 12.92 : Math.pow((v / 255 + 0.055) / 1.055, 2.4))); - return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; - } - - /** - Calculates the contrast ratio between two colors, useful for ensuring accessibility and readability. */ - contrastRatio(color1: SupportedColorFormats, color2: SupportedColorFormats): number { - const luminance1 = this.getLuminanceWCAG(color1); - const luminance2 = this.getLuminanceWCAG(color2); - const contrast = (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05); - return Math.round(contrast * 100) / 100; - } - - /** - Returns a boolean indicating whether the color is considered "dark" or not */ - isDark(color: SupportedColorFormats): boolean { - const luminance = this.getLuminanceWCAG(color); - return luminance < 0.5; - } - - /** - Returns a boolean indicating whether the color is considered "light" or not */ - isLight(color: SupportedColorFormats): boolean { - const luminance = this.getLuminanceWCAG(color); - return luminance >= 0.5; - } - - /** - * - Check if two colors are similar within a specified tolerance. - * @example - * const tolerance = 0; - * const isEqual = colorKit.areColorsEqual("#F00", "red", tolerance); // true - */ - areColorsEqual(color1: SupportedColorFormats, color2: SupportedColorFormats, tolerance = 0): boolean { - const rgb1 = this.RGB(color1).object(); - const rgb2 = this.RGB(color2).object(); - - const deltaR = rgb1.r - rgb2.r; - const deltaG = rgb1.g - rgb2.g; - const deltaB = rgb1.b - rgb2.b; - const difference = Math.sqrt(deltaR * deltaR + deltaG * deltaG + deltaB * deltaB); - - return difference <= tolerance; - } - - /** - * - Blends two colors by a certain amount. - * @example - * blend('yellow', 'red', 50).hex(); // #ff8000 - */ - blend(color1: SupportedColorFormats, color2: SupportedColorFormats, percentage: number): ConversionMethods { - percentage = percentage / 100; - - const rgba1 = this.RGB(color1).object(); - const rgba2 = this.RGB(color2).object(); - - const r = clampRGB(rgba1.r * (1 - percentage) + rgba2.r * percentage), - g = clampRGB(rgba1.g * (1 - percentage) + rgba2.g * percentage), - b = clampRGB(rgba1.b * (1 - percentage) + rgba2.b * percentage), - a = clampAlpha(rgba1.a * (1 - percentage) + rgba2.a * percentage); - - const blendedColor = { r, g, b, a }; - - return this.returnColorObject(blendedColor); - } - - /** - Invert (negate) a color, black becomes white, white becomes black, blue becomes orange and so on. */ - invert(color: SupportedColorFormats): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const invertedColor = { r: 255 - r, g: 255 - g, b: 255 - b, a }; - return this.returnColorObject(invertedColor); - } - - /** - Completely desaturate a color into grayscale. */ - grayscale(color: SupportedColorFormats): ConversionMethods { - const { r, g, b, a } = this.RGB(color).object(); - const gray = clampRGB(r * 0.3 + g * 0.59 + b * 0.11); - const grayColor = { r: gray, g: gray, b: gray, a }; - - return this.returnColorObject(grayColor); - } - - /** - Generate a random color from `HSL` values. */ - randomHslColor({ h = [0, 360], s = [0, 100], l = [0, 100], a = [1, 1] } = {}): ConversionMethods { - const random = { - h: clampHue(randomNumber(h[0], h[1])), - s: clamp100(randomNumber(s[0], s[1])), - l: clamp100(randomNumber(l[0], l[1])), - a: clampAlpha(randomNumber(a[0], a[1])), - }; - - return this.returnColorObject(random); - } - /** - Generate a random color from `HSV` values. */ - randomHsvColor({ h = [0, 360], s = [0, 100], v = [0, 100], a = [1, 1] } = {}): ConversionMethods { - const random = { - h: clampHue(randomNumber(h[0], h[1])), - s: clamp100(randomNumber(s[0], s[1])), - v: clamp100(randomNumber(v[0], v[1])), - a: clampAlpha(randomNumber(a[0], a[1])), - }; - - return this.returnColorObject(random); - } - /** - Generate a random color from `RGB` values. */ - randomRgbColor({ r = [0, 255], g = [0, 255], b = [0, 255], a = [1, 1] } = {}): ConversionMethods { - const random = { - r: clampRGB(randomNumber(r[0], r[1])), - g: clampRGB(randomNumber(g[0], g[1])), - b: clampRGB(randomNumber(b[0], b[1])), - a: clampAlpha(randomNumber(a[0], a[1])), - }; - - return this.returnColorObject(random); - } - /** - Generate a random color from `HWB` values. */ - randomHwbColor({ h = [0, 360], w = [0, 100], b = [0, 100], a = [1, 1] } = {}): ConversionMethods { - const random = { - h: clampHue(randomNumber(h[0], h[1])), - w: clamp100(randomNumber(w[0], w[1])), - b: clamp100(randomNumber(b[0], b[1])), - a: clampAlpha(randomNumber(a[0], a[1])), - }; - - return this.returnColorObject(random); - } - - /** - Returns the first color with the desired contrast ratio against the second color */ - adjustContrast(color1: SupportedColorFormats, color2: SupportedColorFormats, ratio = 4.5): ConversionMethods { - const contrast = this.contrastRatio(color1, color2); - const color1RGB = this.RGB(color1).object(); - const channels = ['r', 'g', 'b'] as const; - - const adjustLuminance = (colorRGB: rgbaT, by: number) => { - const r = clampRGB(colorRGB.r + by); - const g = clampRGB(colorRGB.g + by); - const b = clampRGB(colorRGB.b + by); - return { r, g, b, a: colorRGB.a }; - }; - - let newColor = color1RGB; - - //* increase contrast - if (ratio && contrast < ratio) { - while (this.contrastRatio(newColor, color2) < ratio) { - const increase = this.isDark(color2); // if the background color is dark - newColor = adjustLuminance(newColor, increase ? 1 : -1); - - // break if the color reached the limit - if (channels.every(e => newColor[e] === 0)) break; - if (channels.every(e => newColor[e] === 255)) break; - } - //* decrease contrast - } else if (ratio && contrast > ratio) { - while (this.contrastRatio(newColor, color2) > ratio) { - const increase = !this.isDark(color2); // if the background color is light - newColor = adjustLuminance(newColor, increase ? 1 : -1); - - // break if the color reached the limit - if (channels.every(e => newColor[e] === 0)) break; - if (channels.every(e => newColor[e] === 255)) break; - } - } - - return this.returnColorObject(newColor); - } -} - -const colorKit = new Colors(); - -export default colorKit; diff --git a/src/colorKit/colorManipulation.ts b/src/colorKit/colorManipulation.ts new file mode 100644 index 0000000..0c680d1 --- /dev/null +++ b/src/colorKit/colorManipulation.ts @@ -0,0 +1,367 @@ +import { clamp100, clampAlpha, clampHue, clampRGB } from './utilities'; +import { HEX, HSL, HSV, HWB, RGB } from './colorConversion'; + +import type { ConversionMethods, SupportedColorFormats } from './types'; + +function returnColorObject(color: SupportedColorFormats) { + 'worklet'; + return { + hex: () => { + 'worklet'; + return HEX(color); + }, + rgb: () => { + 'worklet'; + return RGB(color); + }, + hsl: () => { + 'worklet'; + return HSL(color); + }, + hsv: () => { + 'worklet'; + return HSV(color); + }, + hwb: () => { + 'worklet'; + return HWB(color); + }, + }; +} + +// * Red channel +/** Set the `red` value of a color to a specific amount.*/ +export function setRed(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { g, b, a } = RGB(color).object(); + const newR = clampRGB(amount); + const newColor = { r: newR, g, b, a }; + + return returnColorObject(newColor); +} + +/** Increase the `red` value of a color by the given percentage/amount. + * @example + * increaseRed('rgb(100, 100, 100)', 20).hex(); + * increaseRed('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function increaseRed(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const red = typeof amount === 'string' ? r + r * (parseFloat(amount) / 100) : r + amount; + const newR = clampRGB(red); + const newColor = { r: newR, g, b, a }; + + return returnColorObject(newColor); +} + +/** Decrease the `red` value of a color by the given percentage/amount + * @example + * decreaseRed('rgb(100, 100, 100)', 20).hex(); + * decreaseRed('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function decreaseRed(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const red = typeof amount === 'string' ? r - r * (parseFloat(amount) / 100) : r - amount; + const newR = clampRGB(red); + const newColor = { r: newR, g, b, a }; + + return returnColorObject(newColor); +} + +// * Green channel +/** - Set the `green` value of a color to a specific amount.*/ +export function setGreen(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { r, b, a } = RGB(color).object(); + const newG = clampRGB(amount); + const newColor = { r, g: newG, b, a }; + + return returnColorObject(newColor); +} + +/** Increase the `green` value of a color by the given percentage. + * @example + * increaseGreen('rgb(100, 100, 100)', 20).hex(); + * increaseGreen('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function increaseGreen(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const green = typeof amount === 'string' ? g + g * (parseFloat(amount) / 100) : g + amount; + const newG = clampRGB(green); + const newColor = { r, g: newG, b, a }; + + return returnColorObject(newColor); +} + +/** Decrease the `green` value of a color by the given percentage. + * @example + * decreaseGreen('rgb(100, 100, 100)', 20).hex(); + * decreaseGreen('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function decreaseGreen(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const green = typeof amount === 'string' ? g - g * (parseFloat(amount) / 100) : g - amount; + const newG = clampRGB(green); + const newColor = { r, g: newG, b, a }; + + return returnColorObject(newColor); +} + +// * Blue channel +/** - Set the `blue` value of a color to a specific amount.*/ +export function setBlue(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { r, g, a } = RGB(color).object(); + const newB = clampRGB(amount); + const newColor = { r, g, b: newB, a }; + + return returnColorObject(newColor); +} + +/** Increase the `blue` value of a color by the given percentage. + * @example + * increaseBlue('rgb(100, 100, 100)', 20).hex(); + * increaseBlue('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function increaseBlue(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const blue = typeof amount === 'string' ? b + b * (parseFloat(amount) / 100) : b + amount; + const newB = clampRGB(blue); + const newColor = { r, g, b: newB, a }; + + return returnColorObject(newColor); +} + +/** Decrease the `blue` value of a color by the given percentage. + * @example + * decreaseBlue('rgb(100, 100, 100)', 20).hex(); + * decreaseBlue('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function decreaseBlue(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const blue = typeof amount === 'string' ? b - b * (parseFloat(amount) / 100) : b - amount; + const newB = clampRGB(blue); + const newColor = { r, g, b: newB, a }; + + return returnColorObject(newColor); +} + +//* Alpha channel +/** - Get the `alpha` value of a given color. */ +export function getAlpha(color: SupportedColorFormats): number { + 'worklet'; + const { a } = RGB(color).object(); + return a; +} + +/** - Set the `alpha` value of a color to a specific amount.*/ +export function setAlpha(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { r, g, b } = RGB(color).object(); + const newA = clampAlpha(amount); + const newColor = { r, g, b, a: newA }; + + return returnColorObject(newColor); +} + +/** Increase the `alpha` value of a color by the given percentage.*/ +export function increaseAlpha(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const alpha = typeof amount === 'string' ? a + a * (parseFloat(amount) / 100) : a + amount; + const newA = clampAlpha(alpha); + const newColor = { r, g, b, a: newA }; + + return returnColorObject(newColor); +} + +/** Decrease the `alpha` value of a color by the given percentage.*/ +export function decreaseAlpha(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const alpha = typeof amount === 'string' ? a - a * (parseFloat(amount) / 100) : a - amount; + const newA = clampAlpha(alpha); + const newColor = { r, g, b, a: newA }; + + return returnColorObject(newColor); +} + +//* Hue +/** - Set the `hue` value of a color to a specific amount.*/ +export function setHue(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { s, l, a } = HSL(color).object(); + const newH = clampHue(amount); + const newColor = { h: newH, s, l, a }; + + return returnColorObject(newColor); +} + +/** Increase the `hue` value of a color by the given percentage/amount. + * @example + * increaseHue('rgb(100, 100, 100)', 20).hex(); + * increaseHue('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function increaseHue(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const hue = typeof amount === 'string' ? h + h * (parseFloat(amount) / 100) : h + amount; + const newH = clampHue(hue); + const newColor = { h: newH, s, l, a }; + + return returnColorObject(newColor); +} + +/** Decrease the `hue` value of a color by the given percentage/amount. + * @example + * decreaseHue('rgb(100, 100, 100)', 20).hex(); + * decreaseHue('rgb(100, 100, 100)', '20%').rgb().string(); + */ +export function decreaseHue(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const hue = typeof amount === 'string' ? h - h * (parseFloat(amount) / 100) : h - amount; + const newH = clampHue(hue); + const newColor = { h: newH, s, l, a }; + + return returnColorObject(newColor); +} + +/** + * - Spin the `hue` channel by a certain percentage/amount. + * @example + * spin('red', 20).hex(); + * spin('rgb(255, 0, 0)', '20%').rgb().string(); + */ +export function spin(color: SupportedColorFormats, degree: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const spinDegree = typeof degree === 'string' ? s * (parseFloat(degree) / 100) : degree; + const newColor = { h: Math.round((h + spinDegree) % 360), s, l, a }; + + return returnColorObject(newColor); +} + +//* Saturation +/** - Set the `saturation` value of a color to a specific amount.*/ +export function setSaturation(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { h, l, a } = HSL(color).object(); + const newS = clamp100(amount); + const saturatedColor = { h, s: newS, l, a }; + + return returnColorObject(saturatedColor); +} + +/** + * - Increase the saturation of the given color by a certain percentage/amount. + * @example + * saturate('red', 20).hex(); + * saturate('rgb(255, 0, 0)', '20%').rgb().string(); + */ +export function saturate(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const saturation = typeof amount === 'string' ? s + s * (parseFloat(amount) / 100) : s + amount; + const newS = clamp100(saturation); + const saturatedColor = { h, s: newS, l, a }; + + return returnColorObject(saturatedColor); +} + +/** + * - Decrease the saturation of the given color by a certain percentage/amount. + * @example + * saturate('red', 20).hex(); + * saturate('rgb(255, 0, 0)', '20%').rgb().string(); + */ +export function desaturate(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const saturation = typeof amount === 'string' ? s - s * (parseFloat(amount) / 100) : s - amount; + const newS = clamp100(saturation); + const desaturatedColor = { h, s: newS, l, a }; + + return returnColorObject(desaturatedColor); +} + +//* Brightness +/** - Set HSL's `luminosity` channel for a given color to a specific amount.*/ +export function setLuminance(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { h, s, a } = HSL(color).object(); + const newL = clamp100(amount); + const newColor = { h, s, l: newL, a }; + + return returnColorObject(newColor); +} + +/** + * - Increase the brightness of the given color by a certain percentage/amount. + * @example + * brighten('red', 20).hex(); + * brighten('rgb(255, 0, 0)', '20%').rgb().string(); + */ +export function brighten(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const lum = typeof amount === 'string' ? l + l * (parseFloat(amount) / 100) : l + amount; + const newL = clamp100(lum); + const brightenedColor = { h, s, l: newL, a }; + + return returnColorObject(brightenedColor); +} + +/** + * - Decrease the brightness of the given color by a certain percentage/amount. + * @example + * darken('red', 20).hex(); + * darken('rgb(255, 0, 0)', '20%').rgb().string(); + */ +export function darken(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, l, a } = HSL(color).object(); + const lum = typeof amount === 'string' ? l - l * (parseFloat(amount) / 100) : l - amount; + const newL = clamp100(lum); + const darkenedColor = { h, s, l: newL, a }; + + return returnColorObject(darkenedColor); +} + +/** - Set HSV's `value` (brightness) channel for a given color to a specific amount.*/ +export function setBrightness(color: SupportedColorFormats, amount: number): ConversionMethods { + 'worklet'; + const { h, s, a } = HSV(color).object(); + const newV = clamp100(amount); + const newColor = { h, s, v: newV, a }; + + return returnColorObject(newColor); +} + +/** Increase HSV's `value` (brightness) channel value of a color by the given percentage/amount.*/ +export function increaseBrightness(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, v, a } = HSV(color).object(); + const value = typeof amount === 'string' ? v + v * (parseFloat(amount) / 100) : v + amount; + const newV = clamp100(value); + const newColor = { h, s, v: newV, a }; + + return returnColorObject(newColor); +} + +/** Decrease HSV's `value` (brightness) channel value of a color by the given percentage/amount.*/ +export function decreaseBrightness(color: SupportedColorFormats, amount: number | string): ConversionMethods { + 'worklet'; + const { h, s, v, a } = HSV(color).object(); + const value = typeof amount === 'string' ? v - v * (parseFloat(amount) / 100) : v - amount; + const newV = clamp100(value); + const newColor = { h, s, v: newV, a }; + + return returnColorObject(newColor); +} diff --git a/src/colorKit/colorUtilities.ts b/src/colorKit/colorUtilities.ts new file mode 100644 index 0000000..1ff9833 --- /dev/null +++ b/src/colorKit/colorUtilities.ts @@ -0,0 +1,165 @@ +import { HEX, HSL, HSV, HWB, RGB } from './colorConversion'; +import { contrastRatio, isDark } from './colorInformation'; +import { clamp100, clampAlpha, clampHue, clampRGB, randomNumber } from './utilities'; + +import type { ConversionMethods, rgbaT, SupportedColorFormats } from './types'; + +function returnColorObject(color: SupportedColorFormats) { + 'worklet'; + return { + hex: () => { + 'worklet'; + return HEX(color); + }, + rgb: () => { + 'worklet'; + return RGB(color); + }, + hsl: () => { + 'worklet'; + return HSL(color); + }, + hsv: () => { + 'worklet'; + return HSV(color); + }, + hwb: () => { + 'worklet'; + return HWB(color); + }, + }; +} + +/** + * - Blends two colors by a certain amount. + * @example + * blend('yellow', 'red', 50).hex(); // #ff8000 + */ +export function blend(color1: SupportedColorFormats, color2: SupportedColorFormats, percentage: number): ConversionMethods { + 'worklet'; + percentage = percentage / 100; + + const rgba1 = RGB(color1).object(); + const rgba2 = RGB(color2).object(); + + const r = clampRGB(rgba1.r * (1 - percentage) + rgba2.r * percentage), + g = clampRGB(rgba1.g * (1 - percentage) + rgba2.g * percentage), + b = clampRGB(rgba1.b * (1 - percentage) + rgba2.b * percentage), + a = clampAlpha(rgba1.a * (1 - percentage) + rgba2.a * percentage); + + const blendedColor = { r, g, b, a }; + + return returnColorObject(blendedColor); +} + +/** - Invert (negate) a color, black becomes white, white becomes black, blue becomes orange and so on. */ +export function invert(color: SupportedColorFormats): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const invertedColor = { r: 255 - r, g: 255 - g, b: 255 - b, a }; + return returnColorObject(invertedColor); +} + +/** - Completely desaturate a color into grayscale. */ +export function grayscale(color: SupportedColorFormats): ConversionMethods { + 'worklet'; + const { r, g, b, a } = RGB(color).object(); + const gray = clampRGB(r * 0.3 + g * 0.59 + b * 0.11); + const grayColor = { r: gray, g: gray, b: gray, a }; + + return returnColorObject(grayColor); +} + +/** - Generate a random color from `HSL` values. */ +export function randomHslColor({ h = [0, 360], s = [0, 100], l = [0, 100], a = [1, 1] } = {}): ConversionMethods { + 'worklet'; + const random = { + h: clampHue(randomNumber(h[0], h[1])), + s: clamp100(randomNumber(s[0], s[1])), + l: clamp100(randomNumber(l[0], l[1])), + a: clampAlpha(randomNumber(a[0], a[1])), + }; + + return returnColorObject(random); +} + +/** - Generate a random color from `HSV` values. */ +export function randomHsvColor({ h = [0, 360], s = [0, 100], v = [0, 100], a = [1, 1] } = {}): ConversionMethods { + 'worklet'; + const random = { + h: clampHue(randomNumber(h[0], h[1])), + s: clamp100(randomNumber(s[0], s[1])), + v: clamp100(randomNumber(v[0], v[1])), + a: clampAlpha(randomNumber(a[0], a[1])), + }; + + return returnColorObject(random); +} + +/** - Generate a random color from `RGB` values. */ +export function randomRgbColor({ r = [0, 255], g = [0, 255], b = [0, 255], a = [1, 1] } = {}): ConversionMethods { + 'worklet'; + const random = { + r: clampRGB(randomNumber(r[0], r[1])), + g: clampRGB(randomNumber(g[0], g[1])), + b: clampRGB(randomNumber(b[0], b[1])), + a: clampAlpha(randomNumber(a[0], a[1])), + }; + + return returnColorObject(random); +} + +/** - Generate a random color from `HWB` values. */ +export function randomHwbColor({ h = [0, 360], w = [0, 100], b = [0, 100], a = [1, 1] } = {}): ConversionMethods { + 'worklet'; + const random = { + h: clampHue(randomNumber(h[0], h[1])), + w: clamp100(randomNumber(w[0], w[1])), + b: clamp100(randomNumber(b[0], b[1])), + a: clampAlpha(randomNumber(a[0], a[1])), + }; + + return returnColorObject(random); +} + +/** - Returns the first color with the desired contrast ratio against the second color */ +export function adjustContrast(color1: SupportedColorFormats, color2: SupportedColorFormats, ratio = 4.5): ConversionMethods { + 'worklet'; + const contrast = contrastRatio(color1, color2); + const color1RGB = RGB(color1).object(); + const channels = ['r', 'g', 'b'] as const; + + function adjustLuminance(colorRGB: rgbaT, by: number) { + 'worklet'; + const r = clampRGB(colorRGB.r + by); + const g = clampRGB(colorRGB.g + by); + const b = clampRGB(colorRGB.b + by); + return { r, g, b, a: colorRGB.a }; + } + + let newColor = color1RGB; + + //* increase contrast + if (ratio && contrast < ratio) { + while (contrastRatio(newColor, color2) < ratio) { + const adjustBy = isDark(color2) ? 1 : -1; // increase or decrease relative to the background color + newColor = adjustLuminance(newColor, adjustBy); + + // break if the color reached the limit + if (channels.every(e => newColor[e] === 0)) break; + if (channels.every(e => newColor[e] === 255)) break; + } + //* decrease contrast + } else if (ratio && contrast > ratio) { + while (contrastRatio(newColor, color2) > ratio) { + const adjustBy = !isDark(color2) ? 1 : -1; // increase or decrease relative to the background color + newColor = adjustLuminance(newColor, adjustBy); + + // break if the color reached the limit + if (channels.every(e => newColor[e] === 0)) break; + if (channels.every(e => newColor[e] === 255)) break; + } + } + + return returnColorObject(newColor); +} diff --git a/src/colorKit/index.ts b/src/colorKit/index.ts new file mode 100644 index 0000000..50bb3e9 --- /dev/null +++ b/src/colorKit/index.ts @@ -0,0 +1,7 @@ +import * as colorConversion from './colorConversion'; +import * as colorInformation from './colorInformation'; +import * as colorManipulation from './colorManipulation'; +import * as colorUtilities from './colorUtilities'; + +const colorKit = { ...colorConversion, ...colorInformation, ...colorManipulation, ...colorUtilities }; +export default colorKit; diff --git a/src/colorKit/types.ts b/src/colorKit/types.ts index 3c3a6c1..6638cc8 100644 --- a/src/colorKit/types.ts +++ b/src/colorKit/types.ts @@ -18,7 +18,7 @@ export type hwbT = Omit; export type SupportedColorFormats = ColorString | rgbaT | rgbT | hslaT | hslT | hsvaT | hsvT | hwbaT | hwbT | number; -export type colorTypes = { +export type colorTypes = { object: () => T; string: (alpha?: boolean) => string; array: () => number[]; diff --git a/src/colorKit/utilities.ts b/src/colorKit/utilities.ts index f18467c..c7cb45a 100644 --- a/src/colorKit/utilities.ts +++ b/src/colorKit/utilities.ts @@ -1,6 +1,46 @@ -export const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max)); -export const clampRGB = (value: number) => clamp(Math.round(value), 0, 255); -export const clampHue = (value: number) => clamp(Math.round(value), 0, 360); -export const clamp100 = (value: number) => clamp(Math.round(value), 0, 100); -export const clampAlpha = (value: number) => clamp(+value.toFixed(2), 0, 1); -export const randomNumber = (min: number, max: number) => Math.random() * (max - min) + min; +export function clamp(value: number, min: number, max: number) { + 'worklet'; + return Math.max(min, Math.min(value, max)); +} + +export function clampRGB(value: number) { + 'worklet'; + return clamp(Math.round(value), 0, 255); +} + +export function clampHue(value: number) { + 'worklet'; + return clamp(Math.round(value), 0, 360); +} + +export function clamp100(value: number) { + 'worklet'; + return clamp(Math.round(value), 0, 100); +} + +export function clampAlpha(value: number) { + 'worklet'; + return clamp(+value.toFixed(2), 0, 1); +} + +export function randomNumber(min: number, max: number) { + 'worklet'; + return Math.random() * (max - min) + min; +} + +export function numberToHexString(c: number): string { + 'worklet'; + c = clampRGB(c); + const hex = c.toString(16).padStart(2, '0'); + return hex; +} + +export function calculateHueValue(p: number, q: number, t: number): number { + 'worklet'; + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; +} diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx index 711cc3e..55ad4fb 100644 --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -9,7 +9,7 @@ import { ConditionalRendering, getStyle, isWeb } from '@utils'; import type { PreviewProps } from '@types'; import type { ReactNode } from 'react'; -import type { StyleProp, TextStyle } from 'react-native'; +import type { StyleProp, TextStyle, ViewStyle } from 'react-native'; import type { SharedValue } from 'react-native-reanimated'; const ReText = ({ text, style, hash }: { text: () => string; style: StyleProp[]; hash: SharedValue[] }) => { @@ -92,7 +92,15 @@ export function Preview({ ); } -function Wrapper({ children, disableTexture, style }: { children: ReactNode; disableTexture: boolean; style: {} | null }) { +function Wrapper({ + children, + disableTexture, + style, +}: { + children: ReactNode; + disableTexture: boolean; + style: StyleProp; +}) { if (disableTexture) { return {children}; } @@ -118,4 +126,4 @@ const previewWrapperWeb = { 'repeating-linear-gradient(45deg, #c1c1c1 25%, transparent 25%, transparent 75%, #c1c1c1 75%, #c1c1c1), repeating-linear-gradient(45deg, #c1c1c1 25%, #fff 25%, #fff 75%, #c1c1c1 75%, #c1c1c1)', backgroundPosition: '0px 0px, 8px 8px', backgroundSize: '16px 16px', -}; +} as StyleProp; diff --git a/src/components/Sliders/OpacitySlider.tsx b/src/components/Sliders/OpacitySlider.tsx index 70c5046..16d7073 100644 --- a/src/components/Sliders/OpacitySlider.tsx +++ b/src/components/Sliders/OpacitySlider.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Image, LayoutChangeEvent, StyleSheet } from 'react-native'; +import { Image, StyleSheet } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; @@ -8,6 +8,7 @@ import Thumb from '@thumb'; import { clamp, getStyle, HSVA2HSLA_string, isRtl, isWeb, RenderNativeOnly, RenderWebOnly } from '@utils'; import type { SliderProps } from '@types'; +import type { LayoutChangeEvent } from 'react-native'; import type { PanGestureHandlerEventPayload } from 'react-native-gesture-handler'; export function OpacitySlider({ diff --git a/src/components/Thumb/BuiltinThumbs/DoubleTriangle.tsx b/src/components/Thumb/BuiltinThumbs/DoubleTriangle.tsx index 62a890c..830e011 100644 --- a/src/components/Thumb/BuiltinThumbs/DoubleTriangle.tsx +++ b/src/components/Thumb/BuiltinThumbs/DoubleTriangle.tsx @@ -6,6 +6,7 @@ import { styles } from '@styles'; import { isRtl } from '@utils'; import type { BuiltinThumbsProps } from '@types'; +import type { StyleProp, ViewStyle } from 'react-native'; export default function DoubleTriangle({ width, @@ -17,7 +18,7 @@ export default function DoubleTriangle({ style, vertical, }: BuiltinThumbsProps) { - const computedStyle = { + const computedStyle: StyleProp = { width, height, flexDirection: vertical ? (isRtl ? 'row' : 'row-reverse') : 'column', diff --git a/src/components/Thumb/BuiltinThumbs/TriangleDown.tsx b/src/components/Thumb/BuiltinThumbs/TriangleDown.tsx index f1d7bfe..b377ac3 100644 --- a/src/components/Thumb/BuiltinThumbs/TriangleDown.tsx +++ b/src/components/Thumb/BuiltinThumbs/TriangleDown.tsx @@ -1,10 +1,11 @@ import React from 'react'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; -import { isRtl } from '@utils'; import { styles } from '@styles'; +import { isRtl } from '@utils'; import type { BuiltinThumbsProps } from '@types'; +import type { StyleProp, ViewStyle } from 'react-native'; export default function ({ width, @@ -16,7 +17,7 @@ export default function ({ style, vertical, }: BuiltinThumbsProps) { - const computedStyle = { + const computedStyle: StyleProp = { width, height, ...(vertical diff --git a/src/components/Thumb/BuiltinThumbs/TriangleUp.tsx b/src/components/Thumb/BuiltinThumbs/TriangleUp.tsx index 584f0e0..860daa6 100644 --- a/src/components/Thumb/BuiltinThumbs/TriangleUp.tsx +++ b/src/components/Thumb/BuiltinThumbs/TriangleUp.tsx @@ -1,10 +1,11 @@ import React from 'react'; import Animated, { useAnimatedStyle } from 'react-native-reanimated'; -import { isRtl } from '@utils'; import { styles } from '@styles'; +import { isRtl } from '@utils'; import type { BuiltinThumbsProps } from '@types'; +import type { StyleProp, ViewStyle } from 'react-native'; export default function TriangleUp({ width, @@ -16,7 +17,7 @@ export default function TriangleUp({ style, vertical, }: BuiltinThumbsProps) { - const computedStyle = { + const computedStyle: StyleProp = { width, height, ...(vertical ? { justifyContent: 'center', alignItems: isRtl ? 'flex-end' : 'flex-start' } : { justifyContent: 'flex-end' }), diff --git a/src/types.ts b/src/types.ts index 0614091..05fe414 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,9 +67,9 @@ export type RenderThumbType = React.FC; export type ThumbProps = { thumbColor?: string; - handleStyle: {}; - innerStyle?: {}; - style?: {}; + handleStyle: StyleProp; + innerStyle?: StyleProp; + style?: StyleProp; renderThumb?: RenderThumbType; vertical?: boolean; adaptSpectrum?: boolean; @@ -84,9 +84,9 @@ export type BuiltinThumbsProps = { borderRadius: number; thumbColor?: string; adaptiveColor: SharedValue; - handleStyle: {}; - innerStyle?: {}; - style?: {}; + handleStyle: StyleProp; + innerStyle?: StyleProp; + style?: StyleProp; solidColor: AnimatedStyleProp; renderThumb?: RenderThumbType; vertical?: boolean; @@ -380,7 +380,7 @@ export interface SliderProps { style?: StyleProp; } -export interface RgbSliderProps extends Omit {} +export type RgbSliderProps = Omit; export interface HueCircular extends Omit { children?: ReactNode; diff --git a/src/utils.tsx b/src/utils.tsx index e6ec021..b4d00a0 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -110,12 +110,12 @@ export function RGBA2HSVA(r: number, g: number, b: number, a = 1) { const max = Math.max(r, g, b), min = Math.min(r, g, b), - d = max - min; - - let v = max, - h = 0, + d = max - min, + v = max, s = max === 0 ? 0 : d / max; + let h = 0; + if (max === min) { h = 0; } else { diff --git a/tsconfig.json b/tsconfig.json index 7dad34e..541d030 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "@panels/*": ["src/components/Panels/*"], "@assets/*": ["src/assets/*"], "@thumb": ["src/components/Thumb/Thumb"], - "@colorKit": ["src/colorKit/colorKit"], + "@colorKit": ["src/colorKit/index.js"], "@context": ["src/AppContext"], "@styles": ["src/styles"], "@utils": ["src/utils"],