Skip to content

Commit

Permalink
feat: add luminance contrast (#1)
Browse files Browse the repository at this point in the history
- color
- contrast for ratio
- luminance calc
  • Loading branch information
hikariNTU authored May 29, 2022
1 parent 7f05917 commit a725118
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 3 deletions.
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"node": ">=14",
"pnpm": ">=6.25.0"
},
"main": "dist/plugin.js",
"scripts": {},
"main": "dist/index.js",
"scripts": {
"test": "vitest"
},
"author": "hikariNTU",
"license": "MIT",
"repository": {
Expand All @@ -32,6 +34,7 @@
"prettier": "^2.5.1",
"tsm": "^2.2.1",
"typescript": "^4.6.0",
"vite": "^2.8.0"
"vite": "^2.8.0",
"vitest": "^0.13.0"
}
}
22 changes: 22 additions & 0 deletions packages/contrast/color.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest'
import { fromHEX, purifyHEX } from './color'

describe('color', () => {
it('purifyHex to 6-digit string', () => {
expect(purifyHEX('#F367DD18')).toBe('F367DD')
expect(purifyHEX('#CCC')).toBe('CCCCCC')
expect(purifyHEX('#2222')).toBe('222222')
})

it('convert HEX color to weight number', () => {
expect(fromHEX('#000')).toEqual([0, 0, 0])
expect(fromHEX('#66666666')).toEqual([0.4, 0.4, 0.4])
expect(fromHEX('f06')).toEqual([1, 0, 0.4])
})

it('will throw error', () => {
expect(() => fromHEX('0')).toThrow()
// @ts-expect-error testing
expect(() => fromHEX()).toThrow()
})
})
21 changes: 21 additions & 0 deletions packages/contrast/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const purifyHEX = (hex: string) => {
const str = hex.startsWith('#') ? hex.slice(1) : hex
return str.length <= 4
? `${str[0].repeat(2)}${str[1].repeat(2)}${str[2].repeat(2)}`
: str.slice(0, 6)
}

/**
* Get each channel weight value from HEX code
* @param hex HEX color string, can omit '#' and support all 3-digit 4-digit, 6-digit, 8-digit color LV4 version.
* @returns Array of `[R, G, B]` value from 0 to 1
*/
export const fromHEX = (hex: string): [number, number, number] => {
const hex6 = purifyHEX(hex)

return [
parseInt(hex6.slice(0, 2), 16) / 255,
parseInt(hex6.slice(2, 4), 16) / 255,
parseInt(hex6.slice(4, 6), 16) / 255,
]
}
14 changes: 14 additions & 0 deletions packages/contrast/contrast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest'
import { getContrastRatio } from './contrast'

describe('contrast', () => {
it('getContrastRatio with given data', () => {
expect(getContrastRatio('#000', '#FFF')).toBe(21)
expect(getContrastRatio('#222', '#222')).toBe(1)

// chrome: 3.51, chrome floor to 2 digit
expect(Math.floor(getContrastRatio('#DCB487', '#135F96') * 100) / 100).toBe(
3.51
)
})
})
10 changes: 10 additions & 0 deletions packages/contrast/contrast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { fromHEX } from './color'
import { toRelativeLuminance } from './luminance'

const SHIFT = 0.05

export const getContrastRatio = (c1: string, c2: string): number => {
const l1 = toRelativeLuminance(...fromHEX(c1)) + SHIFT
const l2 = toRelativeLuminance(...fromHEX(c2)) + SHIFT
return l1 > l2 ? l1 / l2 : l2 / l1
}
3 changes: 3 additions & 0 deletions packages/contrast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './color'
export * from './luminance'
export * from './contrast'
24 changes: 24 additions & 0 deletions packages/contrast/luminance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect } from 'vitest'
import { standardizeWeight, toRelativeLuminance } from './luminance'

describe('luminance', () => {
it('standardizeColor with given weight', () => {
expect(standardizeWeight(0), 'black to black').toBe(0)
expect(standardizeWeight(1), 'white to white').toBe(1)
})

it('convert given color to relative luminance', () => {
expect(toRelativeLuminance(0, 0, 0)).toBe(0)
expect(toRelativeLuminance(1, 1, 1)).toBe(1)

expect(
toRelativeLuminance(65 / 255, 90 / 255, 110 / 255),
'#415A6E = 0.095619'
).toBeCloseTo(0.095619, 5)

expect(
toRelativeLuminance(220 / 255, 180 / 255, 135 / 255),
'#DCB487 = 0.49607'
).toBeCloseTo(0.49607, 5)
})
})
36 changes: 36 additions & 0 deletions packages/contrast/luminance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const R_WEIGHT = 0.2126
const G_WEIGHT = 0.7152
const B_WEIGHT = 0.0722

const L_THRESHOLD = 0.04045
const L_LINEAR_SLOPE = 12.92
const GAMMA = 2.4

/**
* channel weight in RGB system, 0~1 value.
*/
export type ColorWeight = number

/**
* Standardize color weight into perceptual weight
* @param c
* @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
*/
export const standardizeWeight = (c: ColorWeight): ColorWeight =>
c <= L_THRESHOLD ? c / L_LINEAR_SLOPE : ((c + 0.055) / 1.055) ** GAMMA

/**
* Standardize color weight into perceptual weight
* @param r sRGB red value in 0~1
* @param g sRGB green value in 0~1
* @param b sRGB blue value in 0~1
* @see https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
*/
export const toRelativeLuminance = (
r: ColorWeight,
g: ColorWeight,
b: ColorWeight
): number =>
R_WEIGHT * standardizeWeight(r) +
G_WEIGHT * standardizeWeight(g) +
B_WEIGHT * standardizeWeight(b)
108 changes: 108 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 a725118

Please sign in to comment.