Skip to content

Commit

Permalink
feat: added color package (#37)
Browse files Browse the repository at this point in the history
* feat: create color package

* feat: added color functions and unit test
  • Loading branch information
vtrbo committed Oct 26, 2023
1 parent 93f5c37 commit c4b3258
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
62 changes: 62 additions & 0 deletions packages/color/__test__/color.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
1 change: 1 addition & 0 deletions packages/color/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/color'
57 changes: 57 additions & 0 deletions packages/color/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"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-color": "workspace:*",
"@vtrbo/utils-tool": "workspace:*"
}
}
101 changes: 101 additions & 0 deletions packages/color/src/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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)
}
9 changes: 9 additions & 0 deletions packages/color/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'tsup'

export default defineConfig({
entry: ['index.ts'],
format: ['cjs', 'esm'],
dts: true,
clean: true,
splitting: true,
})
1 change: 1 addition & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
22 changes: 22 additions & 0 deletions packages/is/__test__/is.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
isArray,
isBoolean,
isColor,
isDate,
isEmptyArr,
isEmptyObj,
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions packages/is/src/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c4b3258

Please sign in to comment.