Skip to content

Commit 75cab86

Browse files
Awjin AhnAwjin
authored andcommitted
Add trigonometric math functions. (#921)
See sass/sass-spec#1504
1 parent 3cbaec1 commit 75cab86

File tree

2 files changed

+135
-37
lines changed

2 files changed

+135
-37
lines changed

CHANGELOG.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
## 1.25.0
22

33
* Add functions to the built-in "sass:math" module.
4-
* `clamp()`: given a `$min`, $number`, and `$max` values, clamps the `$number`
5-
in between `$min` and `$max`.
6-
* `hypot()`: given *n* numbers, outputs the length of the *n*-dimensional
7-
vector that has components equal to each of the inputs.
8-
* Exponential (all inputs must be unitless):
9-
* `log($number)` or `log($number, $base)`. If no base is provided, `log()`
10-
performs a natural log.
4+
5+
* `clamp($min, $number, $max)`. Clamps `$number` in between `$min` and `$max`.
6+
7+
* `hypot($numbers...)`. Given *n* numbers, outputs the length of the
8+
*n*-dimensional vector that has components equal to each of the inputs.
9+
10+
* Exponential. All inputs must be unitless.
11+
* `log($number)` or `log($number, $base)`. If no base is provided, performs
12+
a natural log.
1113
* `pow($base, $exponent)`
1214
* `sqrt($number)`
1315

16+
* Trigonometric. The input must be an angle. If no unit is given, the input is
17+
assumed to be in `rad`.
18+
* `cos($number)`
19+
* `sin($number)`
20+
* `tan($number)`
21+
22+
* Inverse trigonometric. The output is in `deg`.
23+
* `acos($number)`. Input must be unitless.
24+
* `asin($number)`. Input must be unitless.
25+
* `atan($number)`. Input must be unitless.
26+
* `atan2($y, $x)`. `$y` and `$x` must have compatible units or be unitless.
27+
1428
* Add the variables `$pi` and `$e` to the built-in "sass:math" module.
1529

1630
## 1.24.4

lib/src/functions/math.dart

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ final global = UnmodifiableListView([
2323

2424
/// The Sass math module.
2525
final module = BuiltInModule("math", functions: [
26-
_abs, _ceil, _clamp, _compatible, _floor, _hypot, _isUnitless, _log, _max, //
27-
_min, _pow, _percentage, _randomFunction, _round, _sqrt, _unit,
26+
_abs, _acos, _asin, _atan, _atan2, _ceil, _clamp, _cos, _compatible, //
27+
_floor, _hypot, _isUnitless, _log, _max, _min, _percentage, _pow, //
28+
_randomFunction, _round, _sin, _sqrt, _tan, _unit,
2829
], variables: {
2930
"e": SassNumber(math.e),
3031
"pi": SassNumber(math.pi),
@@ -51,7 +52,6 @@ final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
5152
var min = arguments[0].assertNumber("min");
5253
var number = arguments[1].assertNumber("number");
5354
var max = arguments[2].assertNumber("max");
54-
5555
if (min.hasUnits == number.hasUnits && number.hasUnits == max.hasUnits) {
5656
if (min.greaterThanOrEquals(max).isTruthy) return min;
5757
if (min.greaterThanOrEquals(number).isTruthy) return min;
@@ -63,7 +63,6 @@ final _clamp = BuiltInCallable("clamp", r"$min, $number, $max", (arguments) {
6363
var arg2Name = min.hasUnits != number.hasUnits ? "\$number" : "\$max";
6464
var unit1 = min.hasUnits ? "has unit ${min.unitString}" : "is unitless";
6565
var unit2 = arg2.hasUnits ? "has unit ${arg2.unitString}" : "is unitless";
66-
6766
throw SassScriptException(
6867
"\$min $unit1 but $arg2Name $unit2. Arguments must all have units or all "
6968
"be unitless.");
@@ -102,19 +101,19 @@ final _abs = _numberFunction("abs", (value) => value.abs());
102101
final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
103102
var numbers =
104103
arguments[0].asList.map((argument) => argument.assertNumber()).toList();
105-
106104
if (numbers.isEmpty) {
107105
throw SassScriptException("At least one argument must be passed.");
108106
}
109107

110108
var numeratorUnits = numbers[0].numeratorUnits;
111109
var denominatorUnits = numbers[0].denominatorUnits;
112110
var subtotal = 0.0;
113-
114111
for (var i = 0; i < numbers.length; i++) {
115112
var number = numbers[i];
116-
117-
if (number.hasUnits != numbers[0].hasUnits) {
113+
if (number.hasUnits == numbers[0].hasUnits) {
114+
number = number.coerce(numeratorUnits, denominatorUnits);
115+
subtotal += math.pow(number.value, 2);
116+
} else {
118117
var unit1 = numbers[0].hasUnits
119118
? "has unit ${numbers[0].unitString}"
120119
: "is unitless";
@@ -124,11 +123,7 @@ final _hypot = BuiltInCallable("hypot", r"$numbers...", (arguments) {
124123
"Argument 1 $unit1 but argument ${i + 1} $unit2. Arguments must all "
125124
"have units or all be unitless.");
126125
}
127-
128-
number = number.coerce(numeratorUnits, denominatorUnits);
129-
subtotal += math.pow(number.value, 2);
130126
}
131-
132127
return SassNumber.withUnits(math.sqrt(subtotal),
133128
numeratorUnits: numeratorUnits, denominatorUnits: denominatorUnits);
134129
});
@@ -143,46 +138,37 @@ final _log = BuiltInCallable("log", r"$number, $base: null", (arguments) {
143138
throw SassScriptException("\$number: Expected $number to have no units.");
144139
}
145140

146-
var numberValue = fuzzyEquals(number.value, 0) ? 0 : number.value;
147-
141+
var numberValue = fuzzyRoundIfZero(number.value);
148142
if (arguments[1] == sassNull) return SassNumber(math.log(numberValue));
149143

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

155-
var baseValue = fuzzyEquals(base.value, 0) || fuzzyEquals(base.value, 1)
149+
var baseValue = fuzzyEquals(base.value, 1)
156150
? fuzzyRound(base.value)
157-
: base.value;
158-
151+
: fuzzyRoundIfZero(base.value);
159152
return SassNumber(math.log(numberValue) / math.log(baseValue));
160153
});
161154

162155
final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
163156
var base = arguments[0].assertNumber("base");
164157
var exponent = arguments[1].assertNumber("exponent");
165-
166158
if (base.hasUnits) {
167159
throw SassScriptException("\$base: Expected $base to have no units.");
168160
} else if (exponent.hasUnits) {
169161
throw SassScriptException(
170162
"\$exponent: Expected $exponent to have no units.");
171163
}
172164

173-
var baseValue = base.value;
174-
var exponentValue = exponent.value;
175-
176-
if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
177-
return SassNumber(double.nan);
178-
}
179-
180165
// Exponentiating certain real numbers leads to special behaviors. Ensure that
181166
// these behaviors are consistent for numbers within the precision limit.
182-
if (fuzzyEquals(exponentValue, 0)) {
183-
exponentValue = 0;
167+
var baseValue = fuzzyRoundIfZero(base.value);
168+
var exponentValue = fuzzyRoundIfZero(exponent.value);
169+
if (fuzzyEquals(baseValue.abs(), 1) && exponentValue.isInfinite) {
170+
return SassNumber(double.nan);
184171
} else if (fuzzyEquals(baseValue, 0)) {
185-
baseValue = baseValue.isNegative ? -0.0 : 0;
186172
if (exponentValue.isFinite &&
187173
fuzzyIsInt(exponentValue) &&
188174
fuzzyAsInt(exponentValue) % 2 == 1) {
@@ -200,7 +186,6 @@ final _pow = BuiltInCallable("pow", r"$base, $exponent", (arguments) {
200186
fuzzyAsInt(exponentValue) % 2 == 1) {
201187
exponentValue = fuzzyRound(exponentValue);
202188
}
203-
204189
return SassNumber(math.pow(baseValue, exponentValue));
205190
});
206191

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

213-
return SassNumber(math.sqrt(number.value));
198+
var numberValue = fuzzyRoundIfZero(number.value);
199+
return SassNumber(math.sqrt(numberValue));
200+
});
201+
202+
num fuzzyRoundIfZero(num number) {
203+
if (!fuzzyEquals(number, 0)) return number;
204+
return number.isNegative ? -0.0 : 0;
205+
}
206+
207+
///
208+
/// Trigonometric functions
209+
///
210+
211+
final _acos = BuiltInCallable("acos", r"$number", (arguments) {
212+
var number = arguments[0].assertNumber("number");
213+
if (number.hasUnits) {
214+
throw SassScriptException("\$number: Expected $number to have no units.");
215+
}
216+
217+
var numberValue = fuzzyEquals(number.value.abs(), 1)
218+
? fuzzyRound(number.value)
219+
: number.value;
220+
var acos = math.acos(numberValue) * 180 / math.pi;
221+
return SassNumber.withUnits(acos, numeratorUnits: ['deg']);
214222
});
215223

224+
final _asin = BuiltInCallable("asin", r"$number", (arguments) {
225+
var number = arguments[0].assertNumber("number");
226+
if (number.hasUnits) {
227+
throw SassScriptException("\$number: Expected $number to have no units.");
228+
}
229+
230+
var numberValue = fuzzyEquals(number.value.abs(), 1)
231+
? fuzzyRound(number.value)
232+
: fuzzyRoundIfZero(number.value);
233+
var asin = math.asin(numberValue) * 180 / math.pi;
234+
return SassNumber.withUnits(asin, numeratorUnits: ['deg']);
235+
});
236+
237+
final _atan = BuiltInCallable("atan", r"$number", (arguments) {
238+
var number = arguments[0].assertNumber("number");
239+
if (number.hasUnits) {
240+
throw SassScriptException("\$number: Expected $number to have no units.");
241+
}
242+
243+
var numberValue = fuzzyRoundIfZero(number.value);
244+
var atan = math.atan(numberValue) * 180 / math.pi;
245+
return SassNumber.withUnits(atan, numeratorUnits: ['deg']);
246+
});
247+
248+
final _atan2 = BuiltInCallable("atan2", r"$y, $x", (arguments) {
249+
var y = arguments[0].assertNumber("y");
250+
var x = arguments[1].assertNumber("x");
251+
if (y.hasUnits != x.hasUnits) {
252+
var unit1 = y.hasUnits ? "has unit ${y.unitString}" : "is unitless";
253+
var unit2 = x.hasUnits ? "has unit ${x.unitString}" : "is unitless";
254+
throw SassScriptException(
255+
"\$y $unit1 but \$x $unit2. Arguments must all have units or all be "
256+
"unitless.");
257+
}
258+
259+
x = x.coerce(y.numeratorUnits, y.denominatorUnits);
260+
var xValue = fuzzyRoundIfZero(x.value);
261+
var yValue = fuzzyRoundIfZero(y.value);
262+
var atan2 = math.atan2(yValue, xValue) * 180 / math.pi;
263+
return SassNumber.withUnits(atan2, numeratorUnits: ['deg']);
264+
});
265+
266+
final _cos = BuiltInCallable("cos", r"$number", (arguments) {
267+
var number = _coerceToRad(arguments[0].assertNumber("number"));
268+
return SassNumber(math.cos(number.value));
269+
});
270+
271+
final _sin = BuiltInCallable("sin", r"$number", (arguments) {
272+
var number = _coerceToRad(arguments[0].assertNumber("number"));
273+
var numberValue = fuzzyRoundIfZero(number.value);
274+
return SassNumber(math.sin(numberValue));
275+
});
276+
277+
final _tan = BuiltInCallable("tan", r"$number", (arguments) {
278+
var number = _coerceToRad(arguments[0].assertNumber("number"));
279+
var asymptoteInterval = 0.5 * math.pi;
280+
var tanPeriod = 2 * math.pi;
281+
if (fuzzyEquals((number.value - asymptoteInterval) % tanPeriod, 0)) {
282+
return SassNumber(double.infinity);
283+
} else if (fuzzyEquals((number.value + asymptoteInterval) % tanPeriod, 0)) {
284+
return SassNumber(double.negativeInfinity);
285+
} else {
286+
var numberValue = fuzzyRoundIfZero(number.value);
287+
return SassNumber(math.tan(numberValue));
288+
}
289+
});
290+
291+
SassNumber _coerceToRad(SassNumber number) {
292+
try {
293+
return number.coerce(['rad'], []);
294+
} on SassScriptException catch (error) {
295+
if (!error.message.startsWith('Incompatible units')) rethrow;
296+
throw SassScriptException('\$number: Expected ${number} to be an angle.');
297+
}
298+
}
299+
216300
///
217301
/// Unit functions
218302
///

0 commit comments

Comments
 (0)