UNPKG

@thi.ng/ramp

Version:

Extensible keyframe interpolation/tweening of arbitrary, nested types

130 lines (129 loc) 3.4 kB
import { binarySearch } from "@thi.ng/arrays/binary-search"; import { peek } from "@thi.ng/arrays/peek"; import { compareNumAsc } from "@thi.ng/compare/numeric"; import { assert } from "@thi.ng/errors/assert"; import { absDiff } from "@thi.ng/math/abs"; import { clamp } from "@thi.ng/math/interval"; import { unconstrained } from "./domain.js"; import { __samples } from "./utils.js"; const ramp = (impl, stops, opts) => new Ramp(impl, stops, opts); class Ramp { constructor(impl, stops, opts) { this.impl = impl; this.stops = stops; assert(stops.length >= 2, `require at least 2 keyframes/stops`); const $opts = { domain: unconstrained, ...opts }; this.domain = $opts.domain; this.sort(); } domain; copy() { return new Ramp( this.impl, this.stops.map((x) => x.slice()) ); } empty() { return new Ramp(this.impl, []); } /** * Samples the ramp at given time `t` and returns interpolated value. * * @remarks * The given `t` is first processed by the configured time * {@link Ramp.domain} function. * * @param t */ at(t) { const { domain, impl, stops } = this; const n = stops.length - 1; const first = stops[0]; const last = stops[n]; t = domain(t, first[0], last[0]); const i = this.timeIndex(t); return i < 0 ? first[1] : i >= n ? last[1] : impl.at(stops, i, t); } samples(n = 100, start, end) { return __samples(this, n, start, end); } bounds() { const { impl, stops } = this; const n = stops.length; let min = null; let max = null; for (let i = n; i-- > 0; ) { const val = stops[i][1]; min = impl.min(min, val); max = impl.max(max, val); } return { min, max, minT: stops[0][0], maxT: stops[n - 1][0] }; } timeBounds() { return [this.stops[0][0], peek(this.stops)[0]]; } setStopAt(t, val, eps = 0.01) { const idx = this.closestIndex(t, eps); if (idx < 0) { this.stops.push([t, val]); this.sort(); return true; } this.stops[idx][1] = val; return false; } removeStopAt(t, eps = 0.01) { return this.stops.length > 2 ? this.removeStopAtIndex(this.closestIndex(t, eps)) : false; } removeStopAtIndex(i) { const stops = this.stops; if (i < 0 || i >= stops.length || stops.length <= 2) return false; stops.splice(i, 1); return true; } closestIndex(t, eps = 0.01) { const stops = this.stops; for (let i = stops.length; i-- > 0; ) { if (absDiff(t, stops[i][0]) < eps) return i; } return -1; } clampedIndexTime(i, t, eps = 0.01) { const stops = this.stops; const n = stops.length - 1; return i == 0 ? Math.min(t, stops[1][0] - eps) : i === n ? Math.max(t, stops[n - 1][0] + eps) : clamp(t, stops[i - 1][0] + eps, stops[i + 1][0] - eps); } sort() { this.stops.sort((a, b) => a[0] - b[0]); } uniform() { const n = this.stops.length - 1; this.stops.forEach((p, i) => p[0] = i / n); } timeIndex(t) { const stops = this.stops; const n = stops.length; if (n < 256) { for (let i = n; i-- > 0; ) { if (t >= stops[i][0]) return i; } return -1; } const idx = binarySearch( stops, [t, null], (x) => x[0], compareNumAsc ); return idx < -1 ? -(idx + 2) : idx; } } export { Ramp, ramp };