apca-introduction

The missing introduction to APCA  https://p.ce9e.org/apca-introduction/
git clone https://git.ce9e.org/apca-introduction.git

commit
3df4fa022c59427e3be3ba538b1b333aefee5659
parent
afce56b362b813d36aa200eb812ab9b6618df14f
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2022-07-15 12:45
add tool

Diffstat

M README.md 2 +-
A tool/index.html 42 ++++++++++++++++++++++++++++++++++++++++++
A tool/style.css 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A tool/tool.js 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

4 files changed, 317 insertions, 1 deletions


diff --git a/README.md b/README.md

@@ -11,7 +11,7 @@ APCA was created by Andrew Somers (Myndex) and is currently being proposed for
   11    11 the next major version of the [W3C Accessibility Guidelines
   12    12 (WCAG)](https://www.w3.org/TR/wcag-3.0/).
   13    13 
   14    -1 An interactive demo is available at <https://xi.github.io/apca-introduction/>.
   -1    14 An interactive demo is available at <https://xi.github.io/apca-introduction/tool/>.
   15    15 
   16    16 ## Algorithm
   17    17 

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

@@ -0,0 +1,42 @@
   -1     1 <!DOCTYPE html>
   -1     2 <html lang="en">
   -1     3 <head>
   -1     4 	<meta charset="utf-8" />
   -1     5 	<title>APCA Contrast</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 	<div id="form">
   -1    11 		<label>
   -1    12 			<span>Background</span>
   -1    13 			<input id="bgInput" value="hsla(200,0%,0%,0.8)" autofocus>
   -1    14 		</label>
   -1    15 		<label>
   -1    16 			<span>Text color</span>
   -1    17 			<input id="fgInput" value="orangered">
   -1    18 		</label>
   -1    19 		<button id="swap">Swap colors</button>
   -1    20 	</div>
   -1    21 
   -1    22 	<div id="output">
   -1    23 		<label>
   -1    24 			<span class="gradient" id="apcaGradient"></span>
   -1    25 			<abbr title="Accessible Perceptual Contrast Algorithm">APCA</abbr>:
   -1    26 			<output id="apcaOutput" for="bgInput fgInput" aria-live="polite"></output>
   -1    27 		</label>
   -1    28 		<label>
   -1    29 			<span class="gradient" id="wcag2Gradient"></span>
   -1    30 			<abbr title="Web Content Accessibility Guidelines Version 2">WCAG 2.x</abbr>:
   -1    31 			<output id="wcag2Output" for="bgInput fgInput" aria-live="polite"></output>
   -1    32 		</label>
   -1    33 		<a href="https://github.com/xi/apca-introduction">About APCA</a>
   -1    34 	</div>
   -1    35 
   -1    36 	<div id="display">
   -1    37 		<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    38 	</div>
   -1    39 
   -1    40 	<script src="tool.js" type="module"></script>
   -1    41 </body>
   -1    42 </html>

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

@@ -0,0 +1,119 @@
   -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 output {
   -1    13 	font-weight: bold;
   -1    14 }
   -1    15 
   -1    16 button {
   -1    17 	font-size: 80%;
   -1    18 	padding: 0.4em 0.6em;
   -1    19 	white-space: nowrap;
   -1    20 }
   -1    21 
   -1    22 html,
   -1    23 body {
   -1    24 	margin: 0;
   -1    25 	padding: 0;
   -1    26 }
   -1    27 
   -1    28 html {
   -1    29 	background: linear-gradient(45deg, currentColor 25%, transparent 25%, transparent 75%, currentColor 75%, currentColor),
   -1    30 		linear-gradient(45deg, currentColor 25%, transparent 25%, transparent 75%, currentColor 75%, currentColor) 0.5rem 0.5rem;
   -1    31 	background-color: #eee;
   -1    32 	background-size: 1rem 1rem;
   -1    33 }
   -1    34 
   -1    35 body {
   -1    36 	display: grid;
   -1    37 	grid-template-rows: min-content 1fr min-content;
   -1    38 	min-height: 100vh;
   -1    39 }
   -1    40 
   -1    41 #form {
   -1    42 	display: flex;
   -1    43 	padding: 0.5em;
   -1    44 	gap: 0.5em;
   -1    45 	flex-direction: column;
   -1    46 	align-items: center;
   -1    47 	justify-content: center;
   -1    48 	text-align: center;
   -1    49 }
   -1    50 
   -1    51 #form label span {
   -1    52 	display: inline-block;
   -1    53 	margin: 0 0.8rem;
   -1    54 	padding: 0.1em 0.4em;
   -1    55 	white-space: nowrap;
   -1    56 	background-color: #666;
   -1    57 	color: #fff;
   -1    58 	font-size: 70%;
   -1    59 	font-weight: bold;
   -1    60 }
   -1    61 
   -1    62 #form input {
   -1    63 	display: block;
   -1    64 	width: 22ch;  /* to fit rgba(255,255,255,0.5) */
   -1    65 	margin-top: -1px;
   -1    66 	padding: 0.2em 0.5ch;
   -1    67 	font-family: monospace;
   -1    68 	font-size: 150%;
   -1    69 	text-align: inherit;
   -1    70 	box-shadow: 0.05em 0.1em 0.2em rgba(0,0,0,.4) inset;
   -1    71 	background: #eee;
   -1    72 	color: #000;
   -1    73 	border-radius: 0.3em;
   -1    74 }
   -1    75 
   -1    76 #form label + label {
   -1    77 	order: 2;
   -1    78 }
   -1    79 
   -1    80 #display {
   -1    81 	grid-row: 2;
   -1    82 }
   -1    83 
   -1    84 #display p {
   -1    85 	max-width: 40em;
   -1    86 	margin: 0 auto;
   -1    87 	padding: 1em;
   -1    88 }
   -1    89 
   -1    90 #output {
   -1    91 	grid-row: 3;
   -1    92 	padding: 0.5em;
   -1    93 	background: #eee;
   -1    94 	color: #000;
   -1    95 	text-align: center;
   -1    96 }
   -1    97 
   -1    98 #output label {
   -1    99 	display: inline-block;
   -1   100 	margin: 0 1em;
   -1   101 }
   -1   102 
   -1   103 .gradient {
   -1   104 	display: inline-block;
   -1   105 	vertical-align: -0.35em;
   -1   106 	width: 1.5em;
   -1   107 	height: 1.5em;
   -1   108 	border-radius: 50%;
   -1   109 	background-color: #ccc;
   -1   110 }
   -1   111 
   -1   112 @media (min-width: 70em) {
   -1   113 	#form {
   -1   114 		flex-direction: row;
   -1   115 	}
   -1   116 	#form button {
   -1   117 		margin-top: 1.2rem;
   -1   118 	}
   -1   119 }

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

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