Skip to content

Commit

Permalink
feat(stdlib): Add linearInterpolate, linearMap and clamp (#1707)
Browse files Browse the repository at this point in the history
* feat: Add `linearInterpolate`, `linearMap` and `clamp`

* Update stdlib/number.gr

Co-authored-by: Oscar Spencer <oscar.spen@gmail.com>

* chore: regen docs

* chore: Apply suggestions from code review

* chore: apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Blaine Bublitz <blaine.bublitz@gmail.com>

* chore: Regen grain doc

---------

Co-authored-by: Oscar Spencer <oscar.spen@gmail.com>
Co-authored-by: Blaine Bublitz <blaine.bublitz@gmail.com>
  • Loading branch information
3 people authored Apr 27, 2023
1 parent 6930794 commit 15842a1
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 0 deletions.
76 changes: 76 additions & 0 deletions compiler/test/stdlib/number.test.gr
Original file line number Diff line number Diff line change
Expand Up @@ -710,3 +710,79 @@ assert Number.toRadians(1/4) == 0.004363323129985824
assert Number.toRadians(9223372036854775809) == 160978210179491630.0
assert Number.toRadians(Infinity) == Infinity
assert Number.isNaN(Number.toRadians(NaN))

// Number.clamp
// TODO(#471): Use Range Syntax
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, 1) == 1
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, 0) == 0
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, 0.5) == 0.5
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, 1/2) == 1/2
assert Number.clamp({ rangeStart: 0, rangeEnd: 1/2 }, 1/2) == 1/2
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, -0.1) == 0
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, -1) == 0
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, 1.1) == 1
assert Number.clamp({ rangeStart: 0, rangeEnd: 1 }, 2) == 1
assert Number.clamp({ rangeStart: -1, rangeEnd: 1 }, -1) == -1
assert Number.clamp({ rangeStart: -2, rangeEnd: -1 }, -1) == -1
assert Number.clamp({ rangeStart: -2, rangeEnd: -1 }, -2) == -2
assert Number.clamp({ rangeStart: -1, rangeEnd: -2 }, -2) == -2
assert Number.clamp({ rangeStart: -1, rangeEnd: -2 }, -2) == -2
assert Number.clamp({ rangeStart: Infinity, rangeEnd: -Infinity }, 2) == 2
assert Number.clamp({ rangeStart: -Infinity, rangeEnd: Infinity }, 2) == 2
assert Number.clamp({ rangeStart: Infinity, rangeEnd: -Infinity }, Infinity) ==
Infinity
assert Number.isNaN(
Number.clamp({ rangeStart: -Infinity, rangeEnd: Infinity }, NaN)
)
assert Number.isNaN(Number.clamp({ rangeStart: 0, rangeEnd: -1 }, NaN))

// Number.linearInterpolate
// TODO(#471): Use Range Syntax
assert Number.linearInterpolate({ rangeStart: 0, rangeEnd: 1 }, 0) == 0
assert Number.linearInterpolate({ rangeStart: 0, rangeEnd: 1 }, 1) == 1
assert Number.linearInterpolate({ rangeStart: 0, rangeEnd: 1 }, 0.5) == 0.5
assert Number.linearInterpolate({ rangeStart: 0, rangeEnd: 1 }, 0.75) == 0.75
assert Number.linearInterpolate({ rangeStart: 0, rangeEnd: 100 }, 0.75) == 75
assert Number.linearInterpolate({ rangeStart: 0, rangeEnd: 100 }, 1/4) == 25
assert Number.linearInterpolate({ rangeStart: -100, rangeEnd: 0 }, 0.5) == -50
assert Number.linearInterpolate({ rangeStart: -100, rangeEnd: 100 }, 0.5) == 0
assert Number.linearInterpolate({ rangeStart: -100, rangeEnd: 100 }, 1/2) == 0

// Number.linearMap
// TODO(#471): Use Range Syntax
assert Number.linearMap(
{ rangeStart: 0, rangeEnd: 1 },
{ rangeStart: 0, rangeEnd: 100 },
1/2
) ==
50
assert Number.linearMap(
{ rangeStart: 0, rangeEnd: 1 },
{ rangeStart: 0, rangeEnd: 50 },
1/2
) ==
25
assert Number.linearMap(
{ rangeStart: -1, rangeEnd: 1 },
{ rangeStart: 0, rangeEnd: 50 },
0
) ==
25
assert Number.linearMap(
{ rangeStart: -1, rangeEnd: 1 },
{ rangeStart: 0, rangeEnd: 50 },
2
) ==
50
assert Number.linearMap(
{ rangeStart: -1, rangeEnd: 1 },
{ rangeStart: 0, rangeEnd: 50 },
-2
) ==
0
assert Number.linearMap(
{ rangeStart: -1, rangeEnd: 1 },
{ rangeStart: 0, rangeEnd: 200 },
0.5
) ==
150
80 changes: 80 additions & 0 deletions stdlib/number.gr
Original file line number Diff line number Diff line change
Expand Up @@ -670,3 +670,83 @@ provide let toRadians = degrees => degrees * (pi / 180)
* @since v0.5.4
*/
provide let toDegrees = radians => radians * (180 / pi)

