contrast

Easily calculate color contrast ratios  https://p.ce9e.org/contrast/
git clone https://git.ce9e.org/contrast.git

commit
576ecafe14bb97c13a746fa88e7c785755cbc774
parent
05aeed9667df1fd74a1f81a46a62f98377b2e091
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2023-05-04 16:25
init

Diffstat

A contrast.js 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A favicon.js 20 ++++++++++++++++++++
A index.html 33 +++++++++++++++++++++++++++++++++
A style.css 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A wcag2.js 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++

5 files changed, 345 insertions, 0 deletions


diff --git a/contrast.js b/contrast.js

@@ -0,0 +1,127 @@
   -1     1 import * as wcag2 from './wcag2.js';
   -1     2 import * as favicon from './favicon.js';
   -1     3 
   -1     4 var fgInput = document.querySelector('#fgInput');
   -1     5 var bgInput = document.querySelector('#bgInput');
   -1     6 var swapButton = document.querySelector('#swap');
   -1     7 var output = document.querySelector('#output');
   -1     8 
   -1     9 var score = function(range, levels) {
   -1    10   var biggerThan = function(t) {
   -1    11     if (range[0] > t) {
   -1    12       return 1;
   -1    13     } else if (range[1] > t) {
   -1    14       return (range[1] - t) / (range[1] - range[0]);
   -1    15     } else {
   -1    16       return 0;
   -1    17     }
   -1    18   };
   -1    19 
   -1    20   var result = [];
   -1    21   var sum = 0;
   -1    22   levels.forEach(level => {
   -1    23     var v = biggerThan(-level) - biggerThan(level);
   -1    24     result.push(v - sum);
   -1    25     sum = v;
   -1    26   });
   -1    27   result.push(1 - sum);
   -1    28 
   -1    29   return result;
   -1    30 };
   -1    31 
   -1    32 var makeGradient = function(scores) {
   -1    33   const colors = [
   -1    34     'hsl(0, 100%, 40%)',
   -1    35     'hsl(40, 100%, 45%)',
   -1    36     'hsl(80, 60%, 45%)',
   -1    37     'hsl(95, 60%, 41%)',
   -1    38   ];
   -1    39 
   -1    40   var stops = [];
   -1    41   var prevScore = 0;
   -1    42   var scale = x => x * 70 + 15;  // compensate for border radius
   -1    43 
   -1    44   for (var i = 0; i < scores.length; i++) {
   -1    45     if (scores[i] > 0) {
   -1    46       var newScore = prevScore + scores[i];
   -1    47       stops.push(`${colors[i]} ${scale(prevScore)}%`, `${colors[i]} ${scale(newScore)}%`);
   -1    48       prevScore = newScore;
   -1    49     }
   -1    50   }
   -1    51 
   -1    52   return `linear-gradient(135deg, ${stops.join(', ')})`;
   -1    53 };
   -1    54 
   -1    55 var parseColor = function(s) {
   -1    56   var rgba = s.match(/rgba?\(([\d.]+), ([\d.]+), ([\d.]+)(?:, ([\d.]+))?\)/);
   -1    57   if (!rgba) {
   -1    58     return null;
   -1    59   }
   -1    60   rgba.shift();
   -1    61   if (rgba[3] === undefined) {
   -1    62     rgba[3] = 1;
   -1    63   }
   -1    64   rgba = rgba.map(x => parseFloat(x, 10));
   -1    65   return rgba;
   -1    66 };
   -1    67 
   -1    68 var setColor = function(input, key) {
   -1    69   var old = getComputedStyle(document.body)[key];
   -1    70   document.body.style[key] = input.value;
   -1    71   var value = getComputedStyle(document.body)[key];
   -1    72   return value !== old;
   -1    73 };
   -1    74 
   -1    75 var formatRange = function(range, places) {
   -1    76   var avg = (range[0] + range[1]) / 2;
   -1    77   var delta = avg - range[0];
   -1    78   if (delta.toFixed(places) === (0).toFixed(places)) {
   -1    79     return `${avg.toFixed(places)}`;
   -1    80   } else {
   -1    81     return `${avg.toFixed(places)} ±${delta.toFixed(places)}`;
   -1    82   }
   -1    83 };
   -1    84 
   -1    85 var oninput = function() {
   -1    86   // NOTE: | to prevent lazy evaluation
   -1    87   if (setColor(bgInput, 'backgroundColor') | setColor(fgInput, 'color')) {
   -1    88     var fgUrl = encodeURIComponent(fgInput.value);
   -1    89     var bgUrl = encodeURIComponent(bgInput.value);
   -1    90     location.hash = `${fgUrl}-on-${bgUrl}`;
   -1    91 
   -1    92     favicon.setFavicon(bgInput.value, fgInput.value);
   -1    93 
   -1    94     var computed = getComputedStyle(document.body);
   -1    95     var bg = parseColor(computed.backgroundColor);
   -1    96     var fg = parseColor(computed.color);
   -1    97 
   -1    98     var wcag2Range = wcag2.contrastAlpha(fg, bg, wcag2.contrast);
   -1    99     output.style.backgroundImage = makeGradient(score(wcag2Range.map(Math.log), wcag2.levels.map(Math.log)));
   -1   100     output.textContent = formatRange(wcag2.getAbsRange(wcag2Range), 2);
   -1   101   }
   -1   102 };
   -1   103 
   -1   104 var onhashchange = function() {
   -1   105   var colors = location.hash.slice(1).split('-on-');
   -1   106   fgInput.value = decodeURIComponent(colors[0]);
   -1   107   bgInput.value = decodeURIComponent(colors[1]);
   -1   108   oninput();
   -1   109 };
   -1   110 
   -1   111 var onswap = function() {
   -1   112   var tmp = bgInput.value;
   -1   113   bgInput.value = fgInput.value;
   -1   114   fgInput.value = tmp;
   -1   115   oninput();
   -1   116 };
   -1   117 
   -1   118 fgInput.addEventListener('input', oninput);
   -1   119 bgInput.addEventListener('input', oninput);
   -1   120 swapButton.addEventListener('click', onswap);
   -1   121 window.addEventListener('hashchange', onhashchange);
   -1   122 
   -1   123 if (location.hash) {
   -1   124   onhashchange();
   -1   125 } else {
   -1   126   oninput();
   -1   127 }

