Use tools to evaluate font size, font stroke width, background color, font color, and nearby colors and adjust the properties of those elements to achieve good visual contrast and readability.
Web
All sighted users need adaquate luminance contrast (lightness/darkness difference) between background and text colors in order to read the text easily.
As used on this page, Visual Contrast means the perceived difference between two colors displayed on a computer monitor, such as the color of the text against the color of the background. Our perception of contrast is a function of the Human Visual System (HVS) which includes the eye, optic nerve, and several areas of the brain that are either dedicated to processing vision, or that share vision processing with other senses such as hearing.
The eye consists of an optical portion including a lens, and a light sensitive portion known as the retina. Muscles in the eye adjust the shape of the lens to adjust the focus of incoming light, so that an image of the world around us is focused onto the retina. Light sensitive cells on the retina then convert light into electrical impulses that are sent to the brain's vision processing center, called the visual cortex.
This complex system is subject to many forms of of impairment. And even "normal" vision changes substantially over a person's lifetime. For instance contrast sensitivity is very poor at birth, and it takes 20 years of development for normal vision to reach peak contrast sensitivity. Many people over the age of 45 develop the need for reading glasses as the natural lens in their eye grows larger and stiffer to where the muscles can no longer pull it into focus. And while people often think of acuity as a defining factor (i.e. 20/20, the ability to focus), vision is often more impacted by contrast sensitivity related impairments.
Our perception of contrast and ultimately the readability of text is affected by many interdependant factors, including:
All of these interdependant factors affect the perception of contrast and ultimately readability. As we are concerned with readability on computer screens, we can made some assumptions about a range of brightness, and a typical viewing environment. This provides a baseline standard for all users for best readability of content, or a minimum level of legibility for non-content. Then, we can set how different impairments require different adjustments or user customization to accomodate the neads of each individual user.
For instance, a user with poor "acuity", that is, poor sharpness where they cannot focus on small text, will need to be able to zoom the text larger. A user with poor contrast sensitivity may have good sharpness, but unable to perceive colors that are too similar in lightness. Yet another user with a glare problem may need lower contrast, and/or a reduction in blue, to reduce glare or halos interfereing with readability. Those with a color vision problem (sometimes called 'color blind') may have difficulty detecting contrast or lightness for certain colors, such as deep red.
Predicted contrast is reported as a percentage using the methods in this guideline, based on the CSS color values in sRGB colorspace, and with device default antialiasing1. The Tests section has a lookup table for specific font and contrast combinations, but as a general guide:
Note: 1These values require that antialiasing be at the device or user agent default, such that added antialiasing such as "Webkit-Font-Smoothing: Antialias;" is not enabled.
It's useful to point out that the term “Visual Contrast” can also be used to describe the differences of other visual perceptions: Contrast of size difference, contrast of position, contrast of speed or motion, etc. but these are not covered in this guideline at this time.
////////////////////////////////////////////////////////////////////////////////
///// Functions to parse color values and determine SAPC contrast
///// REQUIREMENTS: ECMAScript 6 - ECMAScript 2015
///// SAPC tool version 0.97 by Andrew Somers
///// https://www.myndex.com/WEB/Perception
///// Color value input parsing based substantially on rgbcolor.js by
///// Stoyan Stefanov <sstoo@gmail.com>
///// His site: http://www.phpied.com/rgb-color-parser-in-javascript/
///// MIT license
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/////
///// ***** SAPC BLOCK *****
/////
///// For Evaluations, this is referred to as: SAPC-7
///// sRGB Advanced Perceptual Contrast v0.97 beta JAVASCRIPT
///// Copyright © 2019 by Andrew Somers
///// Licensed to the W3C Per Collaborator Agreement
///// SIMPLE VERSION — This Version Is Stripped Of Extensions:
///// * No Color Vision Module
///// * No Spatial Frequency Module
///// * No Light Adaptation Module
///// * No Dynamics Module
///// * No Alpha Module
/////
////////////////////////////////////////////////////////////////////////////////
///// CONSTANTS USED IN THIS VERSION ///////////////////////////////////////////
const sRGBtrc = 2.218; // Gamma for sRGB linearization. 2.223 could be used instead
// 2.218 sets unity with the piecewise sRGB at #777
const Rco = 0.2126; // sRGB Red Coefficient
const Gco = 0.7156; // sRGB Green Coefficient
const Bco = 0.0722; // sRGB Blue Coefficient
const scaleBoW = 161.8; // Scaling for dark text on light (phi * 100)
const scaleWoB = 161.8; // Scaling for light text on dark — same as BoW, but
// this is separate for possible future use.
const normBGExp = 0.38; // Constants for Power Curve Exponents.
const normTXTExp = 0.43; // One pair for normal text,and one for REVERSE
const revBGExp = 0.5; // FUTURE: These will eventually be dynamic
const revTXTExp = 0.43; // as a function of light adaptation and context
const blkThrs = 0.02; // Level that triggers the soft black clamp
const blkClmp = 1.75; // Exponent for the soft black clamp curve
///// Ultra Simple Basic Bare Bones SAPC Function //////////////////////////////
// This REQUIRES linearized R,G,B values of 0.0-1.0
function SAPCbasic(Rbg,Gbg,Bbg,Rtxt,Gtxt,Btxt) {
var SAPC = 0.0;
// Find Y by applying coefficients and sum.
// This REQUIRES linearized R,G,B 0.0-1.0
var Ybg = Rbg*Rco + Gbg*Gco + Bbg*Bco;
var Ytxt = Rtxt*Rco + Gtxt*Gco + Btxt*Bco;
///// INSERT COLOR MODULE HERE /////
// Now, determine polarity, soft clamp black, and calculate contrast
// Finally scale for easy to remember percentages
// Note that reverse (white text on black) intentionally
// returns a negative number
if ( Ybg > Ytxt ) { ///// For normal polarity, black text on white
// soft clamp darkest color if near black.
Ytxt = (Ytxt > blkThrs) ? Ytxt : Ytxt + Math.abs(Ytxt - blkThrs) ** blkClmp;
SAPC = ( Ybg ** normBGExp - Ytxt ** normTXTExp ) * scaleBoW;
return (SAPC < 15 ) ? "0%" : SAPC.toPrecision(3) + "%";
} else { ///// For reverse polarity, white text on black
Ybg = (Ybg > blkThrs) ? Ybg : Ybg + Math.abs(Ybg - blkThrs) ** blkClmp;
SAPC = ( Ybg ** revBGExp - Ytxt ** revTXTExp ) * scaleWoB;
return (SAPC > -15 ) ? "0%" : SAPC.toPrecision(3) + "%";
}
// If SAPC's more than 15%, return that value, otherwise clamp to zero
// this is to remove noise and unusual behavior if the user inputs
// colors too close to each other.
// This will be more important with future modules. Nevertheless
// In order to simplify code, SAPC will not report accurate contrasts
// of less than approximately 15%, so those are clamped.
// 25% is the "point of invisibility" for many people.
}
//////////////////////////////////////////////////////////////
///// END OF SAPC BLOCK //////////////////////////
//////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////
///// sRGB INPUT FORM BLOCK //////////////////////////
//////////////////////////////////////////////////////////////
function RGBColor(color_string) {
this.ok = false;
// strip any leading #
if (color_string.charAt(0) == '#') { // remove # if any
color_string = color_string.substr(1,6);
}
color_string = color_string.replace(/ /g,''); // strip spaces
color_string = color_string.toLowerCase(); // set lowercase
// before getting into regexps, try simple matches
// and overwrite the input
var simple_colors = {
aliceblue: 'f0f8ff',
antiquewhite: 'faebd7',
aqua: '00ffff',
aquamarine: '7fffd4',
azure: 'f0ffff',
beige: 'f5f5dc',
bisque: 'ffe4c4',
black: '000000',
blanchedalmond: 'ffebcd',
blue: '0000ff',
blueviolet: '8a2be2',
brown: 'a52a2a',
burlywood: 'deb887',
cadetblue: '5f9ea0',
chartreuse: '7fff00',
chocolate: 'd2691e',
coral: 'ff7f50',
cornflowerblue: '6495ed',
cornsilk: 'fff8dc',
crimson: 'dc143c',
cyan: '00ffff',
darkblue: '00008b',
darkcyan: '008b8b',
darkgoldenrod: 'b8860b',
darkgray: 'a9a9a9',
darkgreen: '006400',
darkkhaki: 'bdb76b',
darkmagenta: '8b008b',
darkolivegreen: '556b2f',
darkorange: 'ff8c00',
darkorchid: '9932cc',
darkred: '8b0000',
darksalmon: 'e9967a',
darkseagreen: '8fbc8f',
darkslateblue: '483d8b',
darkslategray: '2f4f4f',
darkturquoise: '00ced1',
darkviolet: '9400d3',
deeppink: 'ff1493',
deepskyblue: '00bfff',
dimgray: '696969',
dodgerblue: '1e90ff',
feldspar: 'd19275',
firebrick: 'b22222',
floralwhite: 'fffaf0',
forestgreen: '228b22',
fuchsia: 'ff00ff',
gainsboro: 'dcdcdc',
ghostwhite: 'f8f8ff',
gold: 'ffd700',
goldenrod: 'daa520',
gray: '808080',
green: '008000',
greenyellow: 'adff2f',
honeydew: 'f0fff0',
hotpink: 'ff69b4',
indianred : 'cd5c5c',
indigo : '4b0082',
ivory: 'fffff0',
khaki: 'f0e68c',
lavender: 'e6e6fa',
lavenderblush: 'fff0f5',
lawngreen: '7cfc00',
lemonchiffon: 'fffacd',
lightblue: 'add8e6',
lightcoral: 'f08080',
lightcyan: 'e0ffff',
lightgoldenrodyellow: 'fafad2',
lightgrey: 'd3d3d3',
lightgreen: '90ee90',
lightpink: 'ffb6c1',
lightsalmon: 'ffa07a',
lightseagreen: '20b2aa',
lightskyblue: '87cefa',
lightslateblue: '8470ff',
lightslategray: '778899',
lightsteelblue: 'b0c4de',
lightyellow: 'ffffe0',
lime: '00ff00',
limegreen: '32cd32',
linen: 'faf0e6',
magenta: 'ff00ff',
maroon: '800000',
mediumaquamarine: '66cdaa',
mediumblue: '0000cd',
mediumorchid: 'ba55d3',
mediumpurple: '9370d8',
mediumseagreen: '3cb371',
mediumslateblue: '7b68ee',
mediumspringgreen: '00fa9a',
mediumturquoise: '48d1cc',
mediumvioletred: 'c71585',
midnightblue: '191970',
mintcream: 'f5fffa',
mistyrose: 'ffe4e1',
moccasin: 'ffe4b5',
navajowhite: 'ffdead',
navy: '000080',
oldlace: 'fdf5e6',
olive: '808000',
olivedrab: '6b8e23',
orange: 'ffa500',
orangered: 'ff4500',
orchid: 'da70d6',
palegoldenrod: 'eee8aa',
palegreen: '98fb98',
paleturquoise: 'afeeee',
palevioletred: 'd87093',
papayawhip: 'ffefd5',
peachpuff: 'ffdab9',
peru: 'cd853f',
pink: 'ffc0cb',
plum: 'dda0dd',
powderblue: 'b0e0e6',
purple: '800080',
red: 'ff0000',
rosybrown: 'bc8f8f',
royalblue: '4169e1',
saddlebrown: '8b4513',
salmon: 'fa8072',
sandybrown: 'f4a460',
seagreen: '2e8b57',
seashell: 'fff5ee',
sienna: 'a0522d',
silver: 'c0c0c0',
skyblue: '87ceeb',
slateblue: '6a5acd',
slategray: '708090',
snow: 'fffafa',
springgreen: '00ff7f',
steelblue: '4682b4',
tan: 'd2b48c',
teal: '008080',
thistle: 'd8bfd8',
tomato: 'ff6347',
turquoise: '40e0d0',
violet: 'ee82ee',
violetred: 'd02090',
wheat: 'f5deb3',
white: 'ffffff',
whitesmoke: 'f5f5f5',
yellow: 'ffff00',
yellowgreen: '9acd32'
};
for (var key in simple_colors) {
if (color_string == key) {
color_string = simple_colors[key];
}
}
// end of simple type-in colors
// array of color definition objects
var color_defs = [
{
re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'],
process: function (bits){
return [
parseInt(bits[1]),
parseInt(bits[2]),
parseInt(bits[3])
];
}
},
{
re: /^(\w{2})(\w{2})(\w{2})$/,
example: ['#00ff00', '336699'],
process: function (bits){
return [
parseInt(bits[1], 16),
parseInt(bits[2], 16),
parseInt(bits[3], 16)
];
}
},
{
re: /^(\w{1})(\w{1})(\w{1})$/,
example: ['#fb0', 'f0f'],
process: function (bits){
return [
parseInt(bits[1] + bits[1], 16),
parseInt(bits[2] + bits[2], 16),
parseInt(bits[3] + bits[3], 16)
];
}
}
];
// search through the definitions to find a match
for (var i = 0; i < color_defs.length; i++) {
var re = color_defs[i].re;
var processor = color_defs[i].process;
var bits = re.exec(color_string);
if (bits) {
channels = processor(bits);
this.r = channels[0];
this.g = channels[1];
this.b = channels[2];
this.ok = true;
}
}
// validate & cleanup values
this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r);
this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g);
this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b);
// some getters
// returns rgb() value string
this.toRGB = function () {
return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';
}
// returns rgb() value string
this.toRGB2 = 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')';
// returns hex string
this.toHex = function () {
var r = this.r.toString(16);
var g = this.g.toString(16);
var b = this.b.toString(16);
if (r.length == 1) r = '0' + r;
if (g.length == 1) g = '0' + g;
if (b.length == 1) b = '0' + b;
return '#' + r + g + b;
}
// returns decimal array for R, G, and B
this.toDec = function () {
return [this.r/255.0,this.g/255.0,this.b/255.0];
}
// these return linearized R, G, or B
this.Rlin = function () {
return Math.pow(this.r/255.0, sRGBtrc);
}
this.Glin = function () {
return Math.pow(this.g/255.0, sRGBtrc);
}
this.Blin = function () {
return Math.pow(this.b/255.0, sRGBtrc);
}
this.toY = function () {
return Math.pow(this.r/255.0, sRGBtrc) * Rco + Math.pow(this.g/255.0, sRGBtrc) * Gco + Math.pow(this.b/255.0, sRGBtrc) * Bco;
}
}
//////////////////////////////////////////////////////////////
///// END sRGB INPUT FORM BLOCK //////////////////////
//////////////////////////////////////////////////////////////
<?xml version="1.1" encoding="utf-8"?>
<!DOCTYPE html>
<head>
<title>SAPC Contrast Demo</title>
<script src="SAPCsRGB.js">
// Place the above blocks in file SAPCsRGB.js
</script>
<script>
function getBGColor(s) {
var BGcolor = new RGBColor(s);
if (BGcolor.ok) {
document.getElementById('BGresult').style.backgroundColor
= 'rgb(' + BGcolor.r + ', ' + BGcolor.g + ', ' + BGcolor.b + ')';
document.getElementById('sansSamples').style.backgroundColor
= 'rgb(' + BGcolor.r + ', ' + BGcolor.g + ', ' + BGcolor.b + ')';
document.getElementById('serifSamples').style.backgroundColor
= 'rgb(' + BGcolor.r + ', ' + BGcolor.g + ', ' + BGcolor.b + ')';
document.getElementById('BGresult-text').innerHTML =
'<code><b>HEX:</b> ' + BGcolor.toHex()
+ ' <b>sRGB:</b> ' + BGcolor.toRGB()
+ '<br><b>Decimal:</b> rgb( ' + BGcolor.toDec()[0].toPrecision(2)
+ ', ' + BGcolor.toDec()[1].toPrecision(2)
+ ', ' + BGcolor.toDec()[2].toPrecision(2)
+ ' )<br><b>Lin:</b> R ' + BGcolor.Rlin().toPrecision(3)
+ ' G ' + BGcolor.Glin().toPrecision(3)
+ ' B ' + BGcolor.Blin().toPrecision(3)
+ '<br><b>Luminance:</b> Y ' + BGcolor.toY().toPrecision(3) + '</code>'
;
} else {
document.getElementById('BGresult-text').innerHTML = 'Invalid Color';
document.getElementById('BGresult').style.backgroundColor
= 'rgb(255, 255, 255)';
}
}
function getTextColor(s) {
var textColor = new RGBColor(s);
if (textColor.ok) {
document.getElementById('textResult').style.backgroundColor
= 'rgb(' + textColor.r + ', ' + textColor.g + ', ' + textColor.b + ')';
document.getElementById('sansSamples').style.color
= 'rgb(' + textColor.r + ', ' + textColor.g + ', ' + textColor.b + ')';
document.getElementById('serifSamples').style.color
= 'rgb(' + textColor.r + ', ' + textColor.g + ', ' + textColor.b + ')';
document.getElementById('textResult-text').innerHTML =
'<code><b>HEX:</b> ' + textColor.toHex()
+ ' <b>sRGB:</b> ' + textColor.toRGB()
+ '<br><b>Decimal:</b> rgb( ' + textColor.toDec()[0].toPrecision(2)
+ ', ' + textColor.toDec()[1].toPrecision(2)
+ ', ' + textColor.toDec()[2].toPrecision(2)
+ ' )<br><b>Lin:</b> R ' + textColor.Rlin().toPrecision(3)
+ ' G ' + textColor.Glin().toPrecision(3)
+ ' B ' + textColor.Blin().toPrecision(3)
+ '<br><b>Luminance:</b> Y ' + textColor.toY().toPrecision(3) + '</code>'
;
} else {
document.getElementById('textResult-text').innerHTML = 'Invalid Color';
document.getElementById('textResult').style.backgroundColor = 'rgb(255, 255, 255)';
}
}
</script>
</head>
<body onLoad="document.getElementById('inputBG').focus(); document.getElementById('inputText').focus(); document.getElementById('inputBG').focus(); testContrast(); ">
<div id="demoArea">
<div id="contrast">
<div id="contrastLabel">SAPC Contrast</div>
<div id="contrastResult">NaN</div>
</div>
<h1 style="text-align: left; ">SAPC Visual Contrast Demo</h1>
<div class="">
This is the Basic SAPC contrast method using JavaScript.
<br>Just type in color values as #hex, rgb(), or HTML name
<br>then navigate away from the input field by either
<br>pressing TAB or clicking elsewhere on the page.
</div>
</div>
<div id="inputArea">
<div style="position: relative; float: left;">
<h3>ENTER BACKGROUND COLOR
</h3>
<form id="colorForm" name="colorForm" onsubmit="getBGColor(this.elements[0].value); getTextColor(this.elements[1].value); return false;">
<div class="input">
<input
id="inputBG"
type="text"
value="#BA9"
onblur="getBGColor(this.value); testContrast();">
</div>
<div id="BGresult-wrap"><div id="BGresult"></div></div>
<div class="codeBlock" id="BGresult-text"></div>
</div>
<div style="position: relative; float: right;">
<h3>ENTER TEXT COLOR
</h3>
<div class="input">
<input
id="inputText"
type="text"
value="#319"
onblur="getTextColor(this.value); testContrast();">
</div>
</form>
<div id="textResult-wrap"><div id="textResult"></div></div>
<code id="textResult-text"></code>
</div>
</div>
<div style="font-size: 12px;">The page and code is Copyright © 2020 by Andrew Somers.
<br>Licensed to the W3.org per their cooperative agreement.
<br>Otherwise under the MIT license.
<br>Repository: <a href="https://github.com/Myndex/SAPC/tree/master/JS">https://github.com/Myndex/SAPC/tree/master/JS</a>
<br>Color value input parsing based substantially on rgbcolor.js by
<br>Stoyan Stefanov <sstoo@gmail.com> used per MIT license.
<br>
This project is part of the <a href="https://www.myndex.com/WEB/Perception">Myndex Web Perception Experiment.
</a>
<br><br>
<span onclick="testContrast()" style="cursor: pointer; text-decoration: underline">Manual Refresh</span>
<script>
function testContrast() {
var BG = new RGBColor(document.getElementById('inputBG').value);
var txt = new RGBColor(document.getElementById('inputText').value);
document.getElementById('contrastResult').innerHTML = SAPCbasic(BG.Rlin(),BG.Glin(),BG.Blin(),txt.Rlin(),txt.Glin(),txt.Blin());
}
</script>
</body>
</html>
Directions:
Font | Font Weight | ||||||||
---|---|---|---|---|---|---|---|---|---|
Size | 100 | 200 | 300 | 400(Normal) | 500 | 600 | 700(Bold) | 800 | 900 |
12px | ⊘ | ⊘ | ⊘ | 100% | 94% | 87% | 80% | 80% | 80% |
14px | ⊘ | ⊘ | ⊘ | 90% | 84% | 77% | 70% | 70% | 70% |
16px | ⊘ | ⊘ | 110% | 80% | 77% | 72% | 60% | 60% | 60% |
18px | ⊘ | ⊘ | 100% | 78% | 74% | 70% | 59% | 59% | 59% |
20px | ⊘ | 120% | 94% | 76% | 72% | 67% | 58% | 58% | 58% |
22px | ⊘ | 110% | 87% | 75% | 70% | 65% | 57% | 57% | 57% |
24px | ⊘ | 100% | 80% | 74% | 66% | 60% | 56% | 56% | 56% |
26px | ⊘ | 96% | 78% | 72% | 65% | 59% | 55% | 55% | 55% |
28px | ⊘ | 92% | 76% | 70% | 64% | 58% | 54% | 54% | 52% |
30px | ⊘ | 88% | 74% | 68% | 62% | 57% | 53% | 52% | 50% |
32px | ⊘ | 84% | 72% | 66% | 60% | 56% | 52% | 50% | 48% |
36px | 120% | 80% | 70% | 64% | 58% | 54% | 50% | 48% | 46% |
40px | 114% | 77% | 68% | 62% | 57% | 52% | 48% | 46% | 44% |
44px | 108% | 74% | 66% | 60% | 55% | 50% | 46% | 44% | 42% |
48px | 100% | 70% | 65% | 58% | 53% | 48% | 44% | 42% | 40% |
56px | 95% | 67% | 64% | 56% | 51% | 46% | 42% | 40% | 40% |
64px | 90% | 65% | 62% | 54% | 49% | 44% | 40% | 40% | 40% |
72px | 85% | 63% | 60% | 52% | 47% | 42% | 40% | 40% | 40% |
96px | 80% | 60% | 55% | 50% | 45% | 40% | 40% | 40% | 40% |
In the sRGB colorspace, using CSS color values as integers, with a background color sRGBbg and a text color sRGBtxt convert each channel to decimal 0.0-1.0 by dividing by 255, then linearize the gamma encoded RGB channels by applying a simple exponent.
Rlinbg = (sRbg/255.0) ^ 2.218 Glinbg = (sGbg/255.0) ^ 2.218 Blinbg = (sBbg/255.0) ^ 2.218
Rlintxt = (sRtxt/255.0) ^ 2.218 Glintxt = (sGtxt/255.0) ^ 2.218 Blintxt = (sBtxt/255.0) ^ 2.218
Then find the relative luminance (Y) of each color by applying the sRGB/Rec709 spectral coefficients and summing together.
Ybg = 0.2126 * Rlinbg + 0.7156 * Glinbg + 0.0722 * Blinbg Ytxt = 0.2126 * Rlintxt + 0.7156 * Glintxt + 0.0722 * Blintxt
The Predicted Visual Contrast (APCA) between a foreground color and a background color is expressed as a percentange and is calculated by:
// Define Constants for Basic APCA Version: normBGExp = 0.38; // Constants for Power Curve Exponents. normTXTExp = 0.43; // One pair for normal text, and one for REVERSE revBGExp = 0.5; // FUTURE: These will eventually be dynamic revTXTExp = 0.43; // as a function of light adaptation and context blkThrs = 0.02; // Level that triggers the soft black clamp blkClmp = 1.75; // Exponent for the soft black clamp curve // Calculate Predicted Contrast and return a string for the result if Ybg > Ytxt then { Ytxt = (Ytxt > blkThrs) ? Ytxt : Ytxt + abs(Ytxt - blkThrs) ^ blkClmp; APCA = ( Ybg ^ normBGExp - Ytxt ^ normTXTExp ) * 161.8; return (APCA < 15 ) ? "0%" : str(APCA) + "%"; } else { Ybgg = (Ybg > blkThrs) ? Ybg : Ybg + abs(Ybg - blkThrs) ^ blkClmp; APCA = ( Ybg ^ revBGExp - Ytxt ^ revTXTExp ) * 161.8; return (APCA > -15 ) ? "0%" : str(APCA) + "%"; }
Predicted contrast less than 15% is clamped to zero to simplify the math.
We will use the simple exponent, and not the piecewise sRGB transfer curve, as we are emulating display gamma and not performing image processing.
The “^” character is the exponentiation operator.