//// /// Smoother background gradients. /// /// The default background gradients produced by any browser have a quite harsh transition between /// colors. This is especially apparent if you, for example, use a strong fade-out gradient to make /// text in front of a background more readable. /// /// The function in this file generates smoother gradients by using easing functions of the user's /// choice. /// It's essentially a more flexible alternative to the PostCSS plugin "PostCSS Easing Gradients": /// https://github.com/larsenwork/postcss-easing-gradients /// /// @group Background gradients /// /// @access public //// @use 'sass:math'; @use 'sass:meta'; @use './functions'; @use './easing'; /// /// Number of intermediate color stops generated to achieve easing. /// A higher value results in better quality, but also much more generated code. /// /// @type number /// $easing-gradient-steps: 10 !default; /// /// Generate a new easing background gradient. /// This function is intended to be as similar as possible to the newly proposed syntax for /// linear-gradient and radial-gradient which includes easing hints. /// /// @param {string} $type - Either 'linear' or 'radial', which means the gradient will be either a linear-gradient or a radial-gradient. /// @param {string} $dir - The direction of the gradient. Depending on $type, this value must be a valid direction for linear-gradient or radial-gradient. /// @param {color | list} $stop - A color stop as used for linear-gradient or radial-gradient. /// @param {arglist} $stops - More color stops as used for linear-gradient or radial-gradient. Between two color stops, you may also define an easing hint such as `ease-in-out`, `cubic-bezier 0.42 0 0.58 1`, `steps 3 jump-end`, and so on. /// /// @return {string} A linear-gradient or radial-gradient with an alternative transitioning behavior. /// /// @throw If $type is invalid /// /// @link https://github.com/w3c/csswg-drafts/issues/1332 The new CSSWG proposal /// /// @example scss - A smoother linear gradient /// .background { /// background-image: easing-gradient( /// linear, /// to top, /// #000, /// in-out-sine, /// transparent /// ); /// } /// /// // Generates: /// /// .background { /// background-image: linear-gradient( /// to top, /// black 0%, /// rgba(0, 0, 0, 0.975528) 10%, /// rgba(0, 0, 0, 0.904508) 20%, /// rgba(0, 0, 0, 0.793893) 30%, /// rgba(0, 0, 0, 0.654508) 40%, /// rgba(0, 0, 0, 0.5) 50%, /// rgba(0, 0, 0, 0.345492) 60%, /// rgba(0, 0, 0, 0.206107) 70%, /// rgba(0, 0, 0, 0.0954915) 80%, /// rgba(0, 0, 0, 0.0244717) 90%, /// rgba(0, 0, 0, 3.78257e-11) 100% /// ); /// } /// /// @example scss - A smoother radial gradient /// .background { /// background-image: easing-gradient( /// radial, /// 50em 16em at 0 0, /// #000, /// in-out-sine, /// transparent /// ); /// } /// /// // Generates: /// /// .background { /// background-image: radial-gradient( /// 50em 16em at 0 0, /// black 0%, /// rgba(0, 0, 0, 0.975528) 10%, /// rgba(0, 0, 0, 0.904508) 20%, /// rgba(0, 0, 0, 0.793893) 30%, /// rgba(0, 0, 0, 0.654508) 40%, /// rgba(0, 0, 0, 0.5) 50%, /// rgba(0, 0, 0, 0.345492) 60%, /// rgba(0, 0, 0, 0.206107) 70%, /// rgba(0, 0, 0, 0.0954915) 80%, /// rgba(0, 0, 0, 0.0244717) 90%, /// rgba(0, 0, 0, 3.78257e-11) 100% /// ); /// } /// /// @example scss - A smoother linear gradient with complex color positions /// .background { /// background-image: easing-gradient( /// linear, /// to top, /// #000 20%, /// in-out-sine, /// transparent calc(20% + 25em) /// ); /// } /// /// // Generates: /// /// .background { /// background-image: linear-gradient( /// to top, /// black 20%, /// rgba(0, 0, 0, 0.975528) calc(20% + (20% + 25em - 20%) * 0.1), /// rgba(0, 0, 0, 0.904508) calc(20% + (20% + 25em - 20%) * 0.2), /// rgba(0, 0, 0, 0.793893) calc(20% + (20% + 25em - 20%) * 0.3), /// rgba(0, 0, 0, 0.654508) calc(20% + (20% + 25em - 20%) * 0.4), /// rgba(0, 0, 0, 0.5) calc(20% + (20% + 25em - 20%) * 0.5), /// rgba(0, 0, 0, 0.345492) calc(20% + (20% + 25em - 20%) * 0.6), /// rgba(0, 0, 0, 0.206107) calc(20% + (20% + 25em - 20%) * 0.7), /// rgba(0, 0, 0, 0.0954915) calc(20% + (20% + 25em - 20%) * 0.8), /// rgba(0, 0, 0, 0.0244717) calc(20% + (20% + 25em - 20%) * 0.9), /// transparent calc(20% + 25em)) /// ); /// } /// @function easing-gradient($type, $dir, $stop, $stops...) { $pos-template: null; $stops: functions.list-prepend($stops, $stop); $last-positioned-stop: 1; $generated-stops: (); // // Generate gradient // @for $i from 1 through length($stops) { $stop: nth($stops, $i); @if $i == 1 { @if not easing-gradient-is-color-stop($stop) { @error 'The first color stop argument must be a color stop.'; } @if type-of($stop) == color { // // The first color stop is unpositioned. The default position for the first // color stop is 0, which is explicitly added for easier calculations. // $stop: $stop 0; $stops: set-nth($stops, $i, $stop); } $generated-stops: append($generated-stops, functions.str-implode($stop, ' ')); } @else if easing-gradient-is-positioned-color-stop($stop) or ($i == length($stops)) { @if not easing-gradient-is-color-stop($stop) { @error 'The last color stop argument must be a color stop.'; } // // Either the current stops list item is a positioned color stop, or the end of // the stops list has been reached. // @if (type-of($stop) == color) and ($i == length($stops)) { // // The current stop is an unpositioned color stop, which means this is the end // of the stops list. The default position for the last color stop is 100%, which // is explicitly added for easier calculations. // $stop: $stop 100%; $stops: set-nth($stops, $i, $stop); } // // Now the current color stop is guaranteed to be a positioned color stop. // @if $i > $last-positioned-stop + 1 { // // There is at least one stops list item (unpositioned color stop or easing function) // between the last positioned color stop and the current stops list item. Interpolate // the positions of all stops list items that are color stops. // $interpolated-stops: easing-gradient-interpolate-stop-positions( nth($stops, $last-positioned-stop), functions.list-slice($stops, $last-positioned-stop + 1, $i - 1), $stop ); $new-stops: join( functions.list-slice($stops, 1, $last-positioned-stop), $interpolated-stops ); $new-stops: join( $new-stops, functions.list-slice($stops, $i) ); $stops: $new-stops; } // // Now all color stops between this one and the last positioned one have // interpolated positions. // Next task is to perform an easing transition between all color stops that // have an easing function specified. The rest can be left alone since the // browser will automatically apply a linear transition between them. // $j: $last-positioned-stop + 1; @while $j <= $i { $easing: null; $prev-stop: nth($stops, $j - 1); $next-stop: nth($stops, $j); @if not easing-gradient-is-color-stop($next-stop) { $j: $j + 1; $easing: $next-stop; $next-stop: nth($stops, $j); @if not easing-gradient-is-color-stop($next-stop) { @error 'There can be at most one interpolation hint between to color stops.'; } } @if $easing != null { @if type-of($easing) == number { @error 'Midpoint shifts are not supported.'; } $easing-func: null; $easing-args: (); @if type-of($easing) == list { $easing-args: functions.list-slice($easing, 2); $easing: nth($easing, 1); } $generated-stops: join( $generated-stops, easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args) ); } @else { $generated-stops: append($generated-stops, functions.str-implode($next-stop, ' ')); } $j: $j + 1; } $last-positioned-stop: $i; } } @if $type == 'linear' { @return linear-gradient($dir, unquote(functions.str-implode($generated-stops, ', '))); } @else if $type == 'radial' { @return radial-gradient($dir, unquote(functions.str-implode($generated-stops, ', '))); } @else { @error 'Invalid gradient type: #{inspect($type)}.'; } } /// /// Alias for easing-gradient('linear',...) /// /// @see {function} easing-gradient /// @function easing-linear-gradient($dir, $stop, $stops...) { @return easing-gradient('linear', $dir, $stop, $stops...); } /// /// Alias for easing-gradient('radial',...) /// /// @see {function} easing-gradient /// @function easing-radial-gradient($dir, $stop, $stops...) { @return easing-gradient('radial', $dir, $stop, $stops...); } /// /// Generate a smooth transition from one color stop to another using the provided easing function. /// /// @access private /// @function easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args: ()) { @if $easing == 'steps' { $steps: null; $jump: null; @if length($easing-args) > 1 { $steps: nth($easing-args, 1); $jump: nth($easing-args, 2); } @else { $steps: nth($easing-args, 1); $jump: jump-end; } @return easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump); } @else { $easing-func: get-function($easing, $module: easing); @return easing-gradient-bezier-stops($prev-stop, $next-stop, $easing-func, $easing-args); } } /// /// Generate a smooth transition from one color stop to another using the provided cubic-bezier function. /// /// @access private /// @function easing-gradient-bezier-stops($prev-stop, $next-stop, $easing-func, $easing-args: ()) { $prev-stop-color: nth($prev-stop, 1); $prev-stop-pos: nth($prev-stop, 2); $next-stop-color: nth($next-stop, 1); $next-stop-pos: nth($next-stop, 2); $stops: (); @if ((type-of($prev-stop-pos) == number) and (type-of($next-stop-pos) == number) and (unit($prev-stop-pos) == unit($next-stop-pos))) or ($prev-stop-pos == 0) or ($next-stop-pos == 0) { // // The transition color stop positions can be statically calculated. // $distance: $next-stop-pos - $prev-stop-pos; @for $i from 1 through $easing-gradient-steps { $perc: math.div($i, $easing-gradient-steps); $color: null; $pos: $prev-stop-pos + $perc * $distance; @if $perc == 1 { $color: $next-stop-color; } @else { $color: mix($next-stop-color, $prev-stop-color, call($easing-func, append($easing-args, $perc)...) * 100%); } $stops: append($stops, $color + ' ' + $pos); } } @else { // // The transition color stop positions have to be dynamically calculated with the calc() function. // @if type-of($prev-stop-pos) != number { // must be calc() @if type-of($prev-stop-pos) != calculation { @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; } $prev-stop-pos: meta.calc-args($prev-stop-pos); } @if type-of($next-stop-pos) != number { // must be calc() @if type-of($next-stop-pos) != calculation { @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; } $next-stop-pos: meta.calc-args($next-stop-pos); } @for $i from 1 through $easing-gradient-steps { $perc: math.div($i, $easing-gradient-steps); $color: null; $pos: null; @if $perc == 1 { $color: $next-stop-color; $pos: calc(#{$next-stop-pos}); } @else { $color: mix($next-stop-color, $prev-stop-color, call($easing-func, append($easing-args, $perc)...) * 100%); $pos: calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$perc}); } $stops: append($stops, $color + ' ' + $pos); } } @return $stops; } /// /// Generate a step transition from one color stop to another. /// /// @access private /// @function easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump: jump-end) { $prev-stop-color: nth($prev-stop, 1); $prev-stop-pos: nth($prev-stop, 2); $next-stop-color: nth($next-stop, 1); $next-stop-pos: nth($next-stop, 2); $stops: (); @if ((type-of($prev-stop-pos) == number) and (type-of($next-stop-pos) == number) and (unit($prev-stop-pos) == unit($next-stop-pos))) or ($prev-stop-pos == 0) or ($next-stop-pos == 0) { // // The transition color stop positions can be statically calculated. // $distance: $next-stop-pos - $prev-stop-pos; @for $i from 1 through $steps { $x1: math.div($i - 1, $steps); $x2: math.div($i, $steps); $y: null; @if $jump == jump-start { $y: math.div($i, $steps); } @else if $jump == jump-end { $y: math.div($i - 1, $steps); } @else if $jump == jump-both { $y: math.div($i, $steps + 1); } @else if $jump == jump-none { $y: math.div($i - 1, $steps - 1); } @else { @error 'Invalid $jump: #{inspect($jump)}'; } $color: null; $pos1: if($x1 == 0, $prev-stop-pos, $prev-stop-pos + $x1 * $distance); $pos2: if($x2 == 1, $next-stop-pos, $prev-stop-pos + $x2 * $distance); @if $y == 0 { $color: $prev-stop-color; } @else if $y == 1 { $color: $next-stop-color; } @else { $color: mix($next-stop-color, $prev-stop-color, $y * 100%); } $stops: append($stops, $color + ' ' + $pos1); $stops: append($stops, $color + ' ' + $pos2); } } @else { // // The transition color stop positions have to be dynamically calculated with the calc() function. // @if type-of($prev-stop-pos) != number { // must be calc() @if type-of($prev-stop-pos) != calculation { @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; } $prev-stop-pos: meta.calc-args($prev-stop-pos); } @if type-of($next-stop-pos) != number { // must be calc() @if type-of($next-stop-pos) != calculation { @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; } $next-stop-pos: meta.calc-args($next-stop-pos); } @for $i from 1 through $steps { $x1: math.div($i - 1, $steps); $x2: math.div($i, $steps); $y: null; @if $jump == jump-start { $y: math.div($i, $steps); } @else if $jump == jump-end { $y: math.div($i - 1, $steps); } @else if $jump == jump-both { $y: math.div($i, $steps + 1); } @else if $jump == jump-none { $y: math.div($i - 1, $steps - 1); } @else { @error 'Invalid $jump: #{inspect($jump)}'; } $color: null; $pos1: if($x1 == 0, $prev-stop-pos, calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$x1})); $pos2: if($x2 == 1, $next-stop-pos, calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$x2})); @if $y == 0 { $color: $prev-stop-color; } @else if $y == 1 { $color: $next-stop-color; } @else { $color: mix($next-stop-color, $prev-stop-color, $y * 100%); } $stops: append($stops, $color + ' ' + $pos1); $stops: append($stops, $color + ' ' + $pos2); } } @return $stops; } /// /// Interpolate the positions of multiple color stops between two color stops whose positions are set. /// /// @access private /// @function easing-gradient-interpolate-stop-positions($prev-stop, $stops, $next-stop) { $prev-stop-pos: nth($prev-stop, 2); $next-stop-pos: nth($next-stop, 2); $stops-num: 0; @for $i from 1 through length($stops) { $stop: nth($stops, $i); @if easing-gradient-is-color-stop($stop) { $stops-num: $stops-num + 1; } } $i: 1; $cur-stop-num: 1; @if ((type-of($prev-stop-pos) == number) and (type-of($next-stop-pos) == number) and (unit($prev-stop-pos) == unit($next-stop-pos))) or ($prev-stop-pos == 0) or ($next-stop-pos == 0) { // // The color stop positions can be statically calculated. // $distance: $next-stop-pos - $prev-stop-pos; @for $i from 1 through length($stops) { $stop: nth($stops, $i); @if easing-gradient-is-color-stop($stop) { $pos: $prev-stop-pos + math.div($distance, $stops-num + 1) * $cur-stop-num; $stops: set-nth($stops, $i, $stop $pos); $cur-stop-num: $cur-stop-num + 1; } } } @else { // // The color stop positions have to be dynamically calculated with the calc() function. // @if type-of($prev-stop-pos) != number { // must be calc() @if type-of($prev-stop-pos) != calculation { @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; } $prev-stop-pos: meta.calc-args($prev-stop-pos); } @if type-of($next-stop-pos) != number { // must be calc() @if type-of($next-stop-pos) != calculation { @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; } $next-stop-pos: meta.calc-args($next-stop-pos); } @for $i from 1 through length($stops) { $stop: nth($stops, $i); @if easing-gradient-is-color-stop($stop) { $perc: math.div($cur-stop-num, $stops-num + 1); $pos: calc(#{$prev-stop-pos} + (#{$next-stop-pos} - #{$prev-stop-pos}) * #{$perc}); $stops: set-nth($stops, $i, $stop $pos); $cur-stop-num: $cur-stop-num + 1; } } } @return $stops; } /// /// Check if the input is a valid color stop. /// /// @access private /// @function easing-gradient-is-color-stop($input) { @return (type-of($input) == color) or easing-gradient-is-positioned-color-stop($input); } /// /// Check if the input is a valid positioned color stop. /// /// @access private /// @function easing-gradient-is-positioned-color-stop($input) { @return (type-of($input) == list) and (type-of(nth($input, 1)) == color); }