Skip to content

Commit

Permalink
Add trigonometric math functions. (#921)
Browse files Browse the repository at this point in the history
  • Loading branch information
Awjin Ahn authored and Awjin committed Jan 15, 2020
1 parent 3cbaec1 commit 75cab86
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 37 deletions.
28 changes: 21 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
## 1.25.0

* Add functions to the built-in "sass:math" module.
* `clamp()`: given a `$min`, $number`, and `$max` values, clamps the `$number`
in between `$min` and `$max`.
* `hypot()`: given *n* numbers, outputs the length of the *n*-dimensional
vector that has components equal to each of the inputs.
* Exponential (all inputs must be unitless):
* `log($number)` or `log($number, $base)`. If no base is provided, `log()`
performs a natural log.

* `clamp($min, $number, $max)`. Clamps `$number` in between `$min` and `$max`.

* `hypot($numbers...)`. Given *n* numbers, outputs the length of the
*n*-dimensional vector that has components equal to each of the inputs.

* Exponential. All inputs must be unitless.
* `log($number)` or `log($number, $base)`. If no base is provided, performs
a natural log.
* `pow($base, $exponent)`
* `sqrt($number)`

* Trigonometric. The input must be an angle. If no unit is given, the input is
assumed to be in `rad`.
* `cos($number)`
* `sin($number)`
* `tan($number)`

* Inverse trigonometric. The output is in `deg`.
* `acos($number)`. Input must be unitless.
* `asin($number)`. Input must be unitless.
* `atan($number)`. Input must be unitless.
* `atan2($y, $x)`. `$y` and `$x` must have compatible units or be unitless.

* Add the variables `$pi` and `$e` to the built-in "sass:math" module.

## 1.24.4
Expand Down
144 changes: 114 additions & 30 deletions lib/src/functions/math.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ final global = UnmodifiableListView([

/// The Sass math module.
final module = BuiltInModule("math", functions: [
_abs, _ceil, _clamp, _compatible, _floor, _hypot, _isUnitless, _log, _max, //
_min, _pow, _percentage, _randomFunction, _round, _sqrt, _unit,
_abs, _acos, _asin, _atan, _atan2, _ceil, _clamp, _cos, _compatible, //
_floor, _hypot, _isUnitless, _log, _max, _min, _percentage, _pow, //
_randomFunction, _round, _sin, _sqrt, _tan, _unit,
], variables: {
"e": SassNumber(math.e),
"pi": SassNumber(math.pi),
Expand All @@ -51,7 +52,6 @@ final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
var min = arguments[0].assertNumber("min");
var number = arguments[1].assertNumber("number");
var max = arguments[2].assertNumber("max");

if (min.hasUnits == number.hasUnits && number.hasUnits == max.hasUnits) {
if (min.greaterThanOrEquals(max).isTruthy) return min;
if (min.greaterThanOrEquals(number).isTruthy) return min;
Expand All @@ -63,7 +63,6 @@ final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
var arg2Name = min.hasUnits != number.hasUnits ? "\$number" : "\$max";
var unit1 = min.hasUnits ? "has unit ${min.unitString}" : "is unitless";
var unit2 = arg2.hasUnits ? "has unit ${arg2.unitString}" : "is unitless";

throw SassScriptException(
"\$min $unit1 but $arg2Name $unit2. Arguments must all have units or all "
"be unitless.");
Expand Down Expand Up @@ -102,19 +101,19 @@ final _abs = _numberFunction("abs", (value) => value.abs());
final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
var numbers =
arguments[0].asList.map((argument) => argument.assertNumber()).toList();

if (numbers.isEmpty) {
throw SassScriptException("At least one argument must be passed.");
}

var numeratorUnits = numbers[0].numeratorUnits;
var denominatorUnits = numbers[0].denominatorUnits;
var subtotal = 0.0;

for (var i = 0; i < numbers.length; i++) {
var number = numbers[i];

if (number.hasUnits != numbers[0].hasUnits) {
if (number.hasUnits == numbers[0].hasUnits) {
number = number.coerce(numeratorUnits, denominatorUnits);
subtotal += math.pow(number.value, 2);
} else {
var unit1 = numbers[0].hasUnits
? "has unit ${numbers[0].unitString}"
: "is unitless";
Expand All @@ -124,11 +123,7 @@ final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
"Argument 1 $unit1 but argument ${i + 1} $unit2. Arguments must all "
"have units or all be unitless.");
}

number = number.coerce(numeratorUnits, denominatorUnits);
subtotal += math.pow(number.value, 2);
}

return SassNumber.withUnits(math.sqrt(subtotal),
numeratorUnits: numeratorUnits, denominatorUnits: denominatorUnits);
});
Expand All @@ -143,46 +138,37 @@ final _log = BuiltInCallable("log", r"$number, $base: null", (arguments) {
throw SassScriptException("\$number: Expected $number to have no units.");
}

var numberValue = fuzzyEquals(number.value, 0) ? 0 : number.value;

var numberValue = fuzzyRoundIfZero(number.value);
if (arguments[1] == sassNull) return SassNumber(math.log(numberValue));

var base = arguments[1].assertNumber("base");
if (base.hasUnits) {
throw SassScriptException("\$base: Expected $base to have no units.");
}

var baseValue = fuzzyEquals(base.value, 0) || fuzzyEquals(base.value, 1)
var baseValue = fuzzyEquals(base.value, 1)
? fuzzyRound(base.value)
: base.value;

: fuzzyRoundIfZero(base.value);
return SassNumber(math.log(numberValue) / math.log(baseValue));
});

final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
var base = arguments[0].assertNumber("base");
var exponent = arguments[1].assertNumber("exponent");

if (base.hasUnits) {
throw SassScriptException("\$base: Expected $base to have no units.");
} else if (exponent.hasUnits) {
throw SassScriptException(
"\$exponent: Expected $exponent to have no units.");
}

var baseValue = base.value;
var exponentValue = exponent.value;

if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
return SassNumber(double.nan);
}

// Exponentiating certain real numbers leads to special behaviors. Ensure that
// these behaviors are consistent for numbers within the precision limit.
if (fuzzyEquals(exponentValue, 0)) {
exponentValue = 0;
var baseValue = fuzzyRoundIfZero(base.value);
var exponentValue = fuzzyRoundIfZero(exponent.value);
if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
return SassNumber(double.nan);
} else if (fuzzyEquals(baseValue, 0)) {
baseValue = baseValue.isNegative ? -0.0 : 0;
if (exponentValue.isFinite &&
fuzzyIsInt(exponentValue) &&
fuzzyAsInt(exponentValue) % 2 == 1) {
Expand All @@ -200,7 +186,6 @@ final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
fuzzyAsInt(exponentValue) % 2 == 1) {
exponentValue = fuzzyRound(exponentValue);
}

return SassNumber(math.pow(baseValue, exponentValue));
});

Expand All @@ -210,9 +195,108 @@ final _sqrt = BuiltInCallable("sqrt", r"$number", (arguments) {
throw SassScriptException("\$number: Expected $number to have no units.");
}

return SassNumber(math.sqrt(number.value));
var numberValue = fuzzyRoundIfZero(number.value);
return SassNumber(math.sqrt(numberValue));
});

num fuzzyRoundIfZero(num number) {
if (!fuzzyEquals(number, 0)) return number;
return number.isNegative ? -0.0 : 0;
}

///
/// Trigonometric functions
///
final _acos = BuiltInCallable("acos", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
}

var numberValue = fuzzyEquals(number.value.abs(), 1)
? fuzzyRound(number.value)
: number.value;
var acos = math.acos(numberValue) * 180 / math.pi;
return SassNumber.withUnits(acos, numeratorUnits: ['deg']);
});

final _asin = BuiltInCallable("asin", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
}

var numberValue = fuzzyEquals(number.value.abs(), 1)
? fuzzyRound(number.value)
: fuzzyRoundIfZero(number.value);
var asin = math.asin(numberValue) * 180 / math.pi;
return SassNumber.withUnits(asin, numeratorUnits: ['deg']);
});

final _atan = BuiltInCallable("atan", r"$number", (arguments) {
var number = arguments[0].assertNumber("number");
if (number.hasUnits) {
throw SassScriptException("\$number: Expected $number to have no units.");
}

var numberValue = fuzzyRoundIfZero(number.value);
var atan = math.atan(numberValue) * 180 / math.pi;
return SassNumber.withUnits(atan, numeratorUnits: ['deg']);
});

final _atan2 = BuiltInCallable("atan2", r"$y, $x", (arguments) {
var y = arguments[0].assertNumber("y");
var x = arguments[1].assertNumber("x");
if (y.hasUnits != x.hasUnits) {
var unit1 = y.hasUnits ? "has unit ${y.unitString}" : "is unitless";
var unit2 = x.hasUnits ? "has unit ${x.unitString}" : "is unitless";
throw SassScriptException(
"\$y $unit1 but \$x $unit2. Arguments must all have units or all be "
"unitless.");
}

x = x.coerce(y.numeratorUnits, y.denominatorUnits);
var xValue = fuzzyRoundIfZero(x.value);
var yValue = fuzzyRoundIfZero(y.value);
var atan2 = math.atan2(yValue, xValue) * 180 / math.pi;
return SassNumber.withUnits(atan2, numeratorUnits: ['deg']);
});

final _cos = BuiltInCallable("cos", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
return SassNumber(math.cos(number.value));
});

final _sin = BuiltInCallable("sin", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
var numberValue = fuzzyRoundIfZero(number.value);
return SassNumber(math.sin(numberValue));
});

final _tan = BuiltInCallable("tan", r"$number", (arguments) {
var number = _coerceToRad(arguments[0].assertNumber("number"));
var asymptoteInterval = 0.5 * math.pi;
var tanPeriod = 2 * math.pi;
if (fuzzyEquals((number.value - asymptoteInterval) % tanPeriod, 0)) {
return SassNumber(double.infinity);
} else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) {
return SassNumber(double.negativeInfinity);
} else {
var numberValue = fuzzyRoundIfZero(number.value);
return SassNumber(math.tan(numberValue));
}
});

SassNumber _coerceToRad(SassNumber number) {
try {
return number.coerce(['rad'], []);
} on SassScriptException catch (error) {
if (!error.message.startsWith('Incompatible units')) rethrow;
throw SassScriptException('\$number: Expected ${number} to be an angle.');
}
}

///
/// Unit functions
///
Expand Down

0 comments on commit 75cab86

Please sign in to comment.