jhogue / automated-a11y-sass

Sass functions that support accessible color contrast ratios
Creative Commons Zero v1.0 Universal
34 stars 8 forks source link

Unexpected light output of light-or-dark function #2

Open gfellerph opened 1 year ago

gfellerph commented 1 year ago

Hello, really appreciating the effort and very much looking forward to https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-contrast.

I noticed two things with the functions:

I also did some tests and was not sure if the function light-or-dark is intended to be used on its own because the following output does not seem right:

@debug (light-or-dark(rgb(30,30,30))); // => "light"
@debug (light-or-dark(rgb(25, 25, 25))); // => "dark"

rgb(30, 30, 30) should not be considered light, as writing with black color on this background is almost invisible (https://webaim.org/resources/contrastchecker/?fcolor=000000&bcolor=1E1E1E).

Here is the file I used to test using sass 1.58 with auto-migrated slash division, defined white and black variables and debug statements at the end:

@use "sass:math";

$white: #fff;
$black: #000;

// @function strip-unit($value)
// @param {number with unit} $value
@function strip-unit($value) {
  @return math.div($value, $value * 0 + 1);
}

// @function pow($base, $exponents)
// Helper: Raise a number the power of another number
// Adapted from: https://gist.github.com/hail2u/1964056
//
// @param {number} $base — The base number
// @param {number} $exponents — The exponent to which to raise $base
@function pow($base, $exponents) {
  $raised: 1;
  @for $i from 1 through $exponents {
    $raised: $raised * $base;
  }
  @return $raised;
}

// @function luminance($color)
// Helper: Calculate Luminance of a single color
// Adapted from: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796
// White luminance is 1, Black luminance is 0
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { luminance: luminance(#c00); }
// Output:
// .sample { luminance: 0.1283736922; }
//
@function luminance($color) {
  // Thanks voxpelli for a very concise implementation of luminance measure in sass
  // Adapted from: https://gist.github.com/voxpelli/6304812
  $rgba: red($color), green($color), blue($color);
  $rgba2: ();
  @for $i from 1 through 3 {
    $rgb: nth($rgba, $i);
    $rgb: math.div($rgb, 255);
    $rgb: if($rgb < .03928, math.div($rgb, 12.92), pow(math.div($rgb + .055, 1.055), 2));
    $rgba2: append($rgba2, $rgb);
  }
  @return (.2126 * nth($rgba2, 1) + .7152 * nth($rgba2, 2) + 0.0722 * nth($rgba2, 3))*100;
}

// @function contrast-ratio($fg, $bg)
// Helper: Calculate "readability" as defined by WCAG 2.1
// Adapted from: https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js
// Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { color-contrast: contrast-ratio(#c00, #fff); }
// Output:
// .sample { color-contrast: 5.89; }
//
@function contrast-ratio($fg, $bg) {
  $luminance1: luminance($fg) + 0.05;
  $luminance2: luminance($bg) + 0.05;
  $ratio: math.div($luminance1, $luminance2);
  @if $luminance2 > $luminance1 {
    $ratio: math.div(1, $ratio);
  }
  // Round to a hundreth because 6.96 should not pass a ratio of 7.0
  $ratio: round($ratio * 100) * 0.01;
  @return $ratio;
}

// @function validate-font-size($size)
// Helper: Depending on the unit recalculate a font size value into pixels if possible
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { validate-font-size: validate-font-size(1em); }
// Output:
// .sample { validate-font-size: 16; }
//
@function validate-font-size($size) {
  @if unit($size) == 'em' or unit($size) == 'rem' or unit($size) == 'px' or unit($size) == '' {
    // Check if a flexible unit
    @if unit($size) == 'em' or unit($size) == 'rem' {
      // Need to convert to a pixel value. Let's not overcomplicate it with possible EM inheritence scale factors
      @return strip-unit($size * 16)
    }
    @if unit($size) == 'px' {
      // We expect PX, so strip the value and return it
      @return strip-unit($size);
    }
    @if unit($size) == '' {
      @return $size;
    }
  } @else {
    @error 'validate-font-size(): An unexpected font size unit was supplied.';
  }
}

// @function get-ratio($level: 'AA', $size: 16, $bold: false)
// Helper: Determine the correct ratio value to use based on font-size and WCAG Level
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { get-ratio: get-ratio('AAA', 19, true); }
// Output:
// .sample { get-ratio: 4.5; }
//
@function get-ratio($level: 'AA', $size: 16, $bold: false) {
  // Default ratio
  $ratio: 4.5;
  @if $level == 'AAA' {
    $ratio: 7;
  }

  // Make sure the size is valid. If the value is not EM, REM, or PX (preferred), we can't help
  $size: validate-font-size($size);

  // Check font size
  @if $size < 24 {
    // Small text, use defaults
    // But:
    @if $size >= 19 and $bold == true {
      // Special case: Small text but also bold
      @if $level == 'AAA' {
        $ratio: 4.5;
      } @else {
        $ratio: 3;
      }
    }
  } @else {
    // Larger than 24
    $ratio: 3;
    @if $level == 'AAA' {
      $ratio: 4.5;
    }
  }
  @return $ratio;
}

// @function light-or-dark($color)
// Helper: Use contrast against white or black to determine if a color is "light" or "dark"
// Adapted from: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { light-or-dark: light-or-dark(#c00); }
// Output:
// .sample { light-or-dark: "light"; }
//
@function light-or-dark($color) {
  $light-contrast: contrast-ratio($color, $white);
  $dark-contrast: contrast-ratio($color, $black);

  @if $light-contrast > $dark-contrast {
    // Contrast against white is higher than against black, so, this is a dark color
    @return "dark";
  } @else {
    @return "light";
  }
}

// @function color-contrast($color)
// Helper: Returns black or white compared to a color, whichever produces the highest contrast
// Usage:
// .sample {
//   background-color: #c00;
//   color: color-contrast(#c00);
// }
// Output:
// .sample {
//   background-color: #c00;
//   color: #fff;
// }
//
// Named for the forthcoming CSS Module 5 spec function color-contrast();
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-contrast()
//
@function color-contrast($color) {
  $color-lod: light-or-dark($color);

  @if ($color-lod == "dark") {
    @return $white;
  } @else {
    @return $black;
  }
}

// @function a11y-color($fg, $bg, $level: 'AA', $size: 16, $bold: false)
// Goal: Return a color that passes for the chosen WCAG level without changing the Hue of the color
// Usage:
// .sample {
//   background-color: #000;
//   color: a11y-color(#c0c, #000);
// }
// Output:
// .sample {
//   background-color: #000;
//   color: #d200d2;
// }
//
@function a11y-color($fg, $bg, $level: 'AA', $size: 16, $bold: false) {
  // Helper: make sure the font size value is acceptable
  $font-size: validate-font-size($size);
  // Helper: With the level, font size, and bold boolean, return the proper target ratio. 3.0, 4.5, or 7.0 expected
  $ratio: get-ratio($level, $font-size, $bold);
  // Calculate the first contrast ratio of the given pair
  $original-contrast: contrast-ratio($fg, $bg);

  @if $original-contrast >= $ratio {
    // If we pass the ratio already, return the original color
    @return $fg;
  } @else {
    // Doesn't pass. Time to get to work
    // Should the color be lightened or darkened?
    $fg-lod: light-or-dark($fg);
    $bg-lod: light-or-dark($bg);

    // Set a "step" value to lighten or darken a color
    // Note: Higher percentage steps means faster compile time, but we might overstep the required threshold too far with something higher than 4%
    $step: 1%;

    // Run through some cases where we want to darken, or use a negative step value
    @if $fg-lod == 'light' and $bg-lod == 'light' {
      // Both are light colors, darken the fg
      $step: - $step;
    } @else if $fg-lod == 'dark' and $bg-lod == 'light' {
      // bg is light, fg is dark but does not pass, darken more
      $step: - $step;
    }
    // Keeping the rest of the logic here, but our default values do not change, so this logic is not needed
    //@else if $fg-lod == 'light' and $bg-lod == 'dark' {
    //  // bg is dark, fg is light but does not pass, lighten further
    //  $step: $step;
    //} @else if $fg-lod == 'dark' and $bg-lod == 'dark' {
    //  // Both are dark, so lighten the fg
    //  $step: $step;
    //}

    // The magic happens here
    // Loop through with a @while statement until the color combination passes our required ratio. Scale the color by our step value until the expression is false
    // This might loop 100 times or more depending on the colors
    @while contrast-ratio($fg, $bg) < $ratio {
      $sat-step: 0%;
      @if saturation($fg) > 10 {
        $sat-step: $step;
      }
      $fg: scale-color($fg, $lightness: $step, $saturation:$sat-step);
    }
    @return $fg;
  }
}

@debug (light-or-dark(rgb(30,30,30)));
@debug (light-or-dark(rgb(25, 25, 25)));
@debug (light-or-dark(#1E1E1E));