Skip to content

Instantly share code, notes, and snippets.

@KevinGutowski
Last active August 24, 2017 18:01
Show Gist options
  • Save KevinGutowski/b0fad056a1e75d2bcdd670060d6680b6 to your computer and use it in GitHub Desktop.
Save KevinGutowski/b0fad056a1e75d2bcdd670060d6680b6 to your computer and use it in GitHub Desktop.
D3.UNCONF Submission

Learn about the relationship between Hue, Saturation, Brightness, and Color Contrast. Minimum contrast level is set to 4.5 (AA).

<!DOCTYPE html>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment