Skip to content

Instantly share code, notes, and snippets.

@dbc60
Created February 12, 2020 11:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dbc60/451f16c588b806967b706b45829e49dc to your computer and use it in GitHub Desktop.
Save dbc60/451f16c588b806967b706b45829e49dc to your computer and use it in GitHub Desktop.
// Greatest Common Divisor (gcd) in pure scss from
// https://gist.github.com/voxpelli/6304812
@function gcd($a, $b) {
// From: https://rosettacode.org/wiki/Greatest_common_divisor#JavaScript
@if ($b != 0) {
@return gcd($b, $a % $b);
} @else {
@return abs($a);
}
}
// ipow: raise $base to the power of $exponent, where $exponent is
// an integer.
@function ipow($base, $exponent) {
$value: $base;
$itr: abs($exponent);
@if $exponent == 0 {
$value: 1;
} @else {
@for $i from 2 through $itr {
$value: $value * $base;
}
@if $exponent < 0 {
// invert for negative exponents
$value: 1 / $value;
}
}
@return $value;
}
// The base of the natural logarithm, e, approximated to a more than sufficient
// number of decimal places.
$base_ln: 2.7182818284590452353602874713526624977572470937;
// The Taylor series expansion to calculate the natural logarithm of $x. Note
// that $x MUST be in the range -1 < $x < 1.
@function series_ln($x, $epsilon: 0.000001) {
$i: 2;
$x1: $x;
$xpow: $x * $x;
$x2: $x1 - $xpow/$i;
$doAdd: 1;
@while abs($x1 - $x2) > $epsilon {
$i: $i + 1;
$x1: $x2;
$xpow: $xpow * $x;
@if $doAdd == 1 {
$x2: $x1 + $xpow/$i;
$doAdd: 0;
} @else {
$x2: $x1 - $xpow/$i;
$doAdd: 1;
}
}
@return $x2;
}
/*
Taylor series expansion to raise $base_ln to the power of $exponent
Note that: x^n = pow(x,n) = exp(ln(x)*n), where 'exp(x)' raises e (the base
of the natural logarithm) to the power of x, and 'ln(x)' is the natural
logarithm. The Taylor series expansions for exp(x) and ln(x) are:
exp(x) = 1 + x + {x^2}/{2!} + {x^3}/{3!} + ... for all x
ln(1-x) = - {x} - {x^2}/2 - {x^3}/3 ... |x| < 1
ln(1+x) = x - {x^2}/2 + {x^3}/3 - {x^4}/4 + ... for |x| < 1
If we want to compute exp(z), where z = n + f, and 'n' is an integer and 'f'
lies between -0.5 and +0.5. We can write:
exp(z) = exp(n + f) = exp(n) * exp(f)
The first term can be calculated entirely with mulitplications if n is
positive, or with multiplications and one division if n is negative. The
second term can be computed using the power-series approximation. Since
the argument doesn't exceed 0.5 in magnitude, relatively few terms are
needed. For example, four significant digits can be obtained with no more
than six terms. For example:
(2.5)^(3.5) = (2.5)^3 * (2.5)^(0.5)
*/
@function series_exp($exponent, $epsilon: 0.000001) {
$x: abs($exponent);
$i: 1;
$fact: $i;
$x1: 1;
$xpow: $x;
$x2: $x1 + $xpow;
@while ($x2 - $x1) > $epsilon {
$i: $i + 1;
$fact: $fact * $i;
$x1: $x2;
$xpow: $xpow * $x;
$x2: $x1 + $xpow/$fact;
}
@if $x != $exponent {
// negative exponent, so invert the result
$x2: 1/$x2;
}
@return $x2;
}
// Raise $base_ln to the power of $exponent
@function exp($exponent, $epsilon: 0.000001) {
// Decompose $exponent into an integer part, $n, and a rational part, $r.
$n: floor(abs($exponent));
@if $exponent < 0 {$n: -$n;}
$result: ipow($base_ln, $n);
@if ($n != $exponent) {
// There's a rational part, $r. Raise $base_ln to $r using the Taylor
// expansion and update our result.
$r: $exponent - $n;
$result: $result * series_exp($r, $epsilon);
}
@return $result;
}
// ln(2) approximated to a sufficient number of decimal places.
$ln_2: 0.693147180559945309417232121458;
// Return the number that will raise 2 to the smallest power of 2
// greater than x. We expect $x > 0.
@function next_exponent_of_2($x) {
$exp: 1;
$x: floor($x / 2);
@while $x > 0 {
$exp: $exp + 1;
$x: floor($x / 2);
}
@return $exp;
}
/*
Reference:
Introduction to Computers, Structured Programming, and Applications,
Module A: Applications and Algorithms in Science and Engineering, by
C. William Gear.
Calculate the natural logarithm of $x, where $x > 0. We are using the
following definition of ln(x):
ln(1+x) = x - {x^2}/2 + {x^3}/3 - {x^4}/4 + ... for |x| < 1
Algorithm - Compute ln(z) with /argument reduction/:
- find an integer {n} such that 2^{n-1} <= z < 2^{n}
- ln(z) = ln(2^{-n} * z * 2^{n})
= ln(1 + (2^{-n} * z - 1)) + {n}*ln(2)
- Now the argument to be used in the series expansion of the ln()
function is {x = 2^{-n} * z - 1}, which is between -0.5 and 0.0.
When the argument of -0.5 is used in the expansion, only 10 terms
are needed to achieve four-digit precision, compared with nearly
60 terms when the argument is -0.9.
*/
@function ln($x, $epsilon: 0.000001) {
$n: next_exponent_of_2($x);
@return series_ln((ipow(2, -$n) * $x - 1), $epsilon) + $n * $ln_2;
}
// Raise x to the power of n: x^n = e^(n*ln(x))
// Algorithm:
// - Decompose the exponent into an integer part and a rational part
// - Calculate teh integer power with a loop (you can optimize it by
// decomposing in factors and reusing partial calculations)
// - Calculate the root with any algorithm you like (any iterative
// - approximation or bisection or Newton method could work)
// - Multiply the two results.
// - If the exponent was negative, apply the inverse (result = 1/result).
// Example:
// 2^(-3.5) = (2^3) * (2(1/2)))^(-1) = 1 / (2*2*2 * sqrt(2))
// = 0.08838834764831844055010554526311
@function pow($base, $exponent, $epsilon: 0.000001) {
// Note: if $x is negative, floor() will return the least negative integer
// not greater than $x, which is too negative for us, so we use the floor of
// the absolute value of the exponent.
$x: abs($exponent);
$n: floor($x);
@if $exponent < 0 {
// Fix the sign of the integer part of the exponent
$n: -$n;
}
// Calculate the integer result
$result: ipow($base, $n);
@if $n != $exponent {
// There's a rational part. Use the Taylor series expansion
// to update the result
$r: $exponent - $n;
$result: $result * series_exp($r * ln($base, $epsilon), $epsilon);
}
@return $result;
}
/*
* Here is a version of color_luminance and contrast_ratio in pure SASS!
* From: https://gist.github.com/voxpelli/6304812.
*
* Pure SASS-adaption of Lea Verou's contrast-ratio javascript
* (https://github.com/LeaVerou/contrast-ratio). Can be useful when eg.
* generating colored buttons from a single supplied color as you can then
* check which out of a couple of text colors would give the best contrast.
*
* This script currently lacks the support for alpha-transparency that Lea
* supports in her script though. An exponent of 2.4 is used to calculate the
* luminance of a color.
*
* Normal usage: color: pick_best_color(#f00, (#fff, #ccc, #666));
*
* Bonus feature: Just want to get warned when the contrast becomes
* unacceptably low? Supply just that one color in the color-pick function:
* color:
*
* pick_best_color(#f00, #fff);
*/
@function color_luminance($color) {
// Adapted from: https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js
// Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
$rgba: red($color), green($color), blue($color);
$rgba2: ();
@for $i from 1 through 3 {
$rgb: nth($rgba, $i);
$rgb: $rgb / 255;
$rgb: if($rgb < .03928, $rgb / 12.92, pow(($rgb + .055) / 1.055, 2.4));
$rgba2: append($rgba2, $rgb);
}
@return (.2126 * nth($rgba2, 1)) + (.7152 * nth($rgba2, 2)) + (0.0722 * nth($rgba2, 3));
}
@function contrast_ratio($color1, $color2) {
// Adapted from:
// https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js
// Formula: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
$luminance1: color_luminance($color1) + .05;
$luminance2: color_luminance($color2) + .05;
$ratio: $luminance1 / $luminance2;
@if $luminance2 > $luminance1 {
$ratio: 1 / $ratio;
}
$ratio: round($ratio * 10) / 10;
@return $ratio;
}
// Choose a color from "colors" with the best contrast ratio relative to base.
@function pick_best_color($base, $colors, $tolerance: 0) {
$ratio: contrast_ratio($base, nth($colors, 1));
$best: nth($colors, 1);
@for $i from 2 through length($colors) {
$current_contrast: contrast_ratio($base, nth($colors, $i));
@if ($current_contrast - $ratio > $tolerance) {
$ratio: contrast_ratio($base, nth($colors, $i));
$best: nth($colors, $i);
}
}
@if ($ratio < 4.5) {
@warn "Contrast ratio of #{$best} on #{$base} is just #{$ratio} which is less than 4.5:1, the minimum recommended by the Web Content Accessibility Guidelines.";
}
@return $best;
}
// Given a color, find a color whose contrast is at least $threshold. The
// starting color, $contrast, (a provided one, or $color if none is provided)
// is lightened and darkened by the percents in $list. The first to meet the
// threshold is returned. If none meet the threshold, then text-contrast
// returns white or black, which ever is farther in lightness from $color.
@function make_contrast($color, $contrast: $color) {
$threshold: 7.0; // 4.5 = WCAG AA,7= WCAG AAA
$list: 20 30 40 50 60 70 80 90 100;
@each $percent in $list {
$lighter: lighten($contrast, $percent);
$darker: darken($contrast, $percent);
$darker-ratio: contrast_ratio($color, $darker);
$lighter-ratio: contrast_ratio($color, $lighter);
@if ($lighter-ratio > $darker-ratio) {
@if ($lighter-ratio >= $threshold) {
@return $lighter;
}
}
@if ($darker-ratio > $lighter-ratio) {
@if ($darker-ratio >= $threshold) {
@return $darker;
}
}
}
@return if(lightness($color) < 51, #FFF, #000)
}
// Some test cases. Note: inspect($value) returns a string representation of
// $value, and quote($string) returns $string as a quoted string.
.squareroot2::after {
content: quote(inspect(pow(2, 0.5)));
}
.inverse_squareroot2::after {
content: quote(inspect(pow(2, -0.5)));
}
.ln_54::after {
content: quote(inspect(ln(54)));
}
.exp_ln_54::after {
content: quote(inspect(exp(ln(54))));
}
// Bootstrap functions
//
// Utility mixins and functions for evaluating source code across our variables,
// maps, and mixins.
// Ascending
// Used to evaluate Sass maps like our grid breakpoints.
@mixin _assert-ascending($map, $map-name) {
$prev-key: null;
$prev-num: null;
@each $key, $num in $map {
@if $prev-num == null or unit($num) == "%" {
// Do nothing
} @else if not comparable($prev-num, $num) {
@warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
} @else if $prev-num >= $num {
@warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
}
$prev-key: $key;
$prev-num: $num;
}
}
// Starts at zero
// Used to ensure the min-width of the lowest breakpoint starts at 0.
@mixin _assert-starts-at-zero($map, $map-name: "$grid-breakpoints") {
$values: map-values($map);
$first-value: nth($values, 1);
@if $first-value != 0 {
@warn "First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}.";
}
}
// Replace `$search` with `$replace` in `$string`
// Used on our SVG icon backgrounds for custom forms.
//
// @author Hugo Giraudel
// @param {String} $string - Initial string
// @param {String} $search - Substring to replace
// @param {String} $replace ('') - New value
// @return {String} - Updated string
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
// Color contrast
@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light) {
$r: red($color);
$g: green($color);
$b: blue($color);
$yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000;
@return if($yiq >= $yiq-contrasted-threshold, $dark, $light);
}
// Retrieve color Sass maps
@function color($key: "blue") {
@return map-get($colors, $key);
}
@function theme-color($key: "primary") {
@return map-get($theme-colors, $key);
}
@function gray($key: "100") {
@return map-get($grays, $key);
}
// Request a theme color level
@function theme-color-level($color-name: "primary", $level: 0) {
$color: theme-color($color-name);
$color-base: if($level > 0, $black, $white);
$level: abs($level);
@return mix($color-base, $color, $level * $theme-color-interval);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment