wiki-grav/themes/learn2-git-sync/scss/_contrast.scss
2022-06-07 14:28:01 +02:00

417 lines
13 KiB
SCSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
WCAG color contrast formula
https://www.w3.org/TR/2016/NOTE-WCAG20-TECHS-20161007/G18#G18-procedure
@see https://codepen.io/giana/project/full/ZWbGzD
This pen uses the non-standard Sass pow() function
https://css-tricks.com/snippets/sass/power-function/
Using it outside of CodePen requires you provide your own pow() function with support for decimals
To generate random colors, we're also using a two-variable random() function includded with compass.
*/
@function pow($number, $exponent) {
@if (round($exponent) != $exponent) {
@return exp($exponent * ln($number));
}
$value: 1;
@if $exponent > 0 {
@for $i from 1 through $exponent {
$value: $value * $number;
}
} @else if $exponent < 0 {
@for $i from 1 through -$exponent {
$value: $value / $number;
}
}
@return $value;
}
@function factorial($value) {
$result: 1;
@if $value == 0 {
@return $result;
}
@for $index from 1 through $value {
$result: $result * $index;
}
@return $result;
}
@function summation($iteratee, $input, $initial: 0, $limit: 100) {
$sum: 0;
@for $index from $initial to $limit {
$sum: $sum + call($iteratee, $input, $index);
}
@return $sum;
}
@function exp-maclaurin($x, $n) {
@return (pow($x, $n) / factorial($n));
}
@function exp($value) {
@return summation(exp-maclaurin, $value, 0, 100);
}
@function ln-maclaurin($x, $n) {
@return (pow(-1, $n + 1) / $n) * (pow($x - 1, $n));
}
@function ln($value) {
$ten-exp: 1;
$ln-ten: 2.30258509;
@while ($value > pow(10, $ten-exp)) {
$ten-exp: $ten-exp + 1;
}
@return summation(ln-maclaurin, $value / pow(10, $ten-exp), 1, 100) + $ten-exp * $ln-ten;
}
// Check if value is not a number, eg, NaN or Infinity
@function is-nan($value) {
@return $value / $value != 1;
}
// Constrain number between two values
@function clip($value, $min : 0.0001, $max : 0.9999) {
@return if($value > $max, $max, if($value < $min, $min, $value));
}
// Checks if value is within specified bounds, inclusive
@function in-bounds($value, $min : 0, $max : 1) {
@return if($value >= $min and $value <= $max, true, false);
}
//== Step one: Convert
// Returns an RGB channel processed as XYZ... or partly at least
// See w3.org link for formula
@function xyz($channel) {
$channel: $channel / 255;
@return if($channel <= 0.03928, $channel / 12.92, pow((($channel + 0.055) / 1.055), 2.4));
}
// Reverse of xyz(). Returns XYZ value to RGB channel
// https://en.wikipedia.org/wiki/SRGB
@function srgb($channel) {
@return 255 * if($channel <= 0.0031308, $channel * 12.92, 1.055 * pow($channel, 1/2.4) - 0.055);
}
//== Step two: Measure brightness
// Returns relative luminance of color
// See w3.org link for formula
@function luminance($color) {
$red: xyz(red($color));
$green: xyz(green($color));
$blue: xyz(blue($color));
@return $red * 0.2126 + $green * 0.7152 + $blue * 0.0722;
}
//== Step three: Check contrast
// Checks if two colors pass minimum contrast requirements, option to return ratio instead of true/false
// See w3.org link for formula
@function check-contrast($color1, $color2 : #fff, $min-ratio : 'AA', $return-ratio : false) {
// Accept keywords for ratio
@if($min-ratio == 'AA' or $min-ratio == 'AAALG') { $min-ratio: 4.5; }
@elseif($min-ratio == 'AALG') { $min-ratio: 3; }
@elseif($min-ratio == 'AAA') { $min-ratio: 7; }
// Check brightness of each color
$lum1: luminance($color1);
$lum2: luminance($color2);
// Measure contrast ratio
$ratio: (max($lum1, $lum2) + 0.05) / (min($lum1, $lum2) + 0.05);
// Return ratio if option set
@if($return-ratio) { @return $ratio; }
// Else return boolean
@return if($ratio >= $min-ratio, true, false);
}
//== Step four: Scale luminance and lightness
// Takes color, scales luminance, spits out new color
@function scale-luminance($color, $target-luminance) {
// First, scale the channels by the required amount
$scale: $target-luminance / luminance($color);
// And clip them, so we don't end up dividing by zero... among other things I forget
$red: clip(xyz(red($color))) * $scale;
$green: clip(xyz(green($color))) * $scale;
$blue: clip(xyz(blue($color))) * $scale;
// Sometimes, that's not enough and one channel hits #ff or #00. We'll need to scale the other channels to compensate
$red-passes: in-bounds($red);
$green-passes: in-bounds($green);
$blue-passes: in-bounds($blue);
@if(not $red-passes or not $green-passes or not $blue-passes) {
// First, pick a channel to be a baseline, so the rest can be expressed as ratios
$baseline: min($red, $green, $blue);
// Then set up the variables expressed in terms of the baseline
$r: $red / $baseline;
$g: $green / $baseline;
$b: $blue / $baseline;
// Subtract any channel no longer in bounds
//-- TODO This needs to DRY. how to dry. help
@if(not $red-passes) {
$target-luminance: $target-luminance - 0.2126;
$r: 0;
}
@if(not $green-passes) {
$target-luminance: $target-luminance - 0.7152;
$g: 0;
}
@if (not $blue-passes) {
$target-luminance: $target-luminance - 0.0722;
$b: 0;
}
// Now get the required difference by using the luminance() formula
$x: $target-luminance / ($r * 0.2126 + $g * 0.7152 + $b * 0.0722);
// And multiply the channels by this new per-channel luminance
@if($red-passes) { $red: $r * $x; }
@if($green-passes) { $green: $g * $x; }
@if($blue-passes) { $blue: $b * $x; }
}
// Return the new color
@return rgb(srgb($red), srgb($green), srgb($blue));
}
// Scales lightness by 0.1% while checking contrast ratio. This is just a last-ditch effort to correct rounding errors
@function scale-light($color1, $color2, $min-ratio, $operation, $iterations) {
// Loop this function for however many iterations are passed
@for $n from 1 through $iterations {
// Return color unchanged if it passes contrast check
@if(check-contrast($color1, $color2, $min-ratio)) {
@return $color1;
} @else {
// Otherwise use the built-in lighten() and darken() functions, which change the lightness channel (ie, the L in HSL)
// Our previous scale-luminance() function changes both saturation and lightness
$color1: if($operation == lighten, lighten($color1, 0.1%), darken($color1, 0.1%));
}
}
// Return the best color we've got
@return $color1;
}
//== Step six: Fix colors
// Tries to fix contrast by adjusting $color1
@function fix-color($color1, $color2 : #fff, $min-ratio : 'AA', $iterations : 5) {
// Accept keywords for ratio
@if($min-ratio == 'AA' or $min-ratio == 'AAALG') { $min-ratio: 4.5; }
@elseif($min-ratio == 'AALG') { $min-ratio: 3; }
@elseif($min-ratio == 'AAA') { $min-ratio: 7; }
// If check fails, begin conversion
@if(not check-contrast($color1, $color2, $min-ratio)) {
// First get both luminances and clip so #fff and #000 don't break anything
$lum1: clip(luminance($color1));
$lum2: clip(luminance($color2));
// Defaults we'll set later
$target-luminance: $lum1;
$operation: '';
// If the same luminance is passed, lighten/darken one to make conversion possible
@if($lum1 == $lum2) {
// Darken light colors and lighten dark colors, so we have more room to scale them (eg, we won't hit #fff or #000 before we can fix them)
@if($lum1 > 0.5) {
$color1: darken($color1, 1%);
$lum1: luminance($color1);
} @else {
$color1: lighten($color1, 1%);
$lum1: luminance($color1);
}
}
// Now let's get the target luminance. This basically reverses check-contrast(), so we know what luminance to aim for
@if(max($lum1, $lum2) == $lum1) {
$target-luminance: (($lum2 + 0.05) * $min-ratio - 0.05);
$operation: lighten;
} @else {
$target-luminance: (($lum2 + 0.05) / $min-ratio - 0.05);
$operation: darken;
}
// Skip the whole conversion if we just need #fff or #000
@if($target-luminance >= 1) { @return #fff; }
@elseif ($target-luminance <= 0) { @return #000; }
@else {
// Scale color by calculated difference to arrive at target luminance
$color1: scale-luminance($color1, $target-luminance);
// Try to fix any rounding errors by lightening or darkening
$color1: scale-light($color1, $color2, $min-ratio, $operation, $iterations);
}
}
// Tada
@return $color1;
}
// Tries to fix contrast of both colors by weighted balance (0100)
// 0 = don't change first color, change second color;
// 100 = change first color, don't change second color
@function fix-contrast($color1, $color2, $min-ratio : 'AA', $balance : 50) {
@if(not check-contrast($color1, $color2, $min-ratio)) {
// Fix colors
$color-fixed-1: fix-color($color1, $color2, $min-ratio);
$color-fixed-2: fix-color($color2, $color1, $min-ratio);
// We're just fixing both colors, then mixing back the original color using the native Sass function. Easy-peasy
$color1: mix($color-fixed-1, $color1, $balance);
$color2: mix($color2, $color-fixed-2, $balance);
// If the current configuration doesn't work, try to fix it
@if (not check-contrast($color1, $color2, $min-ratio)) {
// This happens if, again, we reach #fff or #000 before we want to
@if(not in-bounds(luminance($color-fixed-2), 0.00002, 0.99936)) {
// So we scale the opposite color to compensate
$color1: fix-color($color1, $color2, $min-ratio);
@warn "Your settings didn't work. Modifying first color in an attempt to fix."
}
@if(not in-bounds(luminance($color-fixed-1), 0.00002, 0.99936)) {
$color2: fix-color($color2, $color1, $min-ratio);
@warn "Your settings didn't work. Modifying second color in an attempt to fix."
}
}
}
// Returns a list with both colors, use nth($result, 1) and nth($result, 2) to get colors. See below for example
@return $color1, $color2;
}
// Get the best contrast when given three colors
@function best-contrast($color, $color1, $color2, $ratio1 : 'AA', $ratio2 : $ratio1) {
@if(not check-contrast($color, $color1, $ratio1) or not check-contrast($color, $color2, $ratio2)) {
// First get the luminance of the two static colors
$lum1: luminance(fix-color($color1, $color1, $ratio1));
$lum2: luminance(fix-color($color2, $color2, $ratio2));
// Average the luminance together to get the maximum difference
$average-lum: ($lum1 + $lum2) / 2;
// Then set changing color to this luminance
$color: scale-luminance($color, $average-lum);
// Warn if it fails contrast check
@if(not check-contrast($color, $color1, $ratio1)) {
@warn 'Your color fails to contrast with #{$color1}';
}
@if(not check-contrast($color, $color2, $ratio2)) {
@warn 'Your color fails to contrast with #{$color2}';
}
}
@return $color;
}
//====== Helper functions
@function randomColor() {
$color: hsl(random(360), random(100), random(100));
@return $color;
}
@mixin show-color($color) {
background: $color;
color: if(luminance($color) > 0.55, #000, #fff);
&::after {
content: '#{$color}';
}
}
//====== Put in your own settings here
$ratio: random(21); // A number between 1 and 21
$balance: random(100); // A number between 0 and 100
// Any valid color
$color1: randomColor();
$color2: scale-luminance(randomColor(), luminance($color1) + 0.1);
$color3: randomColor();
.ratio::after { content: '#{$ratio}'; }
.balance::after { content: '#{$balance}'; }
.color-block .color1 { @include show-color($color1); }
.color-block .color2 { @include show-color($color2); }
.fix-color {
.color:nth-child(2) { @include show-color(fix-color($color1, $color2, $ratio)); }
.color:nth-child(3) { @include show-color(fix-color($color2, $color1, $ratio)); }
}
.fix-contrast {
.color:nth-child(2) { @include show-color(nth(fix-contrast($color1, $color2, $ratio, $balance),1)); }
.color:nth-child(3) { @include show-color(nth(fix-contrast($color1, $color2, $ratio, $balance),2)); }
}
.best-contrast {
.color:nth-child(2) { @include show-color($color3); }
.color:nth-child(3) { @include show-color(best-contrast($color3, $color1, $color2, $ratio, $ratio)); }
}
.scale-luminance {
.color:nth-child(2) { @include show-color(scale-luminance($color1, luminance($color2))); }
}
.check-contrast {
.result::after { content: '#{check-contrast($color1, $color2, $ratio)}' ;}
}
.luminance {
.result::after { content: '#{luminance($color1), luminance($color2)}' ;}
}
// Amplify (strengthen) color by percentage
// @see https://www.scrivito.com/blog/sass-magic
@function amplify($color, $percentage) {
@if (lightness( $color ) <= 50) {
@return darken($color, $percentage);
}
@else {
@return lighten($color, $percentage);
}
}
// Diminish (weaken) color by percentage
@function diminish($color, $percentage) {
@if (lightness( $color ) >= 50) {
@return darken($color, $percentage);
}
@else {
@return lighten($color, $percentage);
}
}