diff --git a/favicon.js b/favicon.js

@@ -0,0 +1,20 @@
   -1     1 var favicon = document.querySelector('link[rel="shortcut icon"]');
   -1     2 
   -1     3 var canvas = document.createElement('canvas');
   -1     4 var ctx = canvas.getContext('2d');
   -1     5 document.body.appendChild(canvas);
   -1     6 canvas.width = 16;
   -1     7 canvas.height = 16;
   -1     8 canvas.hidden = true;
   -1     9 
   -1    10 export var setFavicon = function(bg, fg) {
   -1    11   ctx.clearRect(0, 0, 16, 16);
   -1    12 
   -1    13   ctx.fillStyle = bg;
   -1    14   ctx.fillRect(0, 0, 8, 16);
   -1    15 
   -1    16   ctx.fillStyle = fg;
   -1    17   ctx.fillRect(8, 0, 8, 16);
   -1    18 
   -1    19   favicon.href = canvas.toDataURL();
   -1    20 };

diff --git a/index.html b/index.html

@@ -0,0 +1,33 @@
   -1     1 <!DOCTYPE html>
   -1     2 <html lang="en" class="checkered">
   -1     3 <head>
   -1     4 	<meta charset="utf-8" />
   -1     5 	<title>Contrast Ratio</title>
   -1     6 	<link rel="stylesheet" href="style.css" />
   -1     7 	<link rel="shortcut icon"  type="image/png" href="about:blank" />
   -1     8 </head>
   -1     9 <body>
   -1    10 	<output class="gradient" id="output" for="bgInput fgInput" aria-live="polite">
   -1    11 		<strong></strong>
   -1    12 		<span></span>
   -1    13 	</output>
   -1    14 
   -1    15 	<div id="form">
   -1    16 		<label>
   -1    17 			<span>Background</span>
   -1    18 			<input id="bgInput" value="hsla(200,0%,0%,0.8)" autofocus>
   -1    19 		</label>
   -1    20 		<label>
   -1    21 			<span>Text color</span>
   -1    22 			<input id="fgInput" value="orangered">
   -1    23 		</label>
   -1    24 		<button id="swap">Swap colors</button>
   -1    25 	</div>
   -1    26 
   -1    27 	<div id="display">
   -1    28 		<p>This sample text attempts to visually demonstrate how readable this color combination is, for normal, <i>italic</i>, <b>bold</b>, or <small>small</small> text.</p>
   -1    29 	</div>
   -1    30 
   -1    31 	<script src="contrast.js" type="module"></script>
   -1    32 </body>
   -1    33 </html>