/**
* Constrains a number within the given inclusive range.
*
* @param range: The inclusive range to clamp within
* @param input: The number to clamp
* @returns The constrained number
*
* @since v0.6.0
*/
provide let clamp = (range, input) => {
if (isNaN(input)) {
input
} else {
let rangeEnd = max(range.rangeStart, range.rangeEnd)
let rangeStart = min(range.rangeStart, range.rangeEnd)

if (input > rangeEnd) rangeEnd else if (input < rangeStart) rangeStart
else input
}
}

/**
* Maps a weight between 0 and 1 within the given inclusive range.
*
* @param range: The inclusive range to interpolate within
* @param weight: The weight to interpolate
* @returns The blended value
*
* @throws InvalidArgument(String): When `weight` is not between 0 and 1
* @throws InvalidArgument(String): When `range` is not finite
* @throws InvalidArgument(String): When `range` includes NaN
*
* @since v0.6.0
*/
provide let linearInterpolate = (range, weight) => {
if (weight < 0 || weight > 1 || isNaN(weight))
throw Exception.InvalidArgument("Weight must be between 0 and 1")
if (isInfinite(range.rangeStart) || isInfinite(range.rangeEnd))
throw Exception.InvalidArgument("The range must be finite")
if (isNaN(range.rangeStart) || isNaN(range.rangeEnd))
throw Exception.InvalidArgument("The range must not include NaN")
(range.rangeEnd - range.rangeStart) * weight + range.rangeStart
}

/**
* Scales a number from one inclusive range to another inclusive range.
* If the number is outside the input range, it will be clamped.
*
* @param inputRange: The inclusive range you are mapping from
* @param outputRange: The inclusive range you are mapping to
* @param current: The number to map
* @returns The mapped number
*
* @throws InvalidArgument(String): When `inputRange` is not finite
* @throws InvalidArgument(String): When `inputRange` includes NaN
* @throws InvalidArgument(String): When `outputRange` is not finite
* @throws InvalidArgument(String): When `outputRange` includes NaN
*
* @since v0.6.0
*/
provide let linearMap = (inputRange, outputRange, current) => {
if (isNaN(current)) {
current
} else {
if (isInfinite(inputRange.rangeStart) || isInfinite(inputRange.rangeEnd))
throw Exception.InvalidArgument("The inputRange must be finite")
if (isNaN(inputRange.rangeStart) || isNaN(inputRange.rangeEnd))
throw Exception.InvalidArgument("The inputRange must not include NaN")
if (isInfinite(outputRange.rangeStart) || isInfinite(outputRange.rangeEnd))
throw Exception.InvalidArgument("The outputRange must be finite")
if (isNaN(outputRange.rangeStart) || isNaN(outputRange.rangeEnd))
throw Exception.InvalidArgument("The outputRange must not include NaN")
let mapped = (current - inputRange.rangeStart) *
(outputRange.rangeEnd - outputRange.rangeStart) /
(inputRange.rangeEnd - inputRange.rangeStart) +
outputRange.rangeStart
clamp(outputRange, mapped)
}
}
99 changes: 99 additions & 0 deletions stdlib/number.md
Original file line number Diff line number Diff line change
Expand Up @@ -986,3 +986,102 @@ Returns:
|----|-----------|
|`Number`|The value in degrees|

### Number.**clamp**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
clamp : (range: Range<Number>, input: Number) -> Number
```

Constrains a number within the given inclusive range.

Parameters:

|param|type|description|
|-----|----|-----------|
|`range`|`Range<Number>`|The inclusive range to clamp within|
|`input`|`Number`|The number to clamp|

Returns:

|type|description|
|----|-----------|
|`Number`|The constrained number|

### Number.**linearInterpolate**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
linearInterpolate : (range: Range<Number>, weight: Number) -> Number
```

Maps a weight between 0 and 1 within the given inclusive range.

Parameters:

|param|type|description|
|-----|----|-----------|
|`range`|`Range<Number>`|The inclusive range to interpolate within|
|`weight`|`Number`|The weight to interpolate|

Returns:

|type|description|
|----|-----------|
|`Number`|The blended value|

Throws:

`InvalidArgument(String)`

* When `weight` is not between 0 and 1
* When `range` is not finite
* When `range` includes NaN

### Number.**linearMap**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
linearMap :
(inputRange: Range<Number>, outputRange: Range<Number>, current: Number) ->
Number
```

Scales a number from one inclusive range to another inclusive range.
If the number is outside the input range, it will be clamped.

Parameters:

|param|type|description|
|-----|----|-----------|
|`inputRange`|`Range<Number>`|The inclusive range you are mapping from|
|`outputRange`|`Range<Number>`|The inclusive range you are mapping to|
|`current`|`Number`|The number to map|

Returns:

|type|description|
|----|-----------|
|`Number`|The mapped number|

Throws:

`InvalidArgument(String)`

* When `inputRange` is not finite
* When `inputRange` includes NaN
* When `outputRange` is not finite
* When `outputRange` includes NaN

0 comments on commit 15842a1

Please sign in to comment.