doc/themes/learn2-git-sync/scss/_contrast.scss

417 lines
13 KiB
SCSS
Raw Normal View History

/*
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);
}
}