diff --git a/style.css b/style.css

@@ -0,0 +1,108 @@
   -1     1 /* Inspired by https://contrast-ratio.com */
   -1     2 
   -1     3 * {
   -1     4 	box-sizing: border-box;
   -1     5 }
   -1     6 
   -1     7 :root {
   -1     8 	font-size: 150%;
   -1     9 	line-height: 1.3;
   -1    10 }
   -1    11 
   -1    12 button {
   -1    13 	font-size: 80%;
   -1    14 	padding-block: 0.4em;
   -1    15 	padding-inline: 0.6em;
   -1    16 	white-space: nowrap;
   -1    17 }
   -1    18 
   -1    19 html,
   -1    20 body {
   -1    21 	margin: 0;
   -1    22 	padding: 0;
   -1    23 }
   -1    24 
   -1    25 .checkered {
   -1    26 	background: linear-gradient(45deg, currentColor 25%, transparent 25%, transparent 75%, currentColor 75%, currentColor),
   -1    27 		linear-gradient(45deg, currentColor 25%, transparent 25%, transparent 75%, currentColor 75%, currentColor) 0.5rem 0.5rem;
   -1    28 	background-color: #eee;
   -1    29 	background-size: 1rem 1rem;
   -1    30 }
   -1    31 
   -1    32 body {
   -1    33 	min-block-size: 100vb;
   -1    34 	padding-block: 2em;
   -1    35 	padding-inline: 1em;
   -1    36 }
   -1    37 
   -1    38 #form {
   -1    39 	display: flex;
   -1    40 	gap: 0.5em;
   -1    41 	flex-direction: column;
   -1    42 	align-items: center;
   -1    43 	justify-content: center;
   -1    44 	text-align: center;
   -1    45 }
   -1    46 
   -1    47 #form label span {
   -1    48 	display: inline-block;
   -1    49 	margin-inline: 0.8rem;
   -1    50 	padding-block: 0.1em;
   -1    51 	padding-inline: 0.4em;
   -1    52 	white-space: nowrap;
   -1    53 	background-color: #666;
   -1    54 	color: #fff;
   -1    55 	font-size: 70%;
   -1    56 	font-weight: bold;
   -1    57 }
   -1    58 
   -1    59 #form input {
   -1    60 	display: block;
   -1    61 	inline-size: 22ch;  /* to fit rgba(255,255,255,0.5) */
   -1    62 	margin-block-start: -1px;
   -1    63 	padding-block: 0.2em;
   -1    64 	padding-inline: 0.5ch;
   -1    65 	font-family: monospace;
   -1    66 	font-size: 150%;
   -1    67 	text-align: inherit;
   -1    68 	box-shadow: 0.05em 0.1em 0.2em rgba(0,0,0,.4) inset;
   -1    69 	background: #eee;
   -1    70 	color: #000;
   -1    71 	border-radius: 0.3em;
   -1    72 }
   -1    73 
   -1    74 #form label + label {
   -1    75 	order: 2;
   -1    76 }
   -1    77 
   -1    78 #display {
   -1    79 	max-inline-size: 30em;
   -1    80 	margin-inline: auto;
   -1    81 }
   -1    82 
   -1    83 .gradient {
   -1    84 	display: flex;
   -1    85 	block-size: 5em;
   -1    86 	inline-size: 5em;
   -1    87 	justify-content: center;
   -1    88 	align-items: center;
   -1    89 	text-align: center;
   -1    90 	border-radius: 50%;
   -1    91 	margin-inline: auto;
   -1    92 	margin-block-end: 1em;
   -1    93 	padding: 0.5em;
   -1    94 	box-shadow: 0 0.1em 0.2em rgba(0,0,0,.4);
   -1    95 	background-color: #888;
   -1    96 	color: #fff;
   -1    97 	text-shadow: 0 -0.06em .05em rgba(0,0,0,.5);
   -1    98 	font-weight: bold;
   -1    99 }
   -1   100 
   -1   101 @media (min-width: 70em) {
   -1   102 	#form {
   -1   103 		flex-direction: row;
   -1   104 	}
   -1   105 	#form button {
   -1   106 		margin-block-start: 1.2rem;
   -1   107 	}
   -1   108 }

