@thi.ng/ramp
Version:
Extensible keyframe interpolation/tweening of arbitrary, nested types
132 lines (131 loc) • 3.41 kB
JavaScript
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();
}
impl;
stops;
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
};