diff --git a/packages/transducers/README.md b/packages/transducers/README.md index a52e724d4c..d263b458fd 100644 --- a/packages/transducers/README.md +++ b/packages/transducers/README.md @@ -30,6 +30,7 @@ This project is part of the - [Bitstream](#bitstream) - [Base64 & UTF-8 en/decoding](#base64--utf-8-endecoding) - [Weighted random choices](#weighted-random-choices) + - [Keyframe interpolation](#keyframe-interpolation) - [API](#api) - [Types](#types) - [IReducible](#ireducible) @@ -507,6 +508,22 @@ tx.transduce(tx.take(1000), tx.frequencies(), tx.choices("abcd", [1, 0.5, 0.25, // Map { 'c' => 132, 'a' => 545, 'b' => 251, 'd' => 72 } ``` +### Keyframe interpolation + +See [`interpolate()`](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/interpolate.ts) for details. + +```ts +[...tx.interpolate( + 10, + (a, b) => [a,b], + ([a, b], t) => Math.floor(a + (b-a) * t), + [0.2, 100], + [0.5, 200], + [0.8, 0] +)] +// [ 100, 100, 100, 133, 166, 200, 133, 66, 0, 0, 0 ] +``` + ## API _Documentation is slowly forthcoming in the form of doc comments (incl. @@ -764,6 +781,7 @@ tx.transduce(tx.map((x) => x*10), tx.push(), tx.range(4)) - [choices](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/choices.ts) - [concat](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/concat.ts) - [cycle](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/cycle.ts) +- [interpolate](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/interpolate.ts) - [iterate](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/iterate.ts) - [keys](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/keys.ts) - [normRange](https://github.com/thi-ng/umbrella/tree/master/packages/transducers/src/iter/normRange.ts) diff --git a/packages/transducers/src/index.ts b/packages/transducers/src/index.ts index 72bf8c7fbf..a70df562e3 100644 --- a/packages/transducers/src/index.ts +++ b/packages/transducers/src/index.ts @@ -124,6 +124,7 @@ export * from "./iter/as-iterable"; export * from "./iter/choices"; export * from "./iter/concat"; export * from "./iter/cycle"; +export * from "./iter/interpolate"; export * from "./iter/iterate"; export * from "./iter/keys"; export * from "./iter/norm-range"; diff --git a/packages/transducers/src/iter/interpolate.ts b/packages/transducers/src/iter/interpolate.ts new file mode 100644 index 0000000000..9fea9618fd --- /dev/null +++ b/packages/transducers/src/iter/interpolate.ts @@ -0,0 +1,68 @@ +import { repeat } from "./repeat"; +import { normRange } from "./norm-range"; + +/** + * 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. + * + * 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. + * + * 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. + * + * ``` + * [...interpolate( + * 10, + * (a, b) => [a,b], + * ([a, b], t) => Math.floor(a + (b - a) * t), + * [0.2, 100], + * [0.5, 200], + * [0.8, 0] + * )] + * // [ 100, 100, 100, 133, 166, 200, 133, 66, 0, 0, 0 ] + * ``` + * + * @param n + * @param init + * @param mix + * @param stops + */ +export function* interpolate( + n: number, + init: (a: A, b: A) => B, + mix: (interval: B, t: number) => C, + ...stops: [number, A][] +): IterableIterator { + let l = stops.length; + if (l < 1) return; + 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]]); + } + if (stops[0][0] > 0) { + stops.unshift([0, stops[0][1]]); + } + let start = stops[0][0]; + let end = stops[1][0]; + let interval = init(stops[0][1], stops[1][1]); + let i = 1; + l = stops.length - 1; + for (let t of normRange(n)) { + if (t > end && t < 1) { + while (i < l && t > stops[i][0]) i++; + start = stops[i - 1][0]; + end = stops[i][0]; + interval = init(stops[i - 1][1], stops[i][1]); + } + yield mix(interval, end !== start ? (t - start) / (end - start) : 0); + } +}