diff --git a/wcag2.js b/wcag2.js

@@ -0,0 +1,57 @@
   -1     1 export const levels = [3, 4.5, 7];
   -1     2 
   -1     3 var sRGBtoY = function(srgb) {
   -1     4   var pre = c => c < 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
   -1     5   var r = pre(srgb[0] / 255);
   -1     6   var g = pre(srgb[1] / 255);
   -1     7   var b = pre(srgb[2] / 255);
   -1     8   return 0.2126 * r + 0.7152 * g + 0.0722 * b;
   -1     9 };
   -1    10 
   -1    11 export var contrast = function(fg, bg) {
   -1    12   // NOTE: this returns a "signed" value.
   -1    13   // `c >= 1 ? c : 1 / c` gives you the actual value.
   -1    14   // See also `getAbsRange()`.
   -1    15 
   -1    16   var yfg = sRGBtoY(fg);
   -1    17   var ybg = sRGBtoY(bg);
   -1    18   return (ybg + 0.05) / (yfg + 0.05);
   -1    19 };
   -1    20 
   -1    21 export var abs = function(c) {
   -1    22   return c < 1 ? 1 / c : c;
   -1    23 };
   -1    24 
   -1    25 export var getAbsRange = function(range) {
   -1    26   if (range[0] >= 1) {
   -1    27     return range;
   -1    28   } else if (range[1] <= 1) {
   -1    29     return [1 / range[1], 1 / range[0]];
   -1    30   } else {
   -1    31     return [1, Math.max(1 / range[0], range[1])];
   -1    32   }
   -1    33 };
   -1    34 
   -1    35 var alphaBlend = function(fg, bg) {
   -1    36   return [
   -1    37     fg[0] * fg[3] + bg[0] * (1 - fg[3]),
   -1    38     fg[1] * fg[3] + bg[1] * (1 - fg[3]),
   -1    39     fg[2] * fg[3] + bg[2] * (1 - fg[3]),
   -1    40   ];
   -1    41 };
   -1    42 
   -1    43 export var contrastAlpha = function(afg, abg, contrast) {
   -1    44   var bgBlack = alphaBlend(abg, [0, 0, 0]);
   -1    45   var fgBlack = alphaBlend(afg, bgBlack);
   -1    46   var cBlack = contrast(fgBlack, bgBlack);
   -1    47 
   -1    48   var bgWhite = alphaBlend(abg, [255, 255, 255]);
   -1    49   var fgWhite = alphaBlend(afg, bgWhite);
   -1    50   var cWhite = contrast(fgWhite, bgWhite);
   -1    51 
   -1    52   return [
   -1    53     Math.min(cBlack, cWhite),
   -1    54     Math.max(cBlack, cWhite),
   -1    55   ];
   -1    56 };
   -1    57