diff --git a/packages/transducers/README.md b/packages/transducers/README.md index d263b458fd..98958bbda3 100644 --- a/packages/transducers/README.md +++ b/packages/transducers/README.md @@ -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 ] ``` diff --git a/packages/transducers/src/iter/interpolate.ts b/packages/transducers/src/iter/interpolate.ts index 9fea9618fd..365be0163d 100644 --- a/packages/transducers/src/iter/interpolate.ts +++ b/packages/transducers/src/iter/interpolate.ts @@ -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( n: number, + minPos: number, + maxPos: number, init: (a: A, b: A) => B, mix: (interval: B, t: number) => C, ...stops: [number, A][] @@ -45,24 +82,29 @@ export function* interpolate( 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); } }