Skip to content

Commit

Permalink
[color engine] Implement rgb() function
Browse files Browse the repository at this point in the history
  • Loading branch information
arnelenero committed Mar 27, 2022
1 parent deec55b commit 2d4b3fe
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 45 deletions.
29 changes: 29 additions & 0 deletions src/color/__tests__/named.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import named, { namedColors } from '../named'

describe('named', () => {
Object.keys(namedColors).forEach(colorName => {
it(`returns the hex string value for recognized color name: ${colorName}`, () => {
const color = named(colorName)
expect(typeof color).toBe('string')
expect(color?.charAt(0)).toBe('#')
})
})

it('is not case sensitive', () => {
expect(named('royalblue')).toBeDefined()
expect(named('RoyalBlue')).toBeDefined()
})

const invalid = [
'#33CCFF', // hex color value
'rgb(127, 255, 255)', // non-hex color value
' blue ', // untrimmed spaces
'transparent', // special keyword
'rainbow', // invalid color string
]
invalid.forEach(str => {
it(`returns undefined for invalid color name: ${str}`, () => {
expect(named(str)).toBeUndefined()
})
})
})
30 changes: 30 additions & 0 deletions src/color/__tests__/rgb.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import rgb from '../rgb'

describe('rgb', () => {
const rgbaHex = ['#FFAACC33']
rgbaHex.forEach(color => {
it(`returns an object with RGB values in decimal, alpha as fraction, for: ${color}`, () => {
expect(rgb(color)).toEqual({ r: 255, g: 170, b: 204, a: 0.2 })
})
})

const rgbHex = ['#FFAACC']
rgbHex.forEach(color => {
it(`returns default alpha value of 1 for: ${color}`, () => {
expect(rgb(color)).toEqual({ r: 255, g: 170, b: 204, a: 1 })
})
})

it('expands shorthand hex value', () => {
expect(rgb('#FAC3')).toEqual({ r: 255, g: 170, b: 204, a: 0.2 })
})

it('returns RGB values from CSS color names', () => {
expect(rgb('blue')).toEqual({ r: 0, g: 0, b: 255, a: 1 })
expect(rgb('yellow')).toEqual({ r: 255, g: 255, b: 0, a: 1 })
})

it('returns null if not a valid color string', () => {
expect(rgb('foo')).toBeNull()
})
})
18 changes: 12 additions & 6 deletions src/color/types/colorName.ts → src/color/named.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const named = {
import type { HexString } from './parsers/hexString'

export const namedColors: Record<string, HexString> = {
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aqua: '#00FFFF',
Expand Down Expand Up @@ -147,10 +149,14 @@ export const named = {
whitesmoke: '#F5F5F5',
yellow: '#FFFF00',
yellowgreen: '#9ACD32',
} as const

export type ColorName = keyof typeof named
}

export function isColorName(color: string): color is ColorName {
return color.toLowerCase() in named
/**
* Looks up the hex value of a given color name
*
* @param colorName
* @returns color hex string (or undefined if invalid color name)
*/
export default function named(colorName: string): HexString | undefined {
return namedColors[colorName.toLowerCase()]
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ describe('matchHexString', () => {
expect(matchHexString(str)).toEqual(['FF', 'AA', 'CC', 'EE'])
})

it('returns alpha value as undefined if not specified in string', () => {
it('returns only a 3-item array if color has no alpha value', () => {
const str = '#ffaabb'
expect(matchHexString(str)).toEqual(['ff', 'aa', 'bb', undefined])
expect(matchHexString(str)).toEqual(['ff', 'aa', 'bb'])
})

it('captures shorthand RGB and alpha hex values', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ describe('matchHslString', () => {
expect(matchHslString(str)).toEqual(['-240', '-100', '-50', '-0.1'])
})

it('returns alpha value as undefined if not specified in string', () => {
it('returns only a 3-item array if color has no alpha value', () => {
const str = 'hsl(240, 100%, 50%)'
expect(matchHslString(str)).toEqual(['240', '100', '50', undefined])
expect(matchHslString(str)).toEqual(['240', '100', '50'])
})

it('returns null if string is not a valid hsl string', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ describe('matchRgbString', () => {
expect(matchRgbString(str)).toEqual(['-127', '-255', '-64', '-0.1'])
})

it('returns alpha value as undefined if not specified in string', () => {
it('returns only a 3-item array if color has no alpha value', () => {
const str = 'rgb(127, 255, 64)'
expect(matchRgbString(str)).toEqual(['127', '255', '64', undefined])
expect(matchRgbString(str)).toEqual(['127', '255', '64'])
})

it('returns null if string is not a valid rgb string', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {
alphaSeparatorMatcher,
cssNumberMatcher,
exact,
extractValuesFromMatch,
separatorMatcher,
} from '../utils'

describe('exact', () => {
it('creates a new regex that is restricted to exact matches only', () => {
it('returns a new regex that is restricted to exact matches only', () => {
const regex = /[0-9]+/
const exactRegex = exact(regex)
expect(regex.test(' 123 ')).toBe(true)
Expand All @@ -20,6 +21,18 @@ describe('exact', () => {
})
})

describe('extractValuesFromMatch', () => {
it('returns an array containing only the color components', () => {
const match = ['#ffaaddee', 'ff', 'aa', 'dd', 'ee'] as RegExpExecArray
expect(extractValuesFromMatch(match)).toEqual(['ff', 'aa', 'dd', 'ee'])
})

it('removes undefined items', () => {
const match = ['#ffaadd', 'ff', 'aa', 'dd', undefined] as RegExpExecArray
expect(extractValuesFromMatch(match)).toEqual(['ff', 'aa', 'dd'])
})
})

describe('cssNumberMatcher', () => {
const matcher = exact(cssNumberMatcher)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exact } from './utils'
import { exact, extractValuesFromMatch } from './utils'

export type HexString = `#${string}`

Expand Down Expand Up @@ -39,6 +39,5 @@ export function matchHexString(colorString: string): string[] | null {
const match =
exact(hexColorMatcher).exec(colorString) ??
exact(shortHexColorMatcher).exec(colorString)

return match?.slice(1) ?? null
return match ? extractValuesFromMatch(match) : null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
alphaSeparatorMatcher,
cssNumberMatcher,
exact,
extractValuesFromMatch,
separatorMatcher,
} from './utils'

Expand Down Expand Up @@ -45,5 +46,5 @@ export function isHslString(colorString: string): colorString is HslString {
*/
export function matchHslString(colorString: string): string[] | null {
const match = exact(hslMatcher).exec(colorString)
return match?.slice(1) ?? null
return match ? extractValuesFromMatch(match) : null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
alphaSeparatorMatcher,
cssNumberMatcher,
exact,
extractValuesFromMatch,
separatorMatcher,
} from './utils'

Expand Down Expand Up @@ -43,5 +44,5 @@ export function isRgbString(colorString: string): colorString is RgbString {
*/
export function matchRgbString(colorString: string): string[] | null {
const match = exact(rgbMatcher).exec(colorString)
return match?.slice(1) ?? null
return match ? extractValuesFromMatch(match) : null
}
13 changes: 13 additions & 0 deletions src/color/types/utils.ts → src/color/parsers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ export function exact(regex: RegExp): RegExp {
return new RegExp(`^${regex.source}$`, regex.flags)
}

/**
* Extracts individual color components from a color string
* match array
*
* @param match
* @returns a string array containing the extracted values
*/
export function extractValuesFromMatch(match: RegExpExecArray): string[] {
return match
.slice(1) // get only the values from regex capturing groups
.filter(val => val !== undefined) // remove undefined items (e.g. alpha)
}

/** Regular expression for valid CSS number */
export const cssNumberMatcher = /[+-]?(?=\.\d|\d)\d*(?:\.\d+)?(?:[eE][+-]?\d+)?/

Expand Down
34 changes: 34 additions & 0 deletions src/color/rgb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import named from './named'
import { isHexString, matchHexString } from './parsers/hexString'

export interface RGB {
r: number
g: number
b: number
a?: number
}

export function rgbFromHexString(colorString: string): RGB | null {
const match = matchHexString(colorString)
if (!match) return null

const rgbValues = match.map(val => {
// Expand if value is shorthand (single digit) hex
if (val.length === 1) val = `${val}${val}`
// Convert hex to decimal
return parseInt(val, 16)
})

// Compute alpha as fraction of 255, defaulting to 1
const alpha = (rgbValues[3] ?? 255) / 255

return { r: rgbValues[0], g: rgbValues[1], b: rgbValues[2], a: alpha }
}

export default function rgb(colorString: string): RGB | null {
// Get hex value if string is a color name
const hexFromName = named(colorString)
if (hexFromName) colorString = hexFromName

return rgbFromHexString(colorString)
}
27 changes: 0 additions & 27 deletions src/color/types/_tests_/colorName.spec.ts

This file was deleted.

0 comments on commit 2d4b3fe

Please sign in to comment.