From d07f664450ddaaebb44127a4bd057763d13d3f82 Mon Sep 17 00:00:00 2001 From: Feuerfuchs Date: Sun, 1 Nov 2020 20:55:14 +0100 Subject: Init --- src/_gradients.scss | 600 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 600 insertions(+) create mode 100644 src/_gradients.scss (limited to 'src/_gradients.scss') diff --git a/src/_gradients.scss b/src/_gradients.scss new file mode 100644 index 0000000..7c52d63 --- /dev/null +++ b/src/_gradients.scss @@ -0,0 +1,600 @@ +//// +/// 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 +//// + +/// +/// Number of intermediate color stops generated to achieve easing. +/// A higher value results in better quality, but also much more generated code. +/// +/// @type number +/// +$iro-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: iro-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: iro-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: iro-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 iro-easing-gradient($type, $dir, $stop, $stops...) { + $pos-template: null; + $stops: iro-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 iro-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, iro-str-implode($stop, ' ')); + } @else if iro-easing-gradient-is-positioned-color-stop($stop) or ($i == length($stops)) { + @if not iro-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: iro-easing-gradient-interpolate-stop-positions( + nth($stops, $last-positioned-stop), + iro-list-slice($stops, $last-positioned-stop + 1, $i - 1), + $stop + ); + + $new-stops: join( + iro-list-slice($stops, 1, $last-positioned-stop), + $interpolated-stops + ); + $new-stops: join( + $new-stops, + iro-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 iro-easing-gradient-is-color-stop($next-stop) { + $j: $j + 1; + + $easing: $next-stop; + $next-stop: nth($stops, $j); + + @if not iro-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: iro-list-slice($easing, 2); + $easing: nth($easing, 1); + } + + $generated-stops: join( + $generated-stops, + iro-easing-gradient-ease-stops($prev-stop, $next-stop, $easing, $easing-args) + ); + } @else { + $generated-stops: append($generated-stops, iro-str-implode($next-stop, ' ')); + } + + $j: $j + 1; + } + + $last-positioned-stop: $i; + } + } + + @if $type == 'linear' { + @return linear-gradient($dir, unquote(iro-str-implode($generated-stops, ', '))); + } @else if $type == 'radial' { + @return radial-gradient($dir, unquote(iro-str-implode($generated-stops, ', '))); + } @else { + @error 'Invalid gradient type: #{inspect($type)}.'; + } +} + +/// +/// Alias for iro-easing-gradient('linear',...) +/// +/// @see {function} iro-easing-gradient +/// +@function iro-easing-linear-gradient($dir, $stop, $stops...) { + @return iro-easing-gradient('linear', $dir, $stop, $stops...); +} + +/// +/// Alias for iro-easing-gradient('radial',...) +/// +/// @see {function} iro-easing-gradient +/// +@function iro-easing-radial-gradient($dir, $stop, $stops...) { + @return iro-easing-gradient('radial', $dir, $stop, $stops...); +} + +/// +/// Generate a smooth transition from one color stop to another using the provided easing function. +/// +/// @access private +/// +@function iro-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 iro-easing-gradient-steps-stops($prev-stop, $next-stop, $steps, $jump); + } @else { + $easing-func: null; + @if function-exists('iro-' + $easing) { + $easing-func: get-function('iro-' + $easing); + } @else { + $easing-func: get-function($easing); + } + + @return iro-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 iro-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 $iro-easing-gradient-steps { + $perc: $i / $iro-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) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; + } + + $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); + } + + @if type-of($next-stop-pos) != number { + // must be calc() + @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; + } + + $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); + } + + @for $i from 1 through $iro-easing-gradient-steps { + $perc: $i / $iro-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 iro-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: ($i - 1) / $steps; + $x2: $i / $steps; + $y: null; + + @if $jump == jump-start { + $y: $i / $steps; + } @else if $jump == jump-end { + $y: ($i - 1) / $steps; + } @else if $jump == jump-both { + $y: $i / ($steps + 1); + } @else if $jump == jump-none { + $y: ($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) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; + } + + $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); + } + + @if type-of($next-stop-pos) != number { + // must be calc() + @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; + } + + $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); + } + + @for $i from 1 through $steps { + $x1: ($i - 1) / $steps; + $x2: $i / $steps; + $y: null; + + @if $jump == jump-start { + $y: $i / $steps; + } @else if $jump == jump-end { + $y: ($i - 1) / $steps; + } @else if $jump == jump-both { + $y: $i / ($steps + 1); + } @else if $jump == jump-none { + $y: ($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 iro-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 iro-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 iro-easing-gradient-is-color-stop($stop) { + $pos: $prev-stop-pos + $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) != string) or (str-index($prev-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($prev-stop-pos)}'; + } + + $prev-stop-pos: str-slice($prev-stop-pos, 6, str-length($prev-stop-pos) - 1); + } + + @if type-of($next-stop-pos) != number { + // must be calc() + @if (type-of($next-stop-pos) != string) or (str-index($next-stop-pos, 'calc(') != 1) { + @error 'Invalid color stop position: #{inspect($next-stop-pos)}'; + } + + $next-stop-pos: str-slice($next-stop-pos, 6, str-length($next-stop-pos) - 1); + } + + @for $i from 1 through length($stops) { + $stop: nth($stops, $i); + @if iro-easing-gradient-is-color-stop($stop) { + $perc: $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 iro-easing-gradient-is-color-stop($input) { + @return (type-of($input) == color) or iro-easing-gradient-is-positioned-color-stop($input); +} + +/// +/// Check if the input is a valid positioned color stop. +/// +/// @access private +/// +@function iro-easing-gradient-is-positioned-color-stop($input) { + @return (type-of($input) == list) and (type-of(nth($input, 1)) == color); +} -- cgit v1.2.3-54-g00ecf