UX: Use oklab color space for perceptually-uniform lightness

Oklab is supported natively in the latest versions of all major browsers. We'll need to wait until we drop support for Safari 14 before using it directly, but we can use it for sass color transformations right away. Its perceptually-uniform lighness makes it significantly better than hsl for our automatic light/dark variations of colors.
This commit is contained in:
David Taylor 2023-12-04 10:36:54 +00:00
parent 56795f5c07
commit 67c68c9c60
No known key found for this signature in database
GPG Key ID: 46904C18B1D3F434
7 changed files with 151 additions and 32 deletions

View File

@ -3,24 +3,46 @@
// all variables should have the !default flag
//primary
$primary-very-low: dark-light-diff($primary, $secondary, 97%, -82%) !default;
$primary-low: dark-light-diff($primary, $secondary, 90%, -78%) !default;
$primary-low-mid: dark-light-diff($primary, $secondary, 70%, -45%) !default;
$primary-medium: dark-light-diff($primary, $secondary, 50%, -35%) !default;
$primary-high: dark-light-diff($primary, $secondary, 30%, -25%) !default;
$primary-very-high: dark-light-diff($primary, $secondary, 15%, -10%) !default;
@function _dark-primary-shade($units) {
$primary-50-lightness: -72%;
$primary-900-lightness: -8%;
$step: math.div($primary-50-lightness - $primary-900-lightness, 850);
@return $primary-50-lightness - ($step * ($units - 50));
}
//primary-numbers
$primary-50: dark-light-diff($primary, $secondary, 97%, -82%) !default;
$primary-100: dark-light-diff($primary, $secondary, 94%, -80%) !default;
$primary-200: dark-light-diff($primary, $secondary, 90%, -78%) !default;
$primary-300: dark-light-diff($primary, $secondary, 80%, -60%) !default;
$primary-400: dark-light-diff($primary, $secondary, 70%, -45%) !default;
$primary-500: dark-light-diff($primary, $secondary, 60%, -40%) !default;
$primary-600: dark-light-diff($primary, $secondary, 50%, -35%) !default;
$primary-700: dark-light-diff($primary, $secondary, 38%, -30%) !default;
$primary-800: dark-light-diff($primary, $secondary, 30%, -25%) !default;
$primary-900: dark-light-diff($primary, $secondary, 15%, -10%) !default;
@function _light-primary-shade($units) {
$primary-50-lightness: 90%;
$primary-900-lightness: 18%;
$step: math.div($primary-50-lightness - $primary-900-lightness, 850);
@return $primary-50-lightness - ($step * ($units - 50));
}
@function _numbered-primary($units) {
@return dark-light-diff(
$primary,
$secondary,
_light-primary-shade($units),
_dark-primary-shade($units)
);
}
$primary-50: _numbered-primary(50) !default;
$primary-100: _numbered-primary(100) !default;
$primary-200: _numbered-primary(200) !default;
$primary-300: _numbered-primary(300) !default;
$primary-400: _numbered-primary(400) !default;
$primary-500: _numbered-primary(500) !default;
$primary-600: _numbered-primary(600) !default;
$primary-700: _numbered-primary(700) !default;
$primary-800: _numbered-primary(800) !default;
$primary-900: _numbered-primary(900) !default;
$primary-very-low: $primary-50 !default;
$primary-low: $primary-200 !default;
$primary-low-mid: $primary-400 !default;
$primary-medium: $primary-600 !default;
$primary-high: $primary-800 !default;
$primary-very-high: $primary-900 !default;
$header_primary-low: blend-header-primary-background(10%) !default;
$header_primary-low-mid: blend-header-primary-background(35%) !default;

View File

@ -0,0 +1,68 @@
@use "sass:math";
@use "sass:color";
@use "sass:map";
/**
Adapted from https://bottosson.github.io/posts/colorwrong and https://bottosson.github.io/posts/oklab/
(public domain)
*/
@function component-to-nonninear($x) {
@if $x >= 0.0031308 {
@return (1.055) * math.pow($x, math.div(1, 2.4)) - 0.055;
} @else {
@return 12.92 * $x;
}
}
@function component-to-linear($x) {
@if ($x >= 0.04045) {
@return math.pow(math.div($x + 0.055, 1 + 0.055), 2.4);
} @else {
@return math.div($x, 12.92);
}
}
@function rgb_to_oklab($color) {
$r: component-to-linear(math.div(color.red($color), 255));
$g: component-to-linear(math.div(color.green($color), 255));
$b: component-to-linear(math.div(color.blue($color), 255));
$l: 0.4122214708 * $r + 0.5363325363 * $g + 0.0514459929 * $b;
$m: 0.2119034982 * $r + 0.6806995451 * $g + 0.1073969566 * $b;
$s: 0.0883024619 * $r + 0.2817188376 * $g + 0.6299787005 * $b;
$l_: math.pow($l, math.div(1, 3));
$m_: math.pow($m, math.div(1, 3));
$s_: math.pow($s, math.div(1, 3));
@return (
"L": 0.2104542553 * $l_ + 0.793617785 * $m_ - 0.0040720468 * $s_,
"a": 1.9779984951 * $l_ - 2.428592205 * $m_ + 0.4505937099 * $s_,
"b": 0.0259040371 * $l_ + 0.7827717662 * $m_ - 0.808675766 * $s_
);
}
@function oklab_to_rgb($oklab) {
$L: map.get($oklab, "L");
$a: map.get($oklab, "a");
$b: map.get($oklab, "b");
$l_: $L + 0.3963377774 * $a + 0.2158037573 * $b;
$m_: $L - 0.1055613458 * $a - 0.0638541728 * $b;
$s_: $L - 0.0894841775 * $a - 1.291485548 * $b;
$l: math.pow($l_, 3);
$m: math.pow($m_, 3);
$s: math.pow($s_, 3);
$linear_r: +4.0767416621 * $l - 3.3077115913 * $m + 0.2309699292 * $s;
$linear_g: -1.2684380046 * $l + 2.6097574011 * $m - 0.3413193965 * $s;
$linear_b: -0.0041960863 * $l - 0.7034186147 * $m + 1.707614701 * $s;
$r: component-to-nonninear($linear_r);
$g: component-to-nonninear($linear_g);
$b: component-to-nonninear($linear_b);
@return rgb($r * 255, $g * 255, $b * 255);
}

