Skip to content

Commit 64bc524

Browse files
committed
First Commit
1. add script 2. add readme
1 parent 935cc7b commit 64bc524

File tree

3 files changed

+265
-2
lines changed

3 files changed

+265
-2
lines changed

README.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,66 @@
1-
# SafeBG
2-
With your given color, generate accessible, random color that compiles with WCAG success criteria 1.4.3 (or any contrast ratio of your choice)
1+
# SafeColor
2+
SafeColor generates accessible colors that compiles with WCAG success criteria 1.4.3 (or any contrast ratio of your choice).
3+
It can be used to:
4+
5+
1. generate a random color that is contrast safe with a given color
6+
2. generate a consistent, contrast safe color for a string
7+
8+
No need to worry about your base color is light/dark or for foreground/background. If the given color is too light to meet your desired contrast ratio, SafeColor will look for a darker color and vise versa.
9+
10+
11+
## Install
12+
13+
`npm install safecolor`
14+
15+
## Usage
16+
17+
`import SafeColor from 'safecolor'`
18+
19+
### Basic
20+
21+
```javascript
22+
// this will assume that the generated color should be contrast safe (>= AA standard: 4.5) with black(rgb(0, 0, 0))
23+
safeColor = new SafeColor()
24+
safeColor.random()
25+
// >> rgb(104, 145, 26)
26+
// contrast ratio = 5.65
27+
safeColor.random('hello world')
28+
// >> rgb(196,226,239)
29+
// contrast ratio = 15.47
30+
```
31+
### With options
32+
33+
```javascript
34+
safeColor = new SafeColor({
35+
color: [255, 255, 255], // 8bit RGB value in array [r, g, b]
36+
contrast: 4.5, // the contrast ratio between the color above and the generated color will be larger or equal to this
37+
})
38+
safeColor.random()
39+
// >> rgb(32,80,46)
40+
// contrast ratio = 9.34
41+
safeColor.random('hello world')
42+
// >> rgb(20,57,74)
43+
// contrast ratio = 12.25
44+
```
45+
46+
## Options
47+
48+
**color**
49+
50+
- type: `Array`
51+
- default: `[0, 0, 0]`
52+
53+
**contrast**
54+
55+
- type: `Number`
56+
- default: `4.5`
57+
58+
## Notice
59+
ES6 destructing and map
60+
61+
Note: to keep this as simple as possible, the output is a RGB value in string. If any built-in conversions (to HEX, to HSL) will make SafeColor much more convenient for you, please contact me to add the feature or feel free to pull request. Cheers!
62+
63+
64+
65+
66+

package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "safecolor",
3+
"version": "1.0.0",
4+
"description": "SafeColor generates accessible colors that compiles with WCAG success criteria 1.4.3 (or any contrast ratio of your choice). It can generate either a random color that is contrast safe with a given color, or a consistent color for a given string.",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "test"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/jessuni/SafeColor.git"
12+
},
13+
"keywords": [
14+
"wacg",
15+
"contrast-ratio",
16+
"accessibility",
17+
"color",
18+
"string",
19+
"hex",
20+
"rgb",
21+
"hsl",
22+
"hci",
23+
"luminance",
24+
"luma"
25+
],
26+
"author": "jessunix@gmail.com",
27+
"license": "MIT",
28+
"bugs": {
29+
"url": "https://github.com/jessuni/SafeColor/issues"
30+
},
31+
"homepage": "https://github.com/jessuni/SafeColor#readme"
32+
}

