Skip to content

Commit

Permalink
fix(transducers): interpolate() interval selection, add minPos/maxPos
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Dec 29, 2018
1 parent eae8d14 commit a90a712
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 32 deletions.
18 changes: 11 additions & 7 deletions packages/transducers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,16 +510,20 @@ tx.transduce(tx.take(1000), tx.frequencies(), tx.choices("abcd", [1, 0.5, 0.25,

### Keyframe interpolation

See [`interpolate()`](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/interpolate.ts) for details.
See
[`interpolate()`](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/interpolate.ts)
docs for details.

```ts
[...tx.interpolate(
[...interpolate(
10,
(a, b) => [a,b],
([a, b], t) => Math.floor(a + (b-a) * t),
[0.2, 100],
[0.5, 200],
[0.8, 0]
0,
100,
(a, b) => [a, b],
([a, b], t) => Math.floor(a + (b - a) * t),
[20, 100],
[50, 200],
[80, 0]
)]
// [ 100, 100, 100, 133, 166, 200, 133, 66, 0, 0, 0 ]
```
Expand Down
92 changes: 67 additions & 25 deletions packages/transducers/src/iter/interpolate.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,78 @@
import { repeat } from "./repeat";
import { normRange } from "./norm-range";
import { repeat } from "./repeat";

/**
* Takes a number of keyframe tuples (`stops`) and yields a sequence of
* `n` equally spaced, interpolated values. Keyframes are defined as
* `[pos, value]`, where `pos` must be a normalized value in [0,1]
* interval.
* `n+1` equally spaced, interpolated values. Keyframes are defined as
* `[pos, value]`. Only values in the closed `minPos` .. `maxPos`
* interval will be computed.
*
* Interpolation happens in two stages: First the given `init` function
* is called for each new key frame pair to produce a single interval
* type. Then for each result value calls `mix` with the current
* interval and interpolation time value `t` (normalized). The iterator
* yields results of these `mix()` function calls.
* is called to transform/prepare pairs of consecutive keyframes into a
* single interval (user defined). Then to produce each interpolated
* value calls `mix` with the currently active interval and
* interpolation time value `t` (re-normalized and relative to current
* interval). The iterator yields results of these `mix()` function
* calls.
*
* Depending on the overall number of samples requested and the distance
* between keyframes, some keyframes MIGHT be skipped. E.g. if
* requesting 10 samples within [0,1], the interval between two
* successive keyframes at 0.12 and 0.19 would be skipped entirely,
* since samples will only be taken at multiples of `1/n` (0.0, 0.1,
* 0.2... in this example).
*
* The given keyframe positions don't need to cover the full [0,1] range
* and interpolated values before the 1st or last keyframe will yield
* the value of the 1st/last keyframe.
* The given keyframe positions can lie outside the `minPos`/`maxPos`
* range and also don't need to cover the range fully. In the latter
* case, interpolated values before the first or after the last keyframe
* will yield the value of the 1st/last keyframe. If only a single
* keyframe is given in total, all `n` yielded samples will be that
* keyframe's transformed value.
*
* ```
* [...interpolate(
* 10,
* (a, b) => [a,b],
* 0,
* 100,
* (a, b) => [a, b],
* ([a, b], t) => Math.floor(a + (b - a) * t),
* [0.2, 100],
* [0.5, 200],
* [0.8, 0]
* [20, 100],
* [50, 200],
* [80, 0]
* )]
* // [ 100, 100, 100, 133, 166, 200, 133, 66, 0, 0, 0 ]
* ```
*
* Using easing functions (e.g. from thi.ng/math), non-linear
* interpolation within each keyframe interval can be achieved:
*
* ```
* import { mix, smoothStep } from "@thi.ng/math"
*
* [...interpolate(
* 10,
* 0,
* 100,
* (a, b) => [a, b],
* ([a, b], t) => Math.floor(mix(a, b, smoothStep(0.1, 0.9, t))),
* [20, 100],
* [50, 200],
* [80, 0]
* )]
* // [ 100, 100, 100, 120, 179, 200, 158, 41, 0, 0, 0 ]
* ```
*
* @param n
* @param init
* @param mix
* @param stops
* @param minPos
* @param maxPos
* @param init interval producer (from 2 keyframe values)
* @param mix interval interpolator
* @param stops keyframe / stops
*/
export function* interpolate<A, B, C>(
n: number,
minPos: number,
maxPos: number,
init: (a: A, b: A) => B,
mix: (interval: B, t: number) => C,
...stops: [number, A][]
Expand All @@ -45,24 +82,29 @@ export function* interpolate<A, B, C>(
if (l === 1) {
yield* repeat(mix(init(stops[0][1], stops[0][1]), 0), n);
}
if (stops[l - 1][0] < 1) {
stops.push([1, stops[l - 1][1]]);
stops.sort((a, b) => a[0] - b[0]);
if (stops[l - 1][0] < maxPos) {
stops.push([maxPos, stops[l - 1][1]]);
}
if (stops[0][0] > 0) {
stops.unshift([0, stops[0][1]]);
if (stops[0][0] > minPos) {
stops.unshift([minPos, stops[0][1]]);
}
const range = maxPos - minPos;
let start = stops[0][0];
let end = stops[1][0];
let delta = end - start;
let interval = init(stops[0][1], stops[1][1]);
let i = 1;
l = stops.length - 1;
l = stops.length;
for (let t of normRange(n)) {
if (t > end && t < 1) {
t = minPos + range * t;
if (t > end) {
while (i < l && t > stops[i][0]) i++;
start = stops[i - 1][0];
end = stops[i][0];
delta = end - start;
interval = init(stops[i - 1][1], stops[i][1]);
}
yield mix(interval, end !== start ? (t - start) / (end - start) : 0);
yield mix(interval, delta !== 0 ? (t - start) / delta : 0);
}
}

0 comments on commit a90a712

Please sign in to comment.