//// /// @group color /// /// This module provides functions that can be used as drop-in replacements for /// some of the HSL based functions included in Sass. /// /// The implementations use sRGB for input colors (including the whitepoint /// D65) and converts them to CIELAB by default, but CIELUV, HSL or YUV are /// also possible. /// /// CIELAB and CIELUV both try to be close to human perception, so they may /// give nicer results in many cases than simple RGB/HSL. /// /// HSLab and HSLuv are variants of CIELAB and CIELUV that scale the chroma /// instead of clipping. With CIELAB, you know that `lch(40, 50, 10deg, 'lab')` /// and `lch(70, 50, 90deg, 'lab')` have the same chroma (except when clipping /// is applied). With HSLab, you know that `lch(40, 50, 10deg, 'hslab')` always /// has half the chroma of `lch(40, 100, 10deg, 'hslab')`. //// @use "sass:color"; @use "sass:math"; /// @type string $planifolia-colorspace: 'lab' !default; @function _perc($x) { @return if(unit($x) == '%', math.div($x, 100%), $x); } @function _clip-needed($rgb) { @for $i from 1 through 3 { @if nth($rgb, $i) < 0 { @return true; } @else if nth($rgb, $i) > 255 { @return true; } } @return false; } @function _srgb-to-rgb($c) { $c: math.div($c, 255); @if $c <= 0.04045 { $c: math.div($c, 12.92); } @else { $c: math.pow(math.div($c + 0.055, 1.055), 2.4); } @return $c * 100; } @function _rgb-to-srgb($c) { $c: $c * 0.01; @if $c <= 0.0031308 { $c: $c * 12.92; } @else { $c: 1.055 * math.pow($c, math.div(1, 2.4)) - 0.055; } @return $c * 255; } @function _to-xyz($color) { $r: _srgb-to-rgb(red($color)); $g: _srgb-to-rgb(green($color)); $b: _srgb-to-rgb(blue($color)); $x: 0.4124 * $r + 0.3576 * $g + 0.1805 * $b; $y: 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; $z: 0.0193 * $r + 0.1192 * $g + 0.9505 * $b; @return ($x, $y, $z); } @function _from-xyz($xyz) { $r: 3.2406 * nth($xyz, 1) - 1.5372 * nth($xyz, 2) - 0.4986 * nth($xyz, 3); $g: -0.9689 * nth($xyz, 1) + 1.8758 * nth($xyz, 2) + 0.0415 * nth($xyz, 3); $b: 0.0557 * nth($xyz, 1) - 0.204 * nth($xyz, 2) + 1.057 * nth($xyz, 3); $r: _rgb-to-srgb($r); $g: _rgb-to-srgb($g); $b: _rgb-to-srgb($b); @return ($r, $g, $b); } @function _to-yuv($color) { $r: _srgb-to-rgb(red($color)); $g: _srgb-to-rgb(green($color)); $b: _srgb-to-rgb(blue($color)); $y: 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; $u: -0.09991 * $r + -0.33609 * $g + 0.436 * $b; $v: 0.615 * $r + -0.55861 * $g + -0.05639 * $b; @return ($y, $v, -$u); } @function _from-yuv($yuv) { $y: nth($yuv, 1); $v: nth($yuv, 2); $u: -1 * nth($yuv, 3); $r: _rgb-to-srgb($y + 1.28033 * $v); $g: _rgb-to-srgb($y + -0.21482 * $u + -0.38059 * $v); $b: _rgb-to-srgb($y + 2.12798 * $u); @return ($r, $g, $b); } @function _xyz-to-lab-f($t) { @if $t > math.div(216, 24389) { @return math.pow($t, math.div(1, 3)); } @else { @return math.div(841, 108) * $t + math.div(4, 29); } } @function _xyz-to-lab($xyz) { $white: (95.05, 100, 108.9); $x: _xyz-to-lab-f(math.div(nth($xyz, 1), nth($white, 1))); $y: _xyz-to-lab-f(math.div(nth($xyz, 2), nth($white, 2))); $z: _xyz-to-lab-f(math.div(nth($xyz, 3), nth($white, 3))); $l: 116 * $y - 16; $a: 500 * ($x - $y); $b: 200 * ($y - $z); @return ($l, $a, $b); } @function _lab-to-xyz-f($t) { @if $t > math.div(6, 29) { @return math.pow($t, 3); } @else { @return math.div(108, 841) * ($t - math.div(4, 29)); } } @function _lab-to-xyz($lab) { $white: (95.05, 100, 108.9); $l: math.div(nth($lab, 1) + 16, 116); $x: nth($white, 1) * _lab-to-xyz-f($l + math.div(nth($lab, 2), 500)); $y: nth($white, 2) * _lab-to-xyz-f($l); $z: nth($white, 3) * _lab-to-xyz-f($l - math.div(nth($lab, 3), 200)); @return ($x, $y, $z); } @function _xyz-to-yuuvv($xyz) { $a: nth($xyz, 1) + 15 * nth($xyz, 2) + 3 * nth($xyz, 3); $uu: if($a == 0, 0, math.div(4 * nth($xyz, 1), $a)); $vv: if($a == 0, 0, math.div(9 * nth($xyz, 2), $a)); @return (nth($xyz, 2), $uu, $vv); } @function _yuuvv-to-xyz($yuuvv) { $y: nth($yuuvv, 1); $uu: nth($yuuvv, 2); $vv: nth($yuuvv, 3); $x: if($vv == 0, 0, math.div($y * (9 * $uu), 4 * $vv)); $z: if($vv == 0, 0, math.div($y * (12 - 3 * $uu - 20 * $vv), 4 * $vv)); @return ($x, $y, $z); } @function _xyz-to-luv($xyz) { $white: _xyz-to-yuuvv((95.05, 100, 108.9)); $yuuvv: _xyz-to-yuuvv($xyz); $y: math.div(nth($yuuvv, 1), nth($white, 1)); $l: if($y > math.div(216, 24389), 116 * math.pow($y, math.div(1, 3)) - 16, math.div(24389, 27) * $y); $u: 13 * $l * (nth($yuuvv, 2) - nth($white, 2)); $v: 13 * $l * (nth($yuuvv, 3) - nth($white, 3)); @return ($l, $u, $v); } @function _luv-to-xyz($luv) { $white: _xyz-to-yuuvv((95.05, 100, 108.9)); $uu: if(nth($luv, 1) == 0, 0, math.div(nth($luv, 2), 13 * nth($luv, 1)) + nth($white, 2)); $vv: if(nth($luv, 1) == 0, 0, math.div(nth($luv, 3), 13 * nth($luv, 1)) + nth($white, 3)); $y: nth($white, 1); @if nth($luv, 1) > 8 { $y: $y * math.pow(math.div(nth($luv, 1) + 16, 116), 3); } @else { $y: math.div($y * nth($luv, 1) * 27, 24389); } @return _yuuvv-to-xyz(($y, $uu, $vv)); } @function _lab-to-lch($lab) { $l: nth($lab, 1); $c: math.sqrt(nth($lab, 2) * nth($lab, 2) + nth($lab, 3) * nth($lab, 3)); $h: 0deg; @if abs(nth($lab, 2)) > 0.0001 or abs(nth($lab, 3)) > 0.0001 { $h: math.atan2(nth($lab, 3), nth($lab, 2)); } @return ($l, $c, $h); } @function _lch-to-lab($lch) { $l: nth($lch, 1); $a: math.cos(nth($lch, 3)) * nth($lch, 2); $b: math.sin(nth($lch, 3)) * nth($lch, 2); @return ($l, $a, $b); } @function _max-chroma($lightness, $hue, $colorspace) { $c-min: 0; $c-max: 200; $c-tmp: ($c-min + $c-max) * 0.5; @while $c-max - $c-min > 1 { $rgb: _lch-unclipped($lightness, $c-tmp, $hue, $colorspace); @if _clip-needed($rgb) { $c-max: $c-tmp; } @else { $c-min: $c-tmp; } $c-tmp: ($c-min + $c-max) * 0.5; } @return $c-tmp; } @function _to-lch($color, $colorspace: $planifolia-colorspace) { @if $colorspace == 'lab' { $xyz: _to-xyz($color); $lab: _xyz-to-lab($xyz); @return _lab-to-lch($lab); } @else if $colorspace == 'hslab' { $lch: _to-lch($color, 'lab'); $max: _max-chroma(nth($lch, 1), nth($lch, 3), 'lab'); @return (nth($lch, 1), math.div(nth($lch, 2), $max) * 100, nth($lch, 3)); } @else if $colorspace == 'luv' { $xyz: _to-xyz($color); $luv: _xyz-to-luv($xyz); @return _lab-to-lch($luv); } @else if $colorspace == 'hsluv' { $lch: _to-lch($color, 'luv'); $max: _max-chroma(nth($lch, 1), nth($lch, 3), 'luv'); @return (nth($lch, 1), math.div(nth($lch, 2), $max) * 100, nth($lch, 3)); } @else if $colorspace == 'hsl' { @return (math.div(color.lightness($color), 1%), math.div(color.saturation($color), 1%), color.hue($color)); } @else if $colorspace == 'yuv' { $yuv: _to-yuv($color); @return _lab-to-lch($yuv); } @else { @error 'unknown colorspace: #{$colorspace}'; } } @function _lch-unclipped($lightness, $chroma, $hue, $colorspace) { @if $colorspace == 'lab' { $lab: _lch-to-lab(($lightness, $chroma, $hue)); $xyz: _lab-to-xyz($lab); @return _from-xyz($xyz); } @else if $colorspace == 'hslab' { $max: _max-chroma($lightness, $hue, 'lab'); @return _lch-unclipped($lightness, $chroma * $max * 0.01, $hue, 'lab'); } @else if $colorspace == 'luv' { $luv: _lch-to-lab(($lightness, $chroma, $hue)); $xyz: _luv-to-xyz($luv); @return _from-xyz($xyz); } @else if $colorspace == 'hsluv' { $max: _max-chroma($lightness, $hue, 'luv'); @return _lch-unclipped($lightness, $chroma * $max * 0.01, $hue, 'luv'); } @else if $colorspace == 'hsl' { $color: hsl(math.div($hue, 1deg) * 1deg, $chroma * 1%, $lightness * 1%); @return (red($color), green($color), blue($color)); } @else if $colorspace == 'yuv' { $yuv: _lch-to-lab(($lightness, $chroma, $hue)); @return _from-yuv($yuv); } @else { @error 'unknown colorspace: #{$colorspace}'; } } /// Create a color from lightness, chroma, and hue values. /// /// Note that the chroma is reduced if the result would otherwise be outside /// of the sRGB colorspace. /// /// @param {number} $lightness 0 .. 100 /// @param {number} $chroma 0 .. 100 (some colorspaces may go beyond) /// @param {angle} $hue /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function lch($lightness, $chroma, $hue, $colorspace: $planifolia-colorspace) { $hue: 0deg + $hue; $rgb: _lch-unclipped($lightness, $chroma, $hue, $colorspace); @if _clip-needed($rgb) { $c-min: 0; $c-max: $chroma; $c-tmp: ($c-min + $c-max) * 0.5; @while $c-max - $c-min > 0.01 { $rgb: _lch-unclipped($lightness, $c-tmp, $hue, $colorspace); @if _clip-needed($rgb) { $c-max: $c-tmp; } @else { $c-min: $c-tmp; } $c-tmp: ($c-min + $c-max) * 0.5; } } @return rgb(nth($rgb, 1), nth($rgb, 2), nth($rgb, 3)); } /// Create a color from lightness, chroma, hue, and alpha values. /// @param {number} $lightness /// @param {number} $chroma /// @param {angle} $hue /// @param {number} $alpha /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function lcha($lightness, $chroma, $hue, $alpha, $colorspace: $planifolia-colorspace) { @return rgba(lch($lightness, $chroma, $hue, $colorspace), $alpha); } /// Get the lightness component of a color. /// @param {color} $color /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {number} @function lightness($color, $colorspace: $planifolia-colorspace) { @return nth(_to-lch($color, $colorspace), 1); } /// Get the chroma component of a color. /// @param {color} $color /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {number} @function chroma($color, $colorspace: $planifolia-colorspace) { @return nth(_to-lch($color, $colorspace), 2); } /// Get the hue component of a color. /// @param {color} $color /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {angle} @function hue($color, $colorspace: $planifolia-colorspace) { @return nth(_to-lch($color, $colorspace), 3); } /// Increase or decrease one or more components of a color. /// @param {color} $color /// @param {number} $lightness [0] /// @param {number} $chroma [0] /// @param {angle} $hue [0] /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function adjust($color, $lightness: 0, $chroma: 0, $hue: 0, $colorspace: $planifolia-colorspace) { $lch: _to-lch($color, $colorspace); $l: nth($lch, 1) + $lightness; $c: nth($lch, 2) + $chroma; $h: nth($lch, 3) + $hue; @return lcha($l, $c, $h, alpha($color), $colorspace); } /// Change one or more properties of a color. /// @param {color} $color /// @param {number} $lightness [null] /// @param {number} $chroma [null] /// @param {angle} $hue [null] /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function change($color, $lightness: null, $chroma: null, $hue: null, $colorspace: $planifolia-colorspace) { $lch: _to-lch($color, $colorspace); $l: if($lightness == null, nth($lch, 1), $lightness); $c: if($chroma == null, nth($lch, 2), $chroma); $h: if($hue == null, nth($lch, 3), $hue); @return lcha($l, $c, $h, alpha($color), $colorspace); } /// @param {color} $color /// @param {number} $angle /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function adjust-hue($color, $angle, $colorspace: $planifolia-colorspace) { @return adjust($color, $hue: $angle, $colorspace: $colorspace); } /// @param {color} $color /// @param {number} $amount /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function lighten($color, $amount, $colorspace: $planifolia-colorspace) { @return adjust($color, $lightness: $amount, $colorspace: $colorspace); } /// @param {color} $color /// @param {number} $amount /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function darken($color, $amount, $colorspace: $planifolia-colorspace) { @return adjust($color, $lightness: -$amount, $colorspace: $colorspace); } /// @param {color} $color /// @param {number} $weight /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function tint($color, $weight, $colorspace: $planifolia-colorspace) { @return mix(white, $color, $weight, $colorspace); } /// @param {color} $color /// @param {number} $weight /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function shade($color, $weight, $colorspace: $planifolia-colorspace) { @return mix(black, $color, $weight, $colorspace); } /// @param {color} $color /// @param {number} $amount /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function saturate($color, $amount, $colorspace: $planifolia-colorspace) { @return adjust($color, $chroma: $amount, $colorspace: $colorspace); } /// @param {color} $color /// @param {number} $amount /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function desaturate($color, $amount, $colorspace: $planifolia-colorspace) { @return adjust($color, $chroma: -$amount, $colorspace: $colorspace); } /// @param {color} $color /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function complement($color, $colorspace: $planifolia-colorspace) { @return adjust-hue($color, math.$pi * 1rad, $colorspace); } /// @param {color} $color /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function grayscale($color, $colorspace: $planifolia-colorspace) { @return change($color, $chroma: 0, $colorspace: $colorspace); } /// Get the euclidean distance between two colors. /// @param {color} $color1 /// @param {color} $color2 /// @return {number} /// @require _to-lch @function distance($color1, $color2) { $lab1: _xyz-to-lab(_to-xyz($color1)); $lab2: _xyz-to-lab(_to-xyz($color2)); $x1: nth($lab1, 1) - nth($lab2, 1); $x2: nth($lab1, 2) - nth($lab2, 2); $x3: nth($lab1, 3) - nth($lab2, 3); @return math.sqrt($x1 * $x1 + $x2 * $x2 + $x3 * $x3); } @function _lch-mix($lch1, $lch2, $weight) { $w: _perc($weight); $l: nth($lch1, 1) * $w + nth($lch2, 1) * (1 - $w); $c: nth($lch1, 2) * $w + nth($lch2, 2) * (1 - $w); $w1: $w * nth($lch1, 2); $w2: (1 - $w) * nth($lch2, 2); @if ($w1 == 0 and $w2 == 0) { $w1: 0.5; $w2: 0.5; } $h1: nth($lch1, 3); $h2: nth($lch2, 3); @while abs($h2 - $h1) > 180deg { $h1: $h1 + if($h1 < $h2, 360deg, -360deg); } $h: math.div($h1 * $w1 + $h2 * $w2, $w1 + $w2); @return ($l, $c, $h); } /// @param {color} $color1 /// @param {color} $color2 /// @param {number} $weight [50%] /// @param {string} $colorspace ['lab'] one of 'lab', 'luv', 'hsl', 'yuv', 'hslab', 'hsluv' /// @return {color} @function mix($color1, $color2, $weight: 50%, $colorspace: $planifolia-colorspace) { $lch1: _to-lch($color1, $colorspace); $lch2: _to-lch($color2, $colorspace); $lch: _lch-mix($lch1, $lch2, $weight); @return lch(nth($lch, 1), nth($lch, 2), nth($lch, 3), $colorspace); }