From d8e0ee32e9ca4d7ebba30cd729279749ca073bb2 Mon Sep 17 00:00:00 2001 From: Victor Bo Date: Thu, 26 Oct 2023 09:57:22 +0800 Subject: [PATCH 1/2] feat: create color package --- package.json | 1 + packages/color/index.ts | 2 ++ packages/color/package.json | 56 +++++++++++++++++++++++++++++++++++ packages/color/src/color.ts | 3 ++ packages/color/tsup.config.ts | 9 ++++++ packages/color/types/index.ts | 1 + packages/core/index.ts | 1 + pnpm-lock.yaml | 9 ++++++ 8 files changed, 82 insertions(+) create mode 100644 packages/color/index.ts create mode 100644 packages/color/package.json create mode 100644 packages/color/src/color.ts create mode 100644 packages/color/tsup.config.ts create mode 100644 packages/color/types/index.ts diff --git a/package.json b/package.json index e6e0ec4..dab279b 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ }, "dependencies": { "@vtrbo/utils-arr": "workspace:*", + "@vtrbo/utils-color": "workspace:*", "@vtrbo/utils-is": "workspace:*", "@vtrbo/utils-log": "workspace:*", "@vtrbo/utils-obj": "workspace:*", diff --git a/packages/color/index.ts b/packages/color/index.ts new file mode 100644 index 0000000..3c00a15 --- /dev/null +++ b/packages/color/index.ts @@ -0,0 +1,2 @@ +export * from './types' +export * from './src/color' diff --git a/packages/color/package.json b/packages/color/package.json new file mode 100644 index 0000000..cba4465 --- /dev/null +++ b/packages/color/package.json @@ -0,0 +1,56 @@ +{ + "name": "@vtrbo/utils-color", + "type": "module", + "version": "0.4.0-beta.4", + "description": "Collection of common JavaScript or TypeScript utils.", + "author": { + "name": "Victor Bo", + "email": "hi@vtrbo.cn" + }, + "license": "MIT", + "homepage": "https://github.com/vtrbo", + "bugs": "https://github.com/vtrbo/utils/issues", + "keywords": [ + "typescript", + "javascript", + "utils", + "vue", + "react", + "svelte", + "vite" + ], + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "require": "./index.cjs" + } + }, + "main": "./index.js", + "module": "./index.js", + "types": "./index.d.ts", + "typesVersions": { + "*": { + "*": [ + "./*", + "./index.d.ts" + ] + } + }, + "files": [ + "README.md", + "index.cjs", + "index.d.cts", + "index.d.ts", + "index.js" + ], + "scripts": { + "build": "tsup", + "clean": "pnpm clean:dist && pnpm clean:deps", + "clean:dist": "rimraf dist", + "clean:deps": "rimraf node_modules" + }, + "dependencies": { + "@vtrbo/utils-tool": "workspace:*" + } +} diff --git a/packages/color/src/color.ts b/packages/color/src/color.ts new file mode 100644 index 0000000..c8ccc1f --- /dev/null +++ b/packages/color/src/color.ts @@ -0,0 +1,3 @@ +export function fn() { + return '' +} diff --git a/packages/color/tsup.config.ts b/packages/color/tsup.config.ts new file mode 100644 index 0000000..e2bc6d0 --- /dev/null +++ b/packages/color/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + splitting: true, +}) diff --git a/packages/color/types/index.ts b/packages/color/types/index.ts new file mode 100644 index 0000000..74a964f --- /dev/null +++ b/packages/color/types/index.ts @@ -0,0 +1 @@ +export type Test = string diff --git a/packages/core/index.ts b/packages/core/index.ts index ddc1f8f..f23fac1 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -4,3 +4,4 @@ export * from '@vtrbo/utils-log' export * from '@vtrbo/utils-obj' export * from '@vtrbo/utils-arr' export * from '@vtrbo/utils-str' +export * from '@vtrbo/utils-color' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82cc5a5..019c0fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@vtrbo/utils-arr': specifier: workspace:* version: link:packages/arr + '@vtrbo/utils-color': + specifier: workspace:* + version: link:packages/color '@vtrbo/utils-is': specifier: workspace:* version: link:packages/is @@ -88,6 +91,12 @@ importers: specifier: workspace:* version: link:../tool + packages/color: + dependencies: + '@vtrbo/utils-tool': + specifier: workspace:* + version: link:../tool + packages/is: dependencies: '@vtrbo/utils-obj': From ad15a4ac5fdac8adb22fc6c33debc7cb96fd044e Mon Sep 17 00:00:00 2001 From: Victor Bo Date: Thu, 26 Oct 2023 10:24:12 +0800 Subject: [PATCH 2/2] feat: added color functions and unit test --- packages/color/__test__/color.spec.ts | 62 ++++++++++++++++ packages/color/index.ts | 1 - packages/color/package.json | 1 + packages/color/src/color.ts | 100 +++++++++++++++++++++++++- packages/color/types/index.ts | 1 - packages/is/__test__/is.spec.ts | 22 ++++++ packages/is/src/is.ts | 9 +++ pnpm-lock.yaml | 3 + 8 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 packages/color/__test__/color.spec.ts delete mode 100644 packages/color/types/index.ts diff --git a/packages/color/__test__/color.spec.ts b/packages/color/__test__/color.spec.ts new file mode 100644 index 0000000..8012c5c --- /dev/null +++ b/packages/color/__test__/color.spec.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest' +import { darken, hexToRgba, lighten, rgbaToHex } from '../src/color' + +describe('color', () => { + it('rgbaToHex', () => { + const testData = [ + { rgba: 'rgba(0, 0, 0, 1)', expected: '#000000ff' }, + { rgba: 'rgba(255, 0, 0, 0.5)', expected: '#ff00007f' }, + { rgba: 'rgba(0, 255, 0, 0.2)', expected: '#00ff0033' }, + { rgba: 'rgb(100, 100, 100)', expected: '#646464' }, + { rgba: '#123456', expected: '#123456' }, + { rgba: 'invalid-color', expected: '' }, + ] + + for (const { rgba, expected } of testData) + expect(rgbaToHex(rgba)).toEqual(expected) + }) + + it('hexToRgba', () => { + const testData = [ + { hex: '#FFFFFF', expected: 'rgb(255, 255, 255)' }, + { hex: '#00FF00', expected: 'rgb(0, 255, 0)' }, + { hex: '#FF000099', expected: 'rgba(255, 0, 0, 0.6)' }, + { hex: '#123456', expected: 'rgb(18, 52, 86)' }, + { hex: 'invalid-color', expected: '' }, + { hex: '#cccc', expected: 'rgba(204, 204, 204, 0.8)' }, + { hex: 'rgb(0, 255, 0)', expected: 'rgb(0, 255, 0)' }, + { hex: 'rgba(0, 255, 0, 0.6)', expected: 'rgba(0, 255, 0, 0.6)' }, + ] + + for (const { hex, expected } of testData) + expect(hexToRgba(hex)).toEqual(expected) + }) + + it('lighten', () => { + const testData = [ + { color: '#FFFFFF', level: 0, expected: '#ffffff' }, + { color: '#007AFF', level: 1, expected: '#1987ff' }, + { color: '#FF0000', level: 5, expected: '#ff7f7f' }, + { color: 'rgb(200, 100, 50)', level: 7, expected: 'rgb(238, 208, 193)' }, + { color: 'rgba(150, 150, 150, 0.5)', level: 9, expected: 'rgba(244, 244, 244, 0.5)' }, + { color: 'invalid-color', level: 3, expected: '' }, + ] + + for (const { color, level, expected } of testData) + expect(lighten(color, level)).toEqual(expected) + }) + + it('darken', () => { + const testData = [ + { color: '#FFFFFF', level: 0, expected: '#ffffff' }, + { color: '#007AFF', level: 1, expected: '#006de5' }, + { color: '#FF0000', level: 5, expected: '#7f0000' }, + { color: 'rgb(200, 100, 50)', level: 7, expected: 'rgb(60, 30, 15)' }, + { color: 'rgba(150, 150, 150, 0.5)', level: 9, expected: 'rgba(15, 15, 15, 0.5)' }, + { color: 'invalid-color', level: 3, expected: '' }, + ] + + for (const { color, level, expected } of testData) + expect(darken(color, level)).toEqual(expected) + }) +}) diff --git a/packages/color/index.ts b/packages/color/index.ts index 3c00a15..b027bcf 100644 --- a/packages/color/index.ts +++ b/packages/color/index.ts @@ -1,2 +1 @@ -export * from './types' export * from './src/color' diff --git a/packages/color/package.json b/packages/color/package.json index cba4465..dc41448 100644 --- a/packages/color/package.json +++ b/packages/color/package.json @@ -51,6 +51,7 @@ "clean:deps": "rimraf node_modules" }, "dependencies": { + "@vtrbo/utils-color": "workspace:*", "@vtrbo/utils-tool": "workspace:*" } } diff --git a/packages/color/src/color.ts b/packages/color/src/color.ts index c8ccc1f..f2ec50b 100644 --- a/packages/color/src/color.ts +++ b/packages/color/src/color.ts @@ -1,3 +1,101 @@ -export function fn() { +import { isColor } from '@vtrbo/utils-is' + +export function rgbaToHex(rgba: string): string { + if (isColor(rgba, 'HEX')) + return rgba + + if (!isColor(rgba, 'RGB') && !isColor(rgba, 'RGBA')) + return '' + + const rgbaValue = rgba.replace('rgba(', '').replace('rgb(', '').replace(')', '') + const [r, g, b, a] = rgbaValue.split(',').map(m => +m) + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}${(a || a === 0) ? (a * 255 | 1 << 8).toString(16).slice(1) : ''}` return '' } + +export function hexToRgba(hex: string): string { + if (isColor(hex, 'RGB') || isColor(hex, 'RGBA')) + return hex + + if (!isColor(hex, 'HEX')) + return '' + + const getSingle = (start: number, end: number) => Number.parseInt(`0x${hex.slice(start, end)}${hex.slice(start, end)}`) + const getDouble = (start: number, end: number) => Number.parseInt(`0x${hex.slice(start, end)}`) + const getAlpha = (start: number, end: number, fn: typeof getSingle | typeof getDouble) => Math.round(fn(start, end) / 255 * 100) / 100 + + const hexMap: { + [key: number]: string + } = { + 4: `rgb(${getSingle(1, 2)}, ${getSingle(2, 3)}, ${getSingle(3, 4)})`, + 5: `rgba(${getSingle(1, 2)}, ${getSingle(2, 3)}, ${getSingle(3, 4)}, ${getAlpha(4, 5, getSingle)})`, + 7: `rgb(${getDouble(1, 3)}, ${getDouble(3, 5)}, ${getDouble(5, 7)})`, + 9: `rgba(${getDouble(1, 3)}, ${getDouble(3, 5)}, ${getDouble(5, 7)}, ${getAlpha(7, 9, getDouble)})`, + } + return hexMap[hex.length] || '' +} + +export function lighten(color: string, level: number = 10): string { + if (!isColor(color, 'HEX') && !isColor(color, 'RGB') && !isColor(color, 'RGBA')) + return '' + + let rgbaColor: string = '' + let rgba: number[] = [] + let type: 'HEX' | 'RGB' | 'RGBA' = 'RGBA' + if (isColor(color, 'HEX')) { + rgbaColor = hexToRgba(color) + type = 'HEX' + } + else { + rgbaColor = color + type = isColor(color, 'RGBA') ? 'RGBA' : 'RGB' + } + + const rgbaValue = rgbaColor.replace('rgba(', '').replace('rgb(', '').replace(')', '') + rgba = rgbaValue.split(',').map(m => +m) + + for (let i = 0; i < 3; i++) rgba[i] = Math.floor((255 - rgba[i]) * level / 10 + rgba[i]) + + const typeMap = { + HEX: '', + RGB: `rgb(${rgba.join(', ')})`, + RGBA: `rgba(${rgba.join(', ')})`, + } + + const lightenColor = typeMap[type] + + return lightenColor || rgbaToHex(rgba.length === 3 ? typeMap.RGB : typeMap.RGBA) +} + +export function darken(color: string, level: number = 0): string { + if (!isColor(color, 'HEX') && !isColor(color, 'RGB') && !isColor(color, 'RGBA')) + return '' + + let rgbaColor: string = '' + let rgba: number[] = [] + let type: 'HEX' | 'RGB' | 'RGBA' = 'RGBA' + if (isColor(color, 'HEX')) { + rgbaColor = hexToRgba(color) + type = 'HEX' + } + else { + rgbaColor = color + type = isColor(color, 'RGBA') ? 'RGBA' : 'RGB' + } + + const rgbaValue = rgbaColor.replace('rgba(', '').replace('rgb(', '').replace(')', '') + rgba = rgbaValue.split(',').map(m => +m) + + for (let i = 0; i < 3; i++) rgba[i] = Math.floor(rgba[i] * (10 - level) / 10) + + const typeMap = { + HEX: '', + RGB: `rgb(${rgba.join(', ')})`, + RGBA: `rgba(${rgba.join(', ')})`, + } + + const darkenColor = typeMap[type] + + return darkenColor || rgbaToHex(rgba.length === 3 ? typeMap.RGB : typeMap.RGBA) +} diff --git a/packages/color/types/index.ts b/packages/color/types/index.ts deleted file mode 100644 index 74a964f..0000000 --- a/packages/color/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type Test = string diff --git a/packages/is/__test__/is.spec.ts b/packages/is/__test__/is.spec.ts index df9fa49..972706a 100644 --- a/packages/is/__test__/is.spec.ts +++ b/packages/is/__test__/is.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { isArray, isBoolean, + isColor, isDate, isEmptyArr, isEmptyObj, @@ -126,6 +127,27 @@ describe('is', () => { expect(isMobile('037166668888')).toBeFalsy() }) + it('isColor', async () => { + expect(isColor('#fff', 'HEX')).toBe(true) + expect(isColor('#F0f0F0', 'HEX')).toBe(true) + expect(isColor('#12345678', 'HEX')).toBe(true) + expect(isColor('#fgh', 'HEX')).toBe(false) + expect(isColor('#1234', 'HEX')).toBe(true) + expect(isColor('#123456789', 'HEX')).toBe(false) + expect(isColor('rgb(255, 0, 0)', 'RGB')).toBe(true) + expect(isColor('Rgb( 0,255,0 )', 'RGB')).toBe(true) + expect(isColor('rgb( 0 , 0 , 255)', 'RGB')).toBe(true) + expect(isColor('rgb(256, 0, 0)', 'RGB')).toBe(false) + expect(isColor('rgba(0, 255, 0)', 'RGB')).toBe(false) + expect(isColor('rgb(0, 0, 256)', 'RGB')).toBe(false) + expect(isColor('rgba(255, 0, 0, 1)', 'RGBA')).toBe(true) + expect(isColor('rgba(0, 255, 0, 0.5)', 'RGBA')).toBe(true) + expect(isColor('rgba(0, 0, 255, 0.1)', 'RGBA')).toBe(true) + expect(isColor('rgba(256, 0, 0)', 'RGBA')).toBe(false) + expect(isColor('rgba(0, 255, 0, 1.5)', 'RGBA')).toBe(false) + expect(isColor('rgba(0, 0, 256, 0)', 'RGBA')).toBe(false) + }) + it('isEmptyObj', async () => { expect(isEmptyObj({})).toBeTruthy() expect(isEmptyObj({ foo: 'bar' })).toBeFalsy() diff --git a/packages/is/src/is.ts b/packages/is/src/is.ts index e020600..14851ab 100644 --- a/packages/is/src/is.ts +++ b/packages/is/src/is.ts @@ -72,6 +72,15 @@ export function isMobile(mobile: string) { return reg.test(mobile) } +export function isColor(color: string, type: 'HEX' | 'RGB' | 'RGBA'): boolean { + const typeMap = { + HEX: /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/g, + RGB: /^[rR][gG][bB][\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\s]*,[\s]*){2}([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\s]*){1}[\)]$/g, + RGBA: /^[rR][gG][bB][aA][\(]([\s]*(2[0-4][0-9]|25[0-5]|[01]?[0-9][0-9]?)[\s]*,[\s]*){3}[\s]*(1|1.0|0|0.[0-9])[\s]*[\)]{1}$/g, + } + return typeMap[type].test(color) +} + export function isEmptyObj(obj: unknown): boolean { return isObject(obj) && !objKeys(obj).length } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 019c0fc..7a04589 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: packages/color: dependencies: + '@vtrbo/utils-color': + specifier: workspace:* + version: 'link:' '@vtrbo/utils-tool': specifier: workspace:* version: link:../tool