Learn about the relationship between Hue, Saturation, Brightness, and Color Contrast. Minimum contrast level is set to 4.5 (AA).
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.16.2/math.js"></script>
<script src="https://use.typekit.net/nmk5cke.js"></script>
<script>try{Typekit.load({ async: true });}catch(e){}</script>
<style>
body {
padding: 0;
color: white;
font-family: Proxima Nova, 'proxima-nova',-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #FAFBFC;
}
.main {
max-width: 760px;
text-align: center;
}
h1 {
padding: 24px 0;
}
.description {
margin-top: 32px;
}
i {
margin-right: 4px;
}
a {
color: #13DCC1;
transition: .2s ease;
text-decoration: none;
}
a:hover {
color: #00A4FF;
}
.description img {
height: 16px;
margin-right: 4px;
}
.content {
color: #4a4a4a;
text-align: left;
border-radius: 2px;
margin-top: 24px;
padding: 24px 24px;
}
.content h1 {
margin: 0;
}
#section1 {
display:flex;
justify-content: flex-start;
}
#section1 > div {
margin-right: 8px;
}
#colorSpaceContainer {
display: block;
position: relative;
width: 200px;
height: 200px;
}
#colorSpaceContainer > * {
position: absolute;
top:0;
left:0;
}
#colorSpace {
display: block;
height: 200px;
}
#hueChannelContainer {
display: block;
position: relative;
width: 200px;
height: 10px;
margin: 8px 0;
}
#hueChannelContainer > * {
position: absolute;
top: 0;
left: 0;
}
#hueNubSpace {
}
#hueChannel {
width: 200px;
height: 10px;
}
.formTitle {
text-transform: uppercase;
font-weight: bold;
letter-spacing: 1px;
font-weight: bold;
font-size: 14px;
}
.formTitle > div {
font-size: 16px;
font-weight: bold;
}
.colorInput {
margin-top: 4px;
}
.colorInput input {
float: right;
}
.textColorTitle {
margin-top: 16px;
}
.buttonAndContrast {
width: 243px;
display: flex;
flex-direction: column;
}
.buttonAndContrast div {
align-self: center;
margin-bottom: 4px;
}
#myButton {
padding: 12px 24px;
display: inline-block;
letter-spacing: 1px;
}
#contrastValue {
font-weight: bold;
font-feature-settings: "tnum"
}
#optionalWarning {
font-size: 12px;
font-weight: bold;
color: red;
letter-spacing: .5px;
}
</style>
</head>
<body>
<div class="main">
<div class="content">
<h1>Accessible Color Spaces</h1>
<div class="section" id='section1'>
<div id="colorPicker">
<div id="colorSpaceContainer">
<canvas width="100" height="100" id="colorSpace"></canvas>
<svg width="200" height ="200" id="satBrightSpace"></svg>
</div>
<div id="hueChannelContainer">
<canvas width="360" height="1" id="hueChannel"></canvas>
<svg width="200" height="10" id="hueNubSpace"></svg>
</div>
</div>
<div id='inputs'>
<div class="formTitle">BUTTON / BG COLOR</div>
<div class='colorInput'>
Hue
<input type="number" name="hueInput" min="0" max="360" inputmode="numeric" pattern="[0-9]*" id="hueInput" class="buttonColor">
</div>
<div class='colorInput'>
Saturation
<input type="number" name="satInput" min="0" max="100" inputmode="numeric" pattern="[0-9]*" id="satInput" class="buttonColor">
</div>
<div class='colorInput'>
Brightness
<input type="number" name="brightInput" min="0" max="100" inputmode="numeric" pattern="[0-9]*" id="brightInput" class="buttonColor">
</div>
<div class="formTitle textColorTitle">TEXT COLOR</div>
<div class='colorInput'>
Hue
<input type="number" name="textHueInput" min="0" max="360" inputmode="numeric" pattern="[0-9]*" id="textHueInput" class="buttonColor">
</div>
<div class='colorInput'>
Saturation
<input type="number" name="textSatInput" min="0" max="100" inputmode="numeric" pattern="[0-9]*" id="textSatInput" class="buttonColor">
</div>
<div class='colorInput'>
Brightness
<input type="number" name="textBrightInput" min="0" max="100" inputmode="numeric" pattern="[0-9]*" id="textBrightInput" class="buttonColor">
</div>
</div>
<div class="buttonAndContrast">
<div id='myButton'>My Button</div>
<div id="contrastValue">Contrast: 2.6:1</div>
<div id="optionalWarning">! WARNING: Below Contrast Requirement</div>
</div>
</div>
</div>
</div>
<script>
// Color JS
function normHSV(color) {
color = {
h: color.h / 360.0,
s: color.s / 100.0,
v: color.v / 100.0,
}
return color
}
function normRGB(color) {
color = {
r: color.r / 255.0,
g: color.g / 255.0,
b: color.b / 255.0
}
return color
}
function HSVtoRGB(color) {
var h,s,v,i,f,p,q,t
h = color.h;
s = color.s;
v = color.v;
i = math.floor(h * 6);
f = (h * 6) - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
color = {
r: r,
g: g,
b: b
}
return color
}
function RGBtoHSV(color) {
var r = color.r;
var g = color.g;
var b = color.b;
var max = Math.max(r,g,b);
var min = Math.min(r,g,b);
var h, s, v = max;
var d = max - min;
s = max == 0 ? 0 : d / max;
if (max == min) {
h = 0;
} else {
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6
}
var tempHSVColor = {
h: h,
s: s,
v: v
}
return tempHSVColor;
}
function getColorContrast(color1, color2) {
if (color1.r > 1 || color1.g > 1 || color1.b > 1) {
return "ERROR: Color1 out of range."
}
if (color2.r > 1 || color2.g > 1 || color2.b > 1) {
return "ERROR: Color2 out of range."
}
// Color 1
// Get Red Value of Color 1
var L1R = color1.r;
if (L1R <= 0.03928) {
L1R = L1R / 12.92;
} else {
L1R = ((L1R + 0.055) / 1.055)**2.4;
}
// Get Green Value of Color 1
var L1G = color1.g;
if (L1G <= 0.03928) {
L1G = L1G / 12.92;
} else {
L1G = ((L1G + 0.055) / 1.055)**2.4;
}
// Get Blue Value of Color 1
var L1B = color1.b;
if (L1B <= 0.03928) {
L1B = L1B / 12.92;
} else {
L1B = ((L1B + 0.055) / 1.055)**2.4;
}
//Color2
//Get Red Value of Color 2
var L2R = color2.r;
if (L2R <= 0.03928) {
L2R = L2R / 12.92;
} else {
L2R = ((L2R + 0.055) / 1.055)**2.4;
}
//Get Green Value of Color 2
var L2G = color2.g;
if (L2G <= 0.03928) {
L2G = L2G / 12.92;
} else {
L2G = ((L2G + 0.055) / 1.055)**2.4;
}
//Get Blue Value of Color 2
var L2B = color2.b;
if (L2B <= 0.03928) {
L2B = L2B / 12.92;
} else {
L2B = ((L2B + 0.055) / 1.055)**2.4;
}
var L1 = 0.2126 * L1R + 0.7152 * L1G + 0.0722 * L1B;
var L2 = 0.2126 * L2R + 0.7152 * L2G + 0.0722 * L2B;
//Make sure L1 is lighter
if (L1 <= L2) {
var temp = L2;
L2 = L1;
L1 = temp;
}
//Calculate Contrast
cr = (L1 + 0.05) / (L2 + 0.05);
return cr
}
function getColorContrastHSV(color1, color2) {
//normalize HSV colors
color1 = normHSV(color1);
color2 = normHSV(color2);
//convert to RGB
color1 = HSVtoRGB(color1);
color2 = HSVtoRGB(color2);
//get contrast of RGB colors
return getColorContrast(color1, color2)
}
function hexToRgb(hex){
hex = hex.replace('#','');
var myTempRGBColor = {
r: parseInt(hex.substring(0,2), 16),
g: parseInt(hex.substring(2,4), 16),
b: parseInt(hex.substring(4,6), 16)
}
return myTempRGBColor
}
// Accessible Colors
var initialColor = {
h:274,
s:49,
v:63
}
var white = {
h: 0,
s: 0,
v: 100,
}
var currentColor = initialColor;
var currentTextColor = white;
var accessibilityValue = 4.5;
function accessibleColors() {
// canvas start
var colorSpaceCanvas = document.querySelector('#colorSpace'),
cSWidth = colorSpaceCanvas.width,
cSHeight = colorSpaceCanvas.height,
cSContext = colorSpaceCanvas.getContext('2d'),
cSImage = cSContext.createImageData(cSWidth,cSHeight);
// generate image data
function gridImageData(hue) {
// iterate over rows
for (var row = 0, i=-1; row < cSHeight; ++row) {
// interate for cells
for (var column = 0; column < cSWidth; ++column) {
var tempColor = {
h: hue,
s: column,
v: cSHeight - row
}
var tempRGBColor = HSVtoRGB(normHSV(tempColor));
cSImage.data[++i] = Math.round(tempRGBColor.r*255);
cSImage.data[++i] = Math.round(tempRGBColor.g*255);
cSImage.data[++i] = Math.round(tempRGBColor.b*255);
cSImage.data[++i] = 255;
}
}
cSContext.putImageData(cSImage,0,0);
}
// generate accessibility curve
function getAccessibilityCurve(hue) {
var accessibilityPath = "M";
var firstCheck; // store the first point in each row (true or false)
for (var column = 0; column <= 100; column++) {
for (var row = 0; row <= 100; row++) {
if (row == 0) {
firstCheck = checkColorContrast(hue, column, row);
}
// check if the contrast has changed from false to true or true to false (boundary condition)
if (checkColorContrast(hue, column, row) != firstCheck) {
var scaledRow = (100 - row) * 2;
var scaledColumn = column * 2;
accessibilityPath = accessibilityPath + " " + scaledColumn + " " + scaledRow;
break;
}
}
}
return accessibilityPath;
}
// canvas start
var hueChannelCanvas = document.querySelector('#hueChannel'),
hCWidth = hueChannelCanvas.width,
hCHeight = hueChannelCanvas.height,
hCContext = hueChannelCanvas.getContext('2d'),
hCImage = hCContext.createImageData(hCWidth, hCHeight);
// generate hue selector data
function hueChannelData() {
for (var column = 0, i=-1; column < 360; ++column) {
var tempColor = {
h: column,
s: 100,
v: 100
}
var tempRGBColor = HSVtoRGB(normHSV(tempColor));
hCImage.data[++i] = Math.round(tempRGBColor.r*255);
hCImage.data[++i] = Math.round(tempRGBColor.g*255);
hCImage.data[++i] = Math.round(tempRGBColor.b*255);
hCImage.data[++i] = 255;
}
hCContext.putImageData(hCImage,0,0);
}
var satBrightSpaceSVG = d3.select('#satBrightSpace');
satBrightSpaceSVG.call(d3.drag()
.on('start', dragstartedSatBrightSpace)
.on('drag', draggedSatBrightSpace)
.on('end', dragendedSatBrightSpace)
);
function dragstartedSatBrightSpace() {
d3.select(this).raise().classed('active', true);
d3.select('#currentColorCircle')
.attr('cx', Math.floor(d3.event.x))
.attr('cy', Math.floor(d3.event.y));
currentColor.s = Math.floor(d3.event.x / 2.0)
currentColor.v = Math.floor((200.0 - d3.event.y) / 2.0);
update(currentColor, currentTextColor);
}
function draggedSatBrightSpace() {
var colorCircleX, colorCircleY;
if (d3.event.x > 200) {
colorCircleX = 200;
} else if (d3.event.x < 0) {
colorCircleX = 0;
} else {
colorCircleX = d3.event.x;
}
if (d3.event.y > 200) {
colorCircleY = 200;
} else if (d3.event.y < 0) {
colorCircleY = 0;
} else {
colorCircleY = d3.event.y;
}
d3.select('#currentColorCircle')
.attr('cx', Math.floor(colorCircleX))
.attr('cy', Math.floor(colorCircleY));
currentColor.s = Math.floor(colorCircleX / 2.0)
currentColor.v = Math.floor((200.0 - colorCircleY) / 2.0);
update(currentColor, currentTextColor);
}
function dragendedSatBrightSpace() {
d3.select(this).classed('active', false);
}
// create the accessibility path
satBrightSpaceSVG.append('path')
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('id', 'accessibilityPath')
.attr('d', getAccessibilityCurve(currentColor.h))
.attr('stroke-width', '1')
// create the saturation & brightness selector
satBrightSpaceSVG.append('circle')
.attr('fill', 'none')
.attr('stroke', 'white')
.attr('id', 'currentColorCircle')
.attr('cx', currentColor.s*2)
.attr('cy', 200 -(currentColor.v*2))
.attr('r', 5)
.attr('stroke-width', 2);
var hueNubSpaceSVG = d3.select('#hueNubSpace');
hueNubSpaceSVG.call(d3.drag()
.on('start', dragstartedHueNubSpace)
.on('drag', draggedHueNubSpace)
.on('end', dragended)
);
function dragstartedHueNubSpace() {
d3.select(this).raise().classed('active', true);
d3.select('#hueSelectorNub')
.attr('x', Math.floor(d3.event.x))
.attr('stroke', '#000000');
currentColor.h = Math.floor(d3.event.x * 1.8);
update(currentColor, currentTextColor);
}
function draggedHueNubSpace() {
var HueX;
if (d3.event.x > 200) {
HueX = 200;
} else if (d3.event.x < 0) {
HueX = 0;
} else {
HueX = d3.event.x;
}
d3.select('#hueSelectorNub')
.attr('x', Math.floor(HueX))
.attr('stroke', '#000000');
currentColor.h = Math.floor(HueX * 1.8);
update(currentColor, currentTextColor);
}
function dragended() {
d3.select('#hueSelectorNub').attr('stroke', '#8E8E8E');
d3.select(this).classed('active', false);
}
// create the hue selector nub
hueNubSpaceSVG.append('rect')
.attr('x', currentColor.h / 1.8)
.attr('y', 0)
.attr('width', 5)
.attr('height', 10)
.attr('id', 'hueSelectorNub')
.attr('fill', 'white')
.attr('stroke-width', 1)
.attr('stroke', '#8E8E8E')
.attr('rx', 2)
.attr('ry', 2);
// Initialize Static Items
hueChannelData();
// Initialize Items that Update
update(initialColor, white);
// Global Update
function update(currentColor, currentTextColor) {
updateExampleButton(currentColor, currentTextColor);
updateContrastWarning(currentColor, currentTextColor);
updateInputs(currentColor, currentTextColor);
updateHues(currentColor.h);
updateCurrentColorCircle(currentColor);
}
function updateExampleButton(currentColor, currentTextColor) {
var normCurrentColorRGB = HSVtoRGB(normHSV(currentColor));
var cCR = Math.round(normCurrentColorRGB.r*255);
var cCG = Math.round(normCurrentColorRGB.g*255);
var cCB = Math.round(normCurrentColorRGB.b*255);
var normCurrentTextColorRGB = HSVtoRGB(normHSV(currentTextColor));
var cTCR = Math.round(normCurrentTextColorRGB.r*255);
var cTCG = Math.round(normCurrentTextColorRGB.g*255);
var cTCB = Math.round(normCurrentTextColorRGB.b*255);
var cCString = 'background-color: rgb(' + cCR + ',' + cCG + ',' + cCB +')';
var cTCString = 'color: rgb(' + cTCR + ',' + cTCG + ',' + cTCB +')';
d3.select('#myButton').attr('style', cCString + "; " + cTCString);
}
function updateContrastWarning(currentColor, currentTextColor) {
var contrastVal = getColorContrastHSV(currentColor, currentTextColor);
var floored = (contrastVal.toString().match(/^-?\d+(?:\.\d{0,1})?/)[0]*1).toFixed(1)
d3.select('#contrastValue').text('Contrast: ' + floored + ':1');
if (contrastVal >= accessibilityValue) {
d3.select('#optionalWarning').attr('style', "display:none");
} else {
d3.select('#optionalWarning').attr('style', "display:inherit");
}
}
function updateInputs(currentColor, currentTextColor) {
d3.select('#hueInput').property('value', currentColor.h);
d3.select('#satInput').property('value', currentColor.s);
d3.select('#brightInput').property('value', currentColor.v);
d3.select('#textHueInput').property('value', currentTextColor.h);
d3.select('#textSatInput').property('value', currentTextColor.s);
d3.select('#textBrightInput').property('value', currentTextColor.v);
}
function updateHues(hue) {
d3.select('#hueSelectorNub').attr('x', hue / 1.8).attr('hue', hue);
gridImageData(hue);
d3.select('#accessibilityPath').attr('d', getAccessibilityCurve(hue));
}
function updateCurrentColorCircle(currentColor) {
d3.select('#currentColorCircle')
.attr('cx', currentColor.s*2)
.attr('cy', 200 - (currentColor.v*2))
}
// Interaction handlers
d3.select('#hueInput').on('input', function() {
currentColor.h = this.value;
update(currentColor, currentTextColor);
});
d3.select('#satInput').on('input', function() {
currentColor.s = this.value;
update(currentColor, currentTextColor);
});
d3.select('#brightInput').on('input', function() {
currentColor.v = this.value;
update(currentColor, currentTextColor);
});
d3.select('#textHueInput').on('input', function() {
currentTextColor.h = this.value;
update(currentColor, currentTextColor);
});
d3.select('#textSatInput').on('input', function() {
currentTextColor.s = this.value;
update(currentColor, currentTextColor);
});
d3.select('#textBrightInput').on('input', function() {
currentTextColor.v = this.value;
update(currentColor, currentTextColor);
});
}
function checkColorContrast(hue, x, y) {
var tempColor = {
h: hue,
s: x,
v: y
}
var contrast = getColorContrastHSV(tempColor, currentTextColor);
return (contrast >= accessibilityValue)
}
accessibleColors();
</script>
</body>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.16.2/math.js
https://use.typekit.net/nmk5cke.js