safecolor.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* @author jessuni
3+
*/
4+
5+
6+
const _CONFIG = {
7+
color: [0, 0, 0],
8+
contrast: 4.5,
9+
}
10+
11+
class SafeColor {
12+
/**
13+
* Create a SafeColor instance with options
14+
* @param {Array} color 8bit RGB value of a given color in the form of [R, G, B]
15+
* @param {Number} contrast contrast ratio of foreground color and background color https://www.w3.org/TR/WCAG20/#contrast-ratiodef
16+
*/
17+
constructor(options = {}) {
18+
this.color = options.color || _CONFIG.color
19+
this.contrast = options.contrast || _CONFIG.contrast
20+
this.hash = 0
21+
}
22+
23+
_rgb2hsl([r, g, b]) {
24+
r /= 255
25+
g /= 255
26+
b /= 255
27+
const cmax = Math.max(r, g, b)
28+
const cmin = Math.min(r, g, b)
29+
const chroma = cmax - cmin
30+
let l = (cmax + cmin) / 2
31+
let h, s
32+
if (!chroma) {
33+
h = s = 0
34+
} else {
35+
s = chroma / (1 - Math.abs(2 * l - 1))
36+
if (cmax === r) {
37+
h = ((g - b) / chroma) % 6
38+
} else if (cmax === g) {
39+
h = (b - r) / chroma + 2
40+
} else {
41+
h = (r - g) / chroma + 4
42+
}
43+
h = Math.round(h * 60)
44+
h = h < 0 ? h + 360 : h
45+
}
46+
return [h, s, l]
47+
}
48+
49+
_hsl2rgb([h, s, l]) {
50+
let c = (1 - Math.abs(2 * l - 1)) * s
51+
let x = c * (1 - Math.abs((h / 60) % 2 - 1))
52+
let m = l - c / 2
53+
let r = 0
54+
let g = 0
55+
let b = 0
56+
if (h >= 0 && h < 60) {
57+
r = c
58+
g = x
59+
b = 0
60+
} else if (h >= 60 && h < 120) {
61+
r = x
62+
g = c
63+
b = 0
64+
} else if (h >= 120 && h < 180) {
65+
r = 0
66+
g = c
67+
b = x
68+
} else if (h >= 180 && h < 240) {
69+
r = 0
70+
g = x
71+
b = c
72+
} else if (h >= 240 && h < 300) {
73+
r = x
74+
g = 0
75+
b = c
76+
} else if (h >= 300 && h < 360) {
77+
r = c
78+
g = 0
79+
b = x
80+
}
81+
r = Math.round((r + m) * 255)
82+
g = Math.round((g + m) * 255)
83+
b = Math.round((b + m) * 255)
84+
return [r, g, b]
85+
}
86+
87+
/**
88+
* Calculate the luma of a given sRGB array
89+
* @param {Array} sRGB
90+
* @return luma of the color
91+
*/
92+
calLuma(sRGB) {
93+
const [linearR, linearG, linearB] = sRGB.map(v => {
94+
const decimal = v / 255
95+
return decimal <= 0.04045 ? decimal / 12.92 : Math.pow((decimal + 0.055) / 1.055, 2.4)
96+
})
97+
return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722
98+
}
99+
100+
/**
101+
* Calculate the valid luma range with the provided color and the contrast ratio
102+
* @param {Array} fgColor
103+
* @param {Number} contrastRatio
104+
*/
105+
calValidLumaRange(fgColor, contrastRatio) {
106+
const fgLuma = this.calLuma(fgColor)
107+
const edgeLuma = (fgLuma + 0.05) / contrastRatio - 0.05
108+
if (edgeLuma < 1 && edgeLuma > 0) {
109+
this.minLuma = 0
110+
this.maxLuma = edgeLuma
111+
} else if (edgeLuma === 1 || edgeLuma === 0) {
112+
this.minLuma = this.maxLuma = edgeLuma
113+
} else {
114+
this.minLuma = (fgLuma + 0.05) * contrastRatio
115+
this.maxLuma = 1
116+
}
117+
}
118+
119+
/**
120+
* Check if generated color's luma is within the valid luma range. If not, launder the color until it is
121+
* @param {Array} color
122+
*/
123+
launderColor(color) {
124+
const luma = this.calLuma(color)
125+
if (luma >= this.minLuma && luma <= this.maxLuma) {
126+
return color
127+
} else {
128+
const hsl = this._rgb2hsl(color)
129+
if (luma < this.minLuma) {
130+
const step = this.minLuma - luma
131+
hsl[2] = this.hash ? step + luma : (1 - hsl[2]) * Math.random() + hsl[2]
132+
color = this._hsl2rgb(hsl)
133+
return this.launderColor(this.color, color)
134+
} else {
135+
const step = luma - this.maxLuma
136+
hsl[2] = this.hash ? luma - step : Math.random() % hsl[2]
137+
color = this._hsl2rgb(hsl)
138+
return this.launderColor(this.color, color)
139+
}
140+
}
141+
}
142+
/**
143+
* Generates a random color that meets (>=) the contrast ratio. If a string is passed in, generate a consistent contrast-safe color for that string.
144+
* @param {String} str
145+
*/
146+
random(str) {
147+
this.calValidLumaRange(this.color, this.contrast)
148+
if (this.minLuma === this.maxLuma) {
149+
this.genColor = this.minLuma ? [255, 255, 255] : [0, 0, 0]
150+
return
151+
}
152+
if (!str) {
153+
this.genColor = [Math.random() * 255, Math.random() * 255, Math.random() * 255]
154+
} else {
155+
for (let i = 0; i < str.length; i++) {
156+
this.hash = str.charCodeAt(i) + ((this.hash << 5) - this.hash)
157+
this.hash = this.hash & this.hash
158+
}
159+
this.genColor = [0, 0, 0]
160+
this.genColor = this.genColor.map((v, index) => (v = (this.hash >> (index * 8)) & 255))
161+
}
162+
this.genColor = this.launderColor(this.color, this.genColor)
163+
return 'rgb(' + this.genColor + ')'
164+
}
165+
}
166+
167+
export default SafeColor

0 commit comments

Comments
 (0)