const smContrastRatio = 4.5; const lgContrastRatio = 3; const rc = 0.2126; const gc = 0.7152; const bc = 0.0722; const lowc = 1 / 12.92; window.addEventListener('load', adaContrast_testContrast); function adaContrast_testContrast() { var computedStyle = window.getComputedStyle(document.body); console.log("ADA Contrast Remediation: Started"); [].slice.apply(document.body.querySelectorAll("*"), null).forEach(function (node) { for (var i = 0; i < node.childNodes.length; i++) { var child = node.childNodes[i]; if (child.nodeType == Node.TEXT_NODE) { adaContrast_getElementBGColor(node); } } }); }; function adaContrast_getBGStyleNode(target) { var computedStyle = window.getComputedStyle(target); var parent = target.parentNode; if (computedStyle.backgroundColor != "rgba(0, 0, 0, 0)" || computedStyle.backgroundImage != "none" || target.tagName.toLowerCase() == "body") { return target; } else { return adaContrast_getBGStyleNode(parent); } }; function adaContrast_getElementBGColor(node) { var bgStyleNode = adaContrast_getBGStyleNode(node); var bgComputedStyle = window.getComputedStyle(bgStyleNode); if (bgComputedStyle.backgroundColor != "rgba(0, 0, 0, 0)" && bgComputedStyle.backgroundImage == "none") { // node is colored without a background image adaContrast_fixContrast(node, adaContrast_RGBStringToValues(bgComputedStyle.backgroundColor)); } else if (bgComputedStyle.backgroundImage != "none") { // node has background image, need to set the backup color let imageURL = bgComputedStyle.backgroundImage.slice(4, -1).replace(/"/g, ""); downloadedImg = new Image; downloadedImg.crossOrigin = "anonymous"; downloadedImg.addEventListener("load", function () { var canvas = document.createElement('canvas'); var context = canvas.getContext && canvas.getContext('2d'); var height = canvas.height = downloadedImg.naturalHeight || downloadedImg.offsetHeight || downloadedImg.height; var width = canvas.width = downloadedImg.naturalWidth || downloadedImg.offsetWidth || downloadedImg.width; if (height != 0 && width != 0) { try { context.drawImage(downloadedImg, 0, 0); var data = context.getImageData(0, 0, width, height); var length = data.data.length; var count = 0; var blockSize = 5; var rgbBackground = { r: 0, g: 0, b: 0 }; var i = -4; while ((i += blockSize * 4) < length) { ++count; rgbBackground.r += data.data[i]; rgbBackground.g += data.data[i + 1]; rgbBackground.b += data.data[i + 2]; } rgbBackground.r = ~~(rgbBackground.r / count); rgbBackground.g = ~~(rgbBackground.g / count); rgbBackground.b = ~~(rgbBackground.b / count); } catch (e) { /* security error, img on diff domain */ console.log("ADA Contrast Remediation (Security Error): " + e); } // set the background for screen reader compatibility if (!bgStyleNode.hasAttribute("data-ada-bgcolor-added")) { bgStyleNode.style.backgroundColor = adaContrast_RGBValuesToString(rgbBackground); bgStyleNode.setAttribute("data-ada-bgcolor-added", true); } // adjust the font color adaContrast_fixContrast(node, rgbBackground); } else { // element is not in screen }; }, false); downloadedImg.src = imageURL; } else { adaContrast_fixContrast(node, { r: 255, g: 255, b: 255 }); }; }; async function adaContrast_fixContrast(node, rgbBackground) { var computedStyle = window.getComputedStyle(node); var fontSize = window.getComputedStyle(node).fontSize.replace("px", ""); var fontBold = false; if (computedStyle.fontWeight) { fontBold = computedStyle.fontWeight >= 700 ? true : false; } var rgbForeground = adaContrast_RGBStringToValues(computedStyle.color); var fgLum = adaContrast_relativeLuminance(rgbForeground); var bgLum = adaContrast_relativeLuminance(rgbBackground); var lumTest = adaContrast_calculateLuminanceRatio(fgLum, bgLum); // TODO PPI calc https://www.mcdpartners.com/news/meeting-wcag-color-contrast-guideline/ // TODO Alpha // TODO Diverge Background Color var isLargeFont = (fontSize >= 24 || fontSize >= 19 && fontBold ); var requiredContrastRatio = isLargeFont ? lgContrastRatio : smContrastRatio; if (lumTest.ratio < requiredContrastRatio) { const label = "ADA Contrast Remediation: " + lumTest.ratio + ":1 Needs " + requiredContrastRatio + ":1"; console.groupCollapsed(label); console.info("DOM Location: " + adaContrast_getQuerySelector(node)); var loopCounter = 0; var fix19Color; var fix19BoldColor; var fixBoldColor; var passed = false; while (lumTest.ratio < requiredContrastRatio && loopCounter < 50) { loopCounter += 1; if (lumTest.ratio >= lgContrastRatio) { if (!fix19Color && fontSize < 19 && fontBold) { fix19Color = rgbForeground; console.info("Increase to 19px Level: " + adaContrast_RGBValuesToString(fix19Color)); } else if (!fix19BoldColor && fontSize < 19 && !fontBold) { fix19BoldColor = rgbForeground; console.info("Increase to 19px & Set Bold Level: " + adaContrast_RGBValuesToString(fix19BoldColor)); } else if (!fixBoldColor && fontSize > 19 && fontSize < 24 && !fontBold) { fixBoldColor = rgbForeground; console.info("Set Bold Level: " + adaContrast_RGBValuesToString(fixBoldColor)); }; } var hsl = adaContrast_RGBToHSL(rgbForeground); if (lumTest.isFgDarker) { hsl.l = hsl.l - (hsl.l * 0.05); // needs to get darker } else { hsl.l = (hsl.l * 1.05); // needs to get lighter } rgbForeground = adaContrast_HSLToRGB(hsl); fgLum = adaContrast_relativeLuminance(rgbForeground); lumTest = adaContrast_calculateLuminanceRatio(fgLum, bgLum); passed = (lumTest.ratio < requiredContrastRatio ? false : true) console.info("Correction Test: " + lumTest.ratio + ":1 w/ " + adaContrast_RGBValuesToString(rgbForeground) + " [" + (passed ? "PASS" : "FAIL") + "]"); if (rgbForeground.r >= 255 && rgbForeground.g >= 255 && rgbForeground.b >= 255) { break; } if (rgbForeground.r <= 0 && rgbForeground.g <= 0 && rgbForeground.b <= 0) { break; } } if (rgbForeground.r > 255) { rgbForeground.r = 255 }; if (rgbForeground.g > 255) { rgbForeground.g = 255 }; if (rgbForeground.b > 255) { rgbForeground.b = 255 }; if (rgbForeground.r < 0) { rgbForeground.r = 0 }; if (rgbForeground.g < 0) { rgbForeground.g = 0 }; if (rgbForeground.b < 0) { rgbForeground.b = 0 }; if (!passed) { console.info("Unable to Find Passing Color"); } if (!passed && fixBoldColor) { rgbForeground = fixBoldColor; node.style.fontWeight = 700; console.info("Bolding Element to Pass with Color: " + adaContrast_RGBValuesToString(rgbForeground)); } else if (!passed && fix19Color) { rgbForeground = fix19Color; node.style.fontSize = 19; console.info("Increasing Font Size of Element to Pass with Color: " + adaContrast_RGBValuesToString(rgbForeground)); } else if (!passed && fix19BoldColor) { rgbForeground = fix19BoldColor; node.style.fontSize = 19; node.style.fontWeight = 700; console.info("Increasing Font Size and Bolding Element to Pass with Color: " + adaContrast_RGBValuesToString(rgbForeground)); } else if (!passed) { console.info("Using Closest Usable Color: " + adaContrast_RGBValuesToString(rgbForeground)); }else { console.info("Using Passing Font Color: " + adaContrast_RGBValuesToString(rgbForeground)); } console.groupEnd(label); node.style.color = adaContrast_RGBValuesToString(rgbForeground); } } function adaContrast_calculateLuminanceRatio(fgLum, bgLum) { var L1 = fgLum > bgLum ? fgLum : bgLum var L2 = fgLum < bgLum ? fgLum : bgLum var lumTest = { isFgDarker: (fgLum < bgLum), ratio: ((L1 + 0.05) / (L2 + 0.05)) }; return lumTest; } function adaContrast_relativeLuminance(rgb) { let rsrgb = rgb.r / 255; let gsrgb = rgb.g / 255; let bsrgb = rgb.b / 255; let r = rsrgb <= 0.03928 ? rsrgb * lowc : Math.pow((rsrgb + 0.055) / 1.055, 2.4); let g = gsrgb <= 0.03928 ? gsrgb * lowc : Math.pow((gsrgb + 0.055) / 1.055, 2.4); let b = bsrgb <= 0.03928 ? bsrgb * lowc : Math.pow((bsrgb + 0.055) / 1.055, 2.4); return r * rc + g * gc + b * bc; } function adaContrast_RGBStringToValues(input) { var rgbArray = input.substr(4).split(")")[0].split(input.indexOf(",") > -1 ? "," : " "); return { r: rgbArray[0], g: rgbArray[1], b: rgbArray[2] };; } function adaContrast_RGBValuesToString(rgb) { return 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')'; } function adaContrast_RGBToHSL(rgb) { var r = rgb.r; var g = rgb.g; var b = rgb.b; // Make r, g, and b fractions of 1 r /= 255; g /= 255; b /= 255; // Find greatest and smallest channel values let cmin = Math.min(r, g, b), cmax = Math.max(r, g, b), delta = cmax - cmin, h = 0, s = 0, l = 0; // Calculate hue // No difference if (delta == 0) h = 0; // Red is max else if (cmax == r) h = ((g - b) / delta) % 6; // Green is max else if (cmax == g) h = (b - r) / delta + 2; // Blue is max else h = (r - g) / delta + 4; h = Math.round(h * 60); // Make negative hues positive behind 360° if (h < 0) h += 360; // Calculate lightness l = (cmax + cmin) / 2; // Calculate saturation s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); // Multiply l and s by 100 s = +(s * 100).toFixed(1); l = +(l * 100).toFixed(1); return { h: h, s: s, l: l }; //return "hsl(" + h + "," + s + "%," + l + "%)"; } function adaContrast_HSLToRGB(hsl) { var h = hsl.h; var s = hsl.s; var l = hsl.l; // Must be fractions of 1 s /= 100; l /= 100; let c = (1 - Math.abs(2 * l - 1)) * s, x = c * (1 - Math.abs((h / 60) % 2 - 1)), m = l - c / 2, r = 0, g = 0, b = 0; if (0 <= h && h < 60) { r = c; g = x; b = 0; } else if (60 <= h && h < 120) { r = x; g = c; b = 0; } else if (120 <= h && h < 180) { r = 0; g = c; b = x; } else if (180 <= h && h < 240) { r = 0; g = x; b = c; } else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else if (300 <= h && h < 360) { r = c; g = 0; b = x; } r = Math.round((r + m) * 255); g = Math.round((g + m) * 255); b = Math.round((b + m) * 255); return { r: r, g: g, b: b }; } function adaContrast_getQuerySelector(el) { // top node if (el.tagName.toLowerCase() == "html") { return "html"; }; if (el.tagName.toLowerCase() == "head") { return adaContrast_getQuerySelector(el.parentNode) + " > " + "head"; }; if (el.tagName.toLowerCase() == "body") { return adaContrast_getQuerySelector(el.parentNode) + " > " + "body"; }; if (el.tagName.toLowerCase() == "title") { return adaContrast_getQuerySelector(el.parentNode) + " > " + "title"; }; // set id if exists var str = el.tagName.toLowerCase(); str += (el.id != "") ? "#" + el.id : ""; // check for items in parent var nth = 0; var child = el.parentNode.firstChild; while (true) { if (child.nodeType === Node.ELEMENT_NODE) { if (el.tagName.toLowerCase() === child.tagName.toLowerCase()) { nth++; } }; if (child === el || !child.nextSibling) { break }; child = child.nextSibling; } return adaContrast_getQuerySelector(el.parentNode) + " > " + str + (nth <= 0 ? "" : ":nth-of-type(" + nth + ")"); }