View File

@ -7,6 +7,8 @@
// --------------------------------------------------
@use "sass:math";
@use "sass:map";
@use "./oklab" as oklab;
$small-width: 800px !default;
$medium-width: 995px !default;
@ -179,11 +181,42 @@ $z-layers: (
@if dc-color-brightness($adjusted-color) <
dc-color-brightness($comparison-color)
{
@return scale-color($adjusted-color, $lightness: $lightness);
@return oklab-scale-color($adjusted-color, $lightness: $lightness);
} @else {
@return scale-color($adjusted-color, $lightness: $darkness);
@return oklab-scale-color($adjusted-color, $lightness: $darkness);
}
}
/**
Scale a colour's luminance using the oklab color space. Eventually, this could be implemented
in the browser using css Relative Color Syntax. In native CSS, this function is equivelant to:
```css
// For lightness > 0
oklab(from var(--color) calc(L + ((1-L) * var(--lightness))) a b)
// For lightness < 0
oklab(from var(--color) calc(L + (L * var(--lightness)) a b)
```
**/
@function oklab-scale-color($color, $lightness: 0) {
$current-oklab: oklab.rgb_to_oklab($color);
$current-lightness: map.get($current-oklab, "L");
$new-lightness: null;
@if ($lightness > 0) {
$new-lightness: $current-lightness +
((1 - $current-lightness) * math.div($lightness, 100%));
} @else {
$new-lightness: $current-lightness +
($current-lightness * math.div($lightness, 100%));
}
$transformed-oklab: map.set($current-oklab, "L", $new-lightness);
$transformed-rgb: oklab.oklab-to-rgb($transformed-oklab);
@return $transformed-rgb;
}
@function dark-light-choose($light-theme-result, $dark-theme-result) {
@if is-light-color-scheme() {
@return $light-theme-result;

View File

@ -76,6 +76,7 @@ module Stylesheet
target = nil
target_match =
long.match(/admin|desktop|mobile|publish|wizard|wcag|color_definitions/)
target_match ||= "color_definitions" if long.match(/color_transformations/)
target = target_match[0] if target_match&.length
{ basename: File.basename(long), target: target, plugin_name: plugin_name }

View File

@ -1,4 +1,4 @@
<section class="color-example">
<div class="color-bg {{@color}}"></div>
<div class="color-name">var(--{{@color}})</div>
<div class="color-name">{{@color}}</div>
</section>

View File

@ -3,8 +3,6 @@
<ColorExample @color="primary-very-low" />
<ColorExample @color="primary-low" />
<ColorExample @color="primary-low-mid" />
</section>
<section class="color-row">
<ColorExample @color="primary-medium" />
<ColorExample @color="primary-high" />
<ColorExample @color="primary" />
@ -19,8 +17,6 @@
<ColorExample @color="primary-300" />
<ColorExample @color="primary-400" />
<ColorExample @color="primary-500" />
</section>
<section class="color-row">
<ColorExample @color="primary-600" />
<ColorExample @color="primary-700" />
<ColorExample @color="primary-800" />
@ -55,8 +51,6 @@
<ColorExample @color="tertiary-300" />
<ColorExample @color="tertiary-400" />
<ColorExample @color="tertiary-500" />
</section>
<section class="color-row">
<ColorExample @color="tertiary-600" />
<ColorExample @color="tertiary-700" />
<ColorExample @color="tertiary-800" />

View File

@ -128,26 +128,27 @@
}
.color-row {
display: flex;
border: 1px solid black;
.color-example {
flex: 1;
display: flex;
flex-direction: column;
height: 120px;
margin: 0.5em 0.5em 0.5em 0;
border: 1px solid var(--primary-300);
// margin: 0.5em 0.5em 0.5em 0;
// border: 1px solid black;
.color-bg {
flex: 4;
border-bottom: 1px solid var(--primary-300);
// border-bottom: 1px solid var(--primary-300);
}
.color-name {
flex: 1;
display: flex;
align-items: center;
padding: 0.25em 0.5em;
font-weight: 700;
font-size: var(--font-down-1);
// padding: 0.25em 0.5em;
// font-weight: 700;
font-size: var(--font-down-2);
}
}
}