From 67c68c9c603860b4887792e052a8dee1b4b24aac Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 4 Dec 2023 10:36:54 +0000 Subject: [PATCH] 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. --- .../foundation/color_transformations.scss | 56 ++++++++++----- .../stylesheets/common/foundation/oklab.scss | 68 +++++++++++++++++++ .../common/foundation/variables.scss | 37 +++++++++- lib/stylesheet/watcher.rb | 1 + .../discourse/components/color-example.hbs | 2 +- .../components/sections/atoms/03-colors.hbs | 6 -- .../assets/stylesheets/styleguide.scss | 13 ++-- 7 files changed, 151 insertions(+), 32 deletions(-) create mode 100644 app/assets/stylesheets/common/foundation/oklab.scss diff --git a/app/assets/stylesheets/common/foundation/color_transformations.scss b/app/assets/stylesheets/common/foundation/color_transformations.scss index fa2a1f05408..28d2ced3524 100644 --- a/app/assets/stylesheets/common/foundation/color_transformations.scss +++ b/app/assets/stylesheets/common/foundation/color_transformations.scss @@ -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; diff --git a/app/assets/stylesheets/common/foundation/oklab.scss b/app/assets/stylesheets/common/foundation/oklab.scss new file mode 100644 index 00000000000..562393a4d98 --- /dev/null +++ b/app/assets/stylesheets/common/foundation/oklab.scss @@ -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); +} diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 5cf234edd63..b7a20906beb 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -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; diff --git a/lib/stylesheet/watcher.rb b/lib/stylesheet/watcher.rb index 9740758d076..6384146826f 100644 --- a/lib/stylesheet/watcher.rb +++ b/lib/stylesheet/watcher.rb @@ -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 } diff --git a/plugins/styleguide/assets/javascripts/discourse/components/color-example.hbs b/plugins/styleguide/assets/javascripts/discourse/components/color-example.hbs index b946fa36329..33510b55cb4 100644 --- a/plugins/styleguide/assets/javascripts/discourse/components/color-example.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/components/color-example.hbs @@ -1,4 +1,4 @@
-
var(--{{@color}})
+
{{@color}}
\ No newline at end of file diff --git a/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/03-colors.hbs b/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/03-colors.hbs index e4b06cec732..7185f4a714c 100644 --- a/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/03-colors.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/03-colors.hbs @@ -3,8 +3,6 @@ - -
@@ -19,8 +17,6 @@ -
-
@@ -55,8 +51,6 @@ -
-
diff --git a/plugins/styleguide/assets/stylesheets/styleguide.scss b/plugins/styleguide/assets/stylesheets/styleguide.scss index cc799b631c1..4bc4fee9a3e 100644 --- a/plugins/styleguide/assets/stylesheets/styleguide.scss +++ b/plugins/styleguide/assets/stylesheets/styleguide.scss @@ -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); } } }