@thi.ng/ramp 2.1.102 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2024-01-30T21:37:19Z
3
+ - **Last updated**: 2024-02-12T16:09:52Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,36 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ # [3.0.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/ramp@3.0.0) (2024-02-12)
13
+
14
+ #### 🛑 Breaking changes
15
+
16
+ - add support for arbitrary value types ([08e12c3](https://github.com/thi-ng/umbrella/commit/08e12c3))
17
+ - BREAKING CHANGE: add support for arbitrary value types, package restructure
18
+ - add unified Ramp class, remove obsolete ARamp, LinearRamp, HermiteRamp
19
+ - add interpolation presets to be used with generic Ramp
20
+ - LINEAR_N, LINEAR_V (numeric/vector valued)
21
+ - HERMITE_N, HERMITE_V
22
+ - update `linear()` & `hermite()` factory fns
23
+ - update Ramp ctor to ensure min. 2 keyframes/stops are provided
24
+ - add new types
25
+ - update/extend readme
26
+ - update pkg meta
27
+ - add nested type support, simplify RampImpl ([0daa663](https://github.com/thi-ng/umbrella/commit/0daa663))
28
+ - BREAKING CHANGE: rename interpolatedPoints() => samples()
29
+ - add nested() RampImpl
30
+ - update IRamp interface
31
+ - simplify RampImpl interface
32
+
33
+ #### 🚀 Features
34
+
35
+ - add time domain fns, grouped ramps, update API ([62c01d1](https://github.com/thi-ng/umbrella/commit/62c01d1))
36
+ - add time domain functions
37
+ - add group() ramp for nested, independent ramps
38
+ - add RampOpts
39
+ - extract IReadonlyRamp, update IRamp
40
+ - update/fix IRamp.addStopAt() to .setStopAt()
41
+
12
42
  ### [2.1.83](https://github.com/thi-ng/umbrella/tree/@thi.ng/ramp@2.1.83) (2023-11-09)
13
43
 
14
44
  #### ♻️ Refactoring
package/README.md CHANGED
@@ -1,5 +1,15 @@
1
1
  <!-- This file is generated - DO NOT EDIT! -->
2
2
  <!-- Please see: https://github.com/thi-ng/umbrella/blob/develop/CONTRIBUTING.md#changes-to-readme-files -->
3
+ > [!IMPORTANT]
4
+ > ‼️ Announcing the thi.ng user survey 2024 📋
5
+ >
6
+ > [Please participate in the survey here!](https://forms.gle/XacbSDEmQMPZg8197)\
7
+ > (open until end of February)
8
+ >
9
+ > **To achieve a better sample size, I'd highly appreciate if you could
10
+ > circulate the link to this survey in your own networks.**
11
+ >
12
+ > [Discussion](https://github.com/thi-ng/umbrella/discussions/447)
3
13
 
4
14
  # ![@thi.ng/ramp](https://media.thi.ng/umbrella/banners-20230807/thing-ramp.svg?4bf9a6f2)
5
15
 
@@ -21,15 +31,24 @@
21
31
  - [Dependencies](#dependencies)
22
32
  - [Usage examples](#usage-examples)
23
33
  - [API](#api)
34
+ - [Numeric](#numeric)
35
+ - [nD vectors](#nd-vectors)
36
+ - [Nested objects](#nested-objects)
37
+ - [Grouped & nested ramps](#grouped--nested-ramps)
24
38
  - [Authors](#authors)
25
39
  - [License](#license)
26
40
 
27
41
  ## About
28
42
 
29
- Parametric (non-)linearly interpolated 1D lookup tables for remapping values.
43
+ Extensible keyframe interpolation/tweening of arbitrary, nested types.
30
44
 
31
45
  ![screenshot](https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/ramp/readme.png)
32
46
 
47
+ This package can perform keyframe interpolation for ramps of numeric values,
48
+ n-dimensional vectors and nested objects of the same. It provides linear and
49
+ cubic hermite interpolation out of the box, but can be extended by implementing
50
+ a simple interface to achieve other interpolation methods.
51
+
33
52
  ## Status
34
53
 
35
54
  **STABLE** - used in production
@@ -56,42 +75,44 @@ For Node.js REPL:
56
75
  const ramp = await import("@thi.ng/ramp");
57
76
  ```
58
77
 
59
- Package sizes (brotli'd, pre-treeshake): ESM: 1.06 KB
78
+ Package sizes (brotli'd, pre-treeshake): ESM: 1.67 KB
60
79
 
61
80
  ## Dependencies
62
81
 
82
+ - [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
63
83
  - [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
64
84
  - [@thi.ng/compare](https://github.com/thi-ng/umbrella/tree/develop/packages/compare)
85
+ - [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
65
86
  - [@thi.ng/math](https://github.com/thi-ng/umbrella/tree/develop/packages/math)
66
87
  - [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/develop/packages/transducers)
67
88
  - [@thi.ng/vectors](https://github.com/thi-ng/umbrella/tree/develop/packages/vectors)
68
89
 
69
90
  ## Usage examples
70
91
 
71
- One project in this repo's
92
+ Several projects in this repo's
72
93
  [/examples](https://github.com/thi-ng/umbrella/tree/develop/examples)
73
- directory is using this package:
94
+ directory are using this package:
74
95
 
75
- | Screenshot | Description | Live demo | Source |
76
- |:------------------------------------------------------------------------------------------------------------------|:--------------------------------------------|:-------------------------------------------------|:------------------------------------------------------------------------------|
77
- | <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/ramp-synth.png" width="240"/> | Unison wavetable synth with waveform editor | [Demo](https://demo.thi.ng/umbrella/ramp-synth/) | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/ramp-synth) |
96
+ | Screenshot | Description | Live demo | Source |
97
+ |:------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------|:-------------------------------------------------------|:------------------------------------------------------------------------------------|
98
+ | <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/ramp-scroll-anim.png" width="240"/> | Scroll-based, reactive, multi-param CSS animation basics | [Demo](https://demo.thi.ng/umbrella/ramp-scroll-anim/) | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/ramp-scroll-anim) |
99
+ | <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/ramp-synth.png" width="240"/> | Unison wavetable synth with waveform editor | [Demo](https://demo.thi.ng/umbrella/ramp-synth/) | [Source](https://github.com/thi-ng/umbrella/tree/develop/examples/ramp-synth) |
78
100
 
79
101
  ## API
80
102
 
81
103
  [Generated API docs](https://docs.thi.ng/umbrella/ramp/)
82
104
 
83
- ```ts
105
+ ### Numeric
106
+
107
+ ```ts tangle:export/readme.ts
84
108
  import { linear, hermite } from "@thi.ng/ramp";
85
109
 
86
110
  const rampL = linear([[0.1, 0], [0.5, 1], [0.9, 0]]);
87
111
  const rampH = hermite([[0.1, 0], [0.5, 1], [0.9, 0]]);
88
112
 
89
113
  for(let i = 0; i <= 10; i++) {
90
- console.log(
91
- i / 10,
92
- rampL.at(i / 10).toFixed(2),
93
- rampH.at(i / 10).toFixed(2)
94
- );
114
+ const t = i / 10;
115
+ console.log(t, rampL.at(t).toFixed(2), rampH.at(t).toFixed(2));
95
116
  }
96
117
 
97
118
  // 0 0.00 0.00
@@ -107,6 +128,120 @@ for(let i = 0; i <= 10; i++) {
107
128
  // 1 0.00 0.00
108
129
  ```
109
130
 
131
+ ### nD vectors
132
+
133
+ ```ts tangle:export/readme-vector.ts
134
+ import { HERMITE_V, ramp } from "@thi.ng/ramp";
135
+ import { FORMATTER, VEC3 } from "@thi.ng/vectors";
136
+
137
+ // use the generic `ramp()` factory function with a custom implementation
138
+ // see: https://docs.thi.ng/umbrella/ramp/interfaces/RampImpl.html
139
+ const rgb = ramp(
140
+ // use a vector interpolation preset with the VEC3 API
141
+ HERMITE_V(VEC3),
142
+ // keyframes
143
+ [
144
+ [0.0, [1, 0, 0]], // red
145
+ [0.5, [0, 1, 0]], // green
146
+ [1.0, [0, 0, 1]], // blue
147
+ ]
148
+ );
149
+
150
+ for (let i = 0; i <= 20; i++) {
151
+ const t = i / 20;
152
+ console.log(t, FORMATTER(rgb.at(t)));
153
+ }
154
+
155
+ // 0 [1.000, 0.000, 0.000]
156
+ // 0.05 [0.972, 0.028, 0.000]
157
+ // 0.1 [0.896, 0.104, 0.000]
158
+ // 0.15 [0.784, 0.216, 0.000]
159
+ // 0.2 [0.648, 0.352, 0.000]
160
+ // 0.25 [0.500, 0.500, 0.000]
161
+ // 0.3 [0.352, 0.648, 0.000]
162
+ // 0.35 [0.216, 0.784, 0.000]
163
+ // 0.4 [0.104, 0.896, 0.000]
164
+ // 0.45 [0.028, 0.972, 0.000]
165
+ // 0.5 [0.000, 1.000, 0.000]
166
+ // 0.55 [0.000, 0.972, 0.028]
167
+ // 0.6 [0.000, 0.896, 0.104]
168
+ // 0.65 [0.000, 0.784, 0.216]
169
+ // 0.7 [0.000, 0.648, 0.352]
170
+ // 0.75 [0.000, 0.500, 0.500]
171
+ // 0.8 [0.000, 0.352, 0.648]
172
+ // 0.85 [0.000, 0.216, 0.784]
173
+ // 0.9 [0.000, 0.104, 0.896]
174
+ // 0.95 [0.000, 0.028, 0.972]
175
+ // 1 [0.000, 0.000, 1.000]
176
+ ```
177
+
178
+ ### Nested objects
179
+
180
+ ```ts tangle:export/readme-nested.ts
181
+ import { HERMITE_V, LINEAR_N, nested, ramp } from "@thi.ng/ramp";
182
+ import { VEC2 } from "@thi.ng/vectors";
183
+
184
+ const r = ramp(
185
+ nested({
186
+ a: LINEAR_N,
187
+ b: nested({
188
+ c: HERMITE_V(VEC2),
189
+ }),
190
+ }),
191
+ [
192
+ [0, { a: 0, b: { c: [100, 100] } }],
193
+ [0.5, { a: 10, b: { c: [300, 50] } }],
194
+ [1, { a: -10, b: { c: [200, 120] } }],
195
+ ]
196
+ );
197
+
198
+ // produce an iterator of N uniformly spaced sample points
199
+ // across the full range of the ramp
200
+ console.log([...r.samples(10)]);
201
+
202
+ // [
203
+ // [0, { a: 0, b: { c: [100, 100] } }],
204
+ // [0.1, { a: 2, b: { c: [120.8, 94.8] } }],
205
+ // [0.2, { a: 4, b: { c: [170.4, 82.4] } }],
206
+ // [0.3, { a: 6, b: { c: [229.6, 67.6] } }],
207
+ // [0.4, { a: 8, b: { c: [279.2, 55.2] } }],
208
+ // [0.5, { a: 10, b: { c: [300, 50] } }],
209
+ // [0.6, { a: 6, b: { c: [289.6, 57.28] } }],
210
+ // [0.7, { a: 2, b: { c: [264.8, 74.64] } }],
211
+ // [0.8, { a: -2, b: { c: [235.2, 95.36] } }],
212
+ // [0.9, { a: -6, b: { c: [210.4, 112.72] } }],
213
+ // [1, { a: -10, b: { c: [200, 120] } }]
214
+ // ]
215
+ ```
216
+
217
+ ### Grouped & nested ramps
218
+
219
+ ```ts tangle:export/readme-group.ts
220
+ import { group, linear, wrap } from "@thi.ng/ramp";
221
+
222
+ const example = group({
223
+ // child timeline with looping behavior
224
+ a: linear([[0, 0], [20, 100]], { domain: wrap }),
225
+ // another child timeline
226
+ b: linear([[10, 100], [90, 200]]),
227
+ });
228
+
229
+ console.log(JSON.stringify([...example.samples(10, 0, 100)]));
230
+ // [
231
+ // [0, { a: 0, b: 100 }],
232
+ // [10, { a: 50, b: 100 }],
233
+ // [20, { a: 100, b: 112.5 }],
234
+ // [30, { a: 50, b: 125 }],
235
+ // [40, { a: 100, b: 137.5 }],
236
+ // [50, { a: 50, b: 150 }],
237
+ // [60, { a: 0, b: 162.5 }],
238
+ // [70, { a: 50, b: 175 }],
239
+ // [80, { a: 0, b: 187.5 }],
240
+ // [90, { a: 50, b: 200 }],
241
+ // [100, { a: 50, b: 200 }]
242
+ // ]
243
+ ```
244
+
110
245
  ## Authors
111
246
 
112
247
  - [Karsten Schmidt](https://thi.ng)
package/api.d.ts CHANGED
@@ -1,23 +1,73 @@
1
- import type { ReadonlyVec, Vec } from "@thi.ng/vectors";
2
- export interface IRamp {
3
- stops: Vec[];
4
- at(t: number): number;
5
- bounds(): RampBounds;
6
- interpolatedPoints(res?: number): Iterable<ReadonlyVec>;
7
- addStopAt(t: number, y: number, eps?: number): boolean;
1
+ import type { Fn2 } from "@thi.ng/api";
2
+ export type Frame<T> = [number, T];
3
+ export interface RampImpl<T> {
4
+ min: Fn2<T | null, T, T>;
5
+ max: Fn2<T | null, T, T>;
6
+ at: (stops: Frame<T>[], index: number, t: number) => T;
7
+ }
8
+ export interface IReadonlyRamp<T> {
9
+ /**
10
+ * Computes interpolated value at time `t`. Depending on implementation, `t`
11
+ * might first be processed using the ramp's time domain function.
12
+ *
13
+ * @remarks
14
+ * Also see {@link RampOpts.domain}.
15
+ *
16
+ * @param t
17
+ */
18
+ at(t: number): T;
19
+ /**
20
+ * Returns an iterator of `n` uniformly spaced samples of the time domain
21
+ * between the first and last keyframe. Each returned sample is a tuple of
22
+ * `[time, interpolatedValue]`.
23
+ *
24
+ * @param n
25
+ * @param start
26
+ * @param end
27
+ */
28
+ samples(n?: number, start?: number, end?: number): Iterable<Frame<T>>;
29
+ /**
30
+ * Computes the ramp's min/max time domain and value bounds.
31
+ *
32
+ * @remarks
33
+ * Also see {@link IReadonlyRamp.timeBounds}.
34
+ */
35
+ bounds(): RampBounds<T>;
36
+ /**
37
+ * Computes the ramp's min/max time bounds (i.e. the positions of the first
38
+ * & last keyframes/stops).
39
+ *
40
+ * @remarks
41
+ * Also see {@link IReadonlyRamp.bounds}.
42
+ */
43
+ timeBounds(): [number, number];
44
+ }
45
+ export interface IRamp<T> extends IReadonlyRamp<T> {
46
+ impl: RampImpl<T>;
47
+ stops: Frame<T>[];
48
+ setStopAt(t: number, y: T, eps?: number): boolean;
8
49
  removeStopAt(t: number, eps?: number): boolean;
9
50
  closestIndex(t: number, eps?: number): number;
10
51
  clampedIndexTime(i: number, t: number, eps?: number): number;
11
- sort(): void;
12
- uniform(): void;
13
52
  }
14
- export interface RampBounds {
15
- min: number;
16
- max: number;
53
+ export interface RampOpts {
54
+ /**
55
+ * Time domain mapping function, e.g. to achieve looping. See {@link clamp},
56
+ * {@link wrap}, {@link wrapInterval}.
57
+ *
58
+ * @remarks
59
+ * The domain function can be changed dynamically by setting the
60
+ * {@link Ramp.domain} property.
61
+ *
62
+ * @defaultValue {@link unconstrained}
63
+ */
64
+ domain: RampDomain;
65
+ }
66
+ export interface RampBounds<T> {
67
+ min: T;
68
+ max: T;
17
69
  minT: number;
18
70
  maxT: number;
19
71
  }
20
- export interface RampConstructor {
21
- new (stops: Vec[]): IRamp;
22
- }
72
+ export type RampDomain = (t: number, min: number, max: number) => number;
23
73
  //# sourceMappingURL=api.d.ts.map
package/domain.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { RampDomain } from "./api.js";
2
+ /**
3
+ * Identity time domain function. Default for {@link RampOpts.domain}.
4
+ * Passthrough, no-op.
5
+ *
6
+ * @param t
7
+ */
8
+ export declare const unconstrained: RampDomain;
9
+ /**
10
+ * Time domain function. Clamps given lookup time `t` to the parent
11
+ * ramp's first & last keyframe times.
12
+ */
13
+ export declare const clamp: RampDomain;
14
+ /**
15
+ * Time domain function. Wraps given lookup time `t` into the interval defined
16
+ * by parent ramp's first & last keyframe times, thereby creating a looping
17
+ * effect.
18
+ */
19
+ export declare const wrap: RampDomain;
20
+ /**
21
+ * Higher-order time domain function and version of {@link wrap} to create a
22
+ * looping effect using the given `min` & `max` keyframe times (instead of the
23
+ * parent ramp's first/last keyframe times).
24
+ */
25
+ export declare const wrapInterval: (min: number, max: number) => RampDomain;
26
+ //# sourceMappingURL=domain.d.ts.map
package/domain.js ADDED
@@ -0,0 +1,11 @@
1
+ import { clamp as $clamp, wrap as $wrap } from "@thi.ng/math/interval";
2
+ const unconstrained = (t) => t;
3
+ const clamp = $clamp;
4
+ const wrap = $wrap;
5
+ const wrapInterval = (min, max) => (t) => $wrap(t, min, max);
6
+ export {
7
+ clamp,
8
+ unconstrained,
9
+ wrap,
10
+ wrapInterval
11
+ };
package/group.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { Frame, IRamp, IReadonlyRamp, RampBounds, RampDomain, RampOpts } from "./api.js";
2
+ export type GroupImpl<T extends Record<string, any>> = {
3
+ [P in keyof T]: IRamp<T[P]>;
4
+ };
5
+ /**
6
+ * Creates a new nested ramp from given object of otherwise independent child
7
+ * ramps (which can be groups themselves).
8
+ *
9
+ * @remarks
10
+ * Similar to {@link nested}, but different in that groups are merely views of
11
+ * independent timelines, each with their own set of keyframes, interpolation
12
+ * logic, time domain functions.
13
+ *
14
+ * Groups can have their own time domain function (given via `opts`, default:
15
+ * {@link unconstrained}), which will be applied _prior_ to evaluating any child ramps.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const example = group({
20
+ * // named, independent child ramps/timelines
21
+ * a: linear([[0.1, 0], [0.5, -10]]),
22
+ * b: hermite([[0, 100], [1, 200]]),
23
+ * });
24
+ *
25
+ * example.at(0.2)
26
+ * // { a: -2.5, b: 110.4 }
27
+ *
28
+ * // set new keyframe for `b` ramp
29
+ * // (in TS need to cast to proper type first)
30
+ * (<Ramp<number>>example.children.b).setStopAt(0.5, 200);
31
+ *
32
+ * example.at(0.2)
33
+ * // { a: -2.5, b: 135.2 }
34
+ * ```
35
+ *
36
+ * @param children
37
+ * @param opts
38
+ */
39
+ export declare const group: <T extends Record<string, any>>(children: GroupImpl<T>, opts?: Partial<RampOpts>) => Group<T>;
40
+ export declare class Group<T extends Record<string, any>> implements IReadonlyRamp<T> {
41
+ children: GroupImpl<T>;
42
+ domain: RampDomain;
43
+ protected childEntries: [keyof T, IReadonlyRamp<any>][];
44
+ constructor(children: GroupImpl<T>, opts?: Partial<RampOpts>);
45
+ at(t: number): T;
46
+ samples(n?: number, start?: number, end?: number): Iterable<Frame<T>>;
47
+ bounds(): RampBounds<T>;
48
+ timeBounds(): [number, number];
49
+ }
50
+ //# sourceMappingURL=group.d.ts.map
package/group.js ADDED
@@ -0,0 +1,60 @@
1
+ import { mix } from "@thi.ng/math/mix";
2
+ import { map } from "@thi.ng/transducers/map";
3
+ import { normRange } from "@thi.ng/transducers/norm-range";
4
+ import { unconstrained } from "./domain.js";
5
+ const group = (children, opts) => new Group(children, opts);
6
+ class Group {
7
+ constructor(children, opts) {
8
+ this.children = children;
9
+ this.childEntries = Object.entries(this.children);
10
+ this.domain = opts?.domain || unconstrained;
11
+ }
12
+ domain;
13
+ childEntries;
14
+ at(t) {
15
+ t = this.domain(t, ...this.timeBounds());
16
+ return this.childEntries.reduce((acc, [id, ramp]) => {
17
+ acc[id] = ramp.at(t);
18
+ return acc;
19
+ }, {});
20
+ }
21
+ samples(n = 100, start, end) {
22
+ if (start == void 0 || end == void 0) {
23
+ const bounds = this.timeBounds();
24
+ start = start ?? bounds[0];
25
+ end = end ?? bounds[1];
26
+ }
27
+ return map((t) => {
28
+ t = mix(start, end, t);
29
+ return [t, this.at(t)];
30
+ }, normRange(n));
31
+ }
32
+ bounds() {
33
+ return this.childEntries.reduce(
34
+ (acc, [id, ramp]) => {
35
+ const bounds = ramp.bounds();
36
+ acc.minT = Math.min(acc.minT, bounds.minT);
37
+ acc.maxT = Math.max(acc.maxT, bounds.maxT);
38
+ acc.min[id] = bounds.min;
39
+ acc.max[id] = bounds.max;
40
+ return acc;
41
+ },
42
+ { minT: Infinity, maxT: -Infinity, min: {}, max: {} }
43
+ );
44
+ }
45
+ timeBounds() {
46
+ return this.childEntries.reduce(
47
+ (acc, [_, ramp]) => {
48
+ const bounds = ramp.timeBounds();
49
+ acc[0] = Math.min(acc[0], bounds[0]);
50
+ acc[1] = Math.max(acc[1], bounds[1]);
51
+ return acc;
52
+ },
53
+ [Infinity, -Infinity]
54
+ );
55
+ }
56
+ }
57
+ export {
58
+ Group,
59
+ group
60
+ };
package/hermite.d.ts CHANGED
@@ -1,8 +1,25 @@
1
- import type { Vec } from "@thi.ng/vectors";
2
- import { ARamp } from "./aramp.js";
3
- export declare const hermite: (stops?: Vec[]) => HermiteRamp;
4
- export declare class HermiteRamp extends ARamp {
5
- at(t: number): number;
6
- interpolatedPoints(res?: number): IterableIterator<number[]>;
7
- }
1
+ import type { Vec, VecAPI } from "@thi.ng/vectors";
2
+ import type { Frame, RampImpl, RampOpts } from "./api.js";
3
+ import { Ramp } from "./ramp.js";
4
+ /**
5
+ * Syntax sugar for creating a numeric {@link Ramp} using the {@link HERMITE_N}
6
+ * ramp hermite spline interpolation impl and given stops (aka keyframes,
7
+ * minimum 2 required).
8
+ *
9
+ * @remarks
10
+ * For vector-valued hermite ramps, use {@link ramp} with {@link HERMITE_V}.
11
+ *
12
+ * References:
13
+ * - https://en.wikipedia.org/wiki/Cubic_Hermite_spline
14
+ *
15
+ * Also see:
16
+ * - https://docs.thi.ng/umbrella/math/functions/mixCubicHermite.html
17
+ * - https://docs.thi.ng/umbrella/math/functions/tangentCardinal.html
18
+ *
19
+ * @param stops
20
+ * @param opts
21
+ */
22
+ export declare const hermite: (stops: Frame<number>[], opts?: Partial<RampOpts>) => Ramp<number>;
23
+ export declare const HERMITE_N: RampImpl<number>;
24
+ export declare const HERMITE_V: <T extends Vec>(vec: VecAPI) => RampImpl<T>;
8
25
  //# sourceMappingURL=hermite.d.ts.map
package/hermite.js CHANGED
@@ -1,54 +1,36 @@
1
1
  import { norm } from "@thi.ng/math/fit";
2
- import { mix, mixCubicHermite, tangentCardinal } from "@thi.ng/math/mix";
3
- import { comp } from "@thi.ng/transducers/comp";
4
- import { extendSides } from "@thi.ng/transducers/extend-sides";
5
- import { iterator } from "@thi.ng/transducers/iterator";
6
- import { map } from "@thi.ng/transducers/map";
7
- import { mapcat } from "@thi.ng/transducers/mapcat";
8
- import { normRange } from "@thi.ng/transducers/norm-range";
9
- import { partition } from "@thi.ng/transducers/partition";
10
- import { ARamp } from "./aramp.js";
11
- const hermite = (stops) => new HermiteRamp(stops);
12
- class HermiteRamp extends ARamp {
13
- at(t) {
14
- const stops = this.stops;
2
+ import { mixCubicHermite, tangentCardinal } from "@thi.ng/math/mix";
3
+ import { mixHermiteCardinal } from "@thi.ng/vectors/mix-hermite";
4
+ import { Ramp } from "./ramp.js";
5
+ const hermite = (stops, opts) => new Ramp(HERMITE_N, stops, opts);
6
+ const HERMITE_N = {
7
+ min: (acc, x) => Math.min(acc ?? Infinity, x),
8
+ max: (acc, x) => Math.max(acc ?? -Infinity, x),
9
+ at: (stops, i, t) => {
15
10
  const n = stops.length - 1;
16
- const i = this.timeIndex(t);
17
- if (i < 0) {
18
- return stops[0][1];
19
- } else if (i >= n) {
20
- return stops[n][1];
21
- } else {
22
- const a = stops[Math.max(i - 1, 0)];
23
- const [bx, by] = stops[Math.max(i, 0)];
24
- const [cx, cy] = stops[Math.min(i + 1, n)];
25
- const d = stops[Math.min(i + 2, n)];
26
- const t1 = tangentCardinal(a[1], cy, 0, a[0], cx);
27
- const t2 = tangentCardinal(by, d[1], 0, bx, d[0]);
28
- return mixCubicHermite(by, t1, cy, t2, norm(t, bx, cx));
29
- }
11
+ const a = stops[Math.max(i - 1, 0)];
12
+ const [bx, by] = stops[Math.max(i, 0)];
13
+ const [cx, cy] = stops[Math.min(i + 1, n)];
14
+ const d = stops[Math.min(i + 2, n)];
15
+ const t1 = tangentCardinal(a[1], cy, 0, a[0], cx);
16
+ const t2 = tangentCardinal(by, d[1], 0, bx, d[0]);
17
+ return mixCubicHermite(by, t1, cy, t2, norm(t, bx, cx));
30
18
  }
31
- interpolatedPoints(res = 20) {
32
- return iterator(
33
- comp(
34
- partition(4, 1),
35
- mapcat(([a, [bx, by], [cx, cy], d]) => {
36
- const t1 = tangentCardinal(a[1], cy, 0, a[0], cx);
37
- const t2 = tangentCardinal(by, d[1], 0, bx, d[0]);
38
- return map(
39
- (t) => [
40
- mix(bx, cx, t),
41
- mixCubicHermite(by, t1, cy, t2, t)
42
- ],
43
- normRange(res, false)
44
- );
45
- })
46
- ),
47
- extendSides(this.stops, 1, 2)
48
- );
19
+ };
20
+ const HERMITE_V = (vec) => ({
21
+ min: (acc, x) => vec.min(acc, acc || vec.setN([], Infinity), x),
22
+ max: (acc, x) => vec.max(acc, acc || vec.setN([], -Infinity), x),
23
+ at: (stops, i, t) => {
24
+ const n = stops.length - 1;
25
+ const [, a] = stops[Math.max(i - 1, 0)];
26
+ const [bt, b] = stops[Math.max(i, 0)];
27
+ const [ct, c] = stops[Math.min(i + 1, n)];
28
+ const [, d] = stops[Math.min(i + 2, n)];
29
+ return mixHermiteCardinal([], a, b, c, d, norm(t, bt, ct), 0);
49
30
  }
50
- }
31
+ });
51
32
  export {
52
- HermiteRamp,
33
+ HERMITE_N,
34
+ HERMITE_V,
53
35
  hermite
54
36
  };
package/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from "./api.js";
2
- export * from "./aramp.js";
2
+ export * from "./domain.js";
3
+ export * from "./group.js";
3
4
  export * from "./hermite.js";
4
5
  export * from "./linear.js";
6
+ export * from "./nested.js";
7
+ export * from "./ramp.js";
5
8
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export * from "./api.js";
2
- export * from "./aramp.js";
2
+ export * from "./domain.js";
3
+ export * from "./group.js";
3
4
  export * from "./hermite.js";
4
5
  export * from "./linear.js";
6
+ export * from "./nested.js";
7
+ export * from "./ramp.js";
package/linear.d.ts CHANGED
@@ -1,8 +1,18 @@
1
- import type { Vec } from "@thi.ng/vectors";
2
- import { ARamp } from "./aramp.js";
3
- export declare const linear: (stops?: Vec[]) => LinearRamp;
4
- export declare class LinearRamp extends ARamp {
5
- at(t: number): number;
6
- interpolatedPoints(): Vec[];
7
- }
1
+ import type { Vec, VecAPI } from "@thi.ng/vectors";
2
+ import type { Frame, RampImpl, RampOpts } from "./api.js";
3
+ import { Ramp } from "./ramp.js";
4
+ /**
5
+ * Syntax sugar for creating a numeric {@link Ramp} using the {@link LINEAR_N}
6
+ * ramp linear interpolation impl and given stops (aka keyframes, minimum 2
7
+ * required).
8
+ *
9
+ * @remarks
10
+ * For vector-valued linear ramps, use {@link ramp} with {@link LINEAR_V}.
11
+ *
12
+ * @param stops
13
+ * @param opts
14
+ */
15
+ export declare const linear: (stops: Frame<number>[], opts?: Partial<RampOpts>) => Ramp<number>;
16
+ export declare const LINEAR_N: RampImpl<number>;
17
+ export declare const LINEAR_V: <T extends Vec>(vec: VecAPI) => RampImpl<T>;
8
18
  //# sourceMappingURL=linear.d.ts.map
package/linear.js CHANGED
@@ -1,26 +1,26 @@
1
- import { fit } from "@thi.ng/math/fit";
2
- import { ARamp } from "./aramp.js";
3
- const linear = (stops) => new LinearRamp(stops);
4
- class LinearRamp extends ARamp {
5
- at(t) {
6
- const stops = this.stops;
7
- const n = stops.length - 1;
8
- const i = this.timeIndex(t);
9
- if (i < 0) {
10
- return stops[0][1];
11
- } else if (i >= n) {
12
- return stops[n][1];
13
- } else {
14
- const a = stops[i];
15
- const b = stops[i + 1];
16
- return fit(t, a[0], b[0], a[1], b[1]);
17
- }
1
+ import { fit, norm } from "@thi.ng/math/fit";
2
+ import { Ramp } from "./ramp.js";
3
+ const linear = (stops, opts) => new Ramp(LINEAR_N, stops, opts);
4
+ const LINEAR_N = {
5
+ min: (acc, x) => Math.min(acc ?? Infinity, x),
6
+ max: (acc, x) => Math.max(acc ?? -Infinity, x),
7
+ at: (stops, i, t) => {
8
+ const a = stops[i];
9
+ const b = stops[i + 1];
10
+ return fit(t, a[0], b[0], a[1], b[1]);
18
11
  }
19
- interpolatedPoints() {
20
- return this.stops;
12
+ };
13
+ const LINEAR_V = (vec) => ({
14
+ min: (acc, x) => vec.min(acc, acc || vec.setN([], Infinity), x),
15
+ max: (acc, x) => vec.max(acc, acc || vec.setN([], -Infinity), x),
16
+ at: (stops, i, t) => {
17
+ const a = stops[i];
18
+ const b = stops[i + 1];
19
+ return vec.mixN([], a[1], b[1], norm(t, a[0], b[0]));
21
20
  }
22
- }
21
+ });
23
22
  export {
24
- LinearRamp,
23
+ LINEAR_N,
24
+ LINEAR_V,
25
25
  linear
26
26
  };
package/nested.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { RampImpl } from "./api.js";
2
+ export type NestedImpl<T extends Record<string, any>> = {
3
+ [P in keyof T]: RampImpl<T[P]>;
4
+ };
5
+ /**
6
+ * Higher order ramp implementation for nested (object based) values. The given
7
+ * `children` object must specify a {@link RampImpl} for each key in the object.
8
+ *
9
+ * @remarks
10
+ * Since this is only a {@link RampImpl}, all keys in the to-be-interpolated
11
+ * objects will share the same keyframe times (specified in the parent
12
+ * {@link ramp}). If a nested ramp is desired where each child can have its own
13
+ * set of keyframes, please use {@link group} instead. Both of these concepts
14
+ * are somewhat similar but satisfy different use cases.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const example = ramp(
19
+ * // nested ramp spec w/ interpolation modes for each key
20
+ * nested({a: LINEAR_N, b: HERMITE_N }),
21
+ * // keyframes
22
+ * [
23
+ * [0, { a: 0, b: 1000 }],
24
+ * [100, { a: -10, b: 2000 }],
25
+ * ]
26
+ * )
27
+ *
28
+ * example.at(25)
29
+ * // { a: -2.5, b: 1156.25 }
30
+ * ```
31
+ *
32
+ * @param children
33
+ */
34
+ export declare const nested: <T extends Record<string, any>>(children: NestedImpl<T>) => RampImpl<T>;
35
+ //# sourceMappingURL=nested.d.ts.map
package/nested.js ADDED
@@ -0,0 +1,24 @@
1
+ const nested = (children) => {
2
+ const pairs = Object.entries(children);
3
+ return {
4
+ min: (acc, x) => pairs.reduce((acc2, [id, impl]) => {
5
+ acc2[id] = impl.min(acc2[id], x[id]);
6
+ return acc2;
7
+ }, acc || {}),
8
+ max: (acc, x) => pairs.reduce((acc2, [id, impl]) => {
9
+ acc2[id] = impl.max(acc2[id], x[id]);
10
+ return acc2;
11
+ }, acc || {}),
12
+ at: (stops, index, t) => pairs.reduce((acc, [id, impl]) => {
13
+ acc[id] = impl.at(
14
+ stops.map((x) => [x[0], x[1][id]]),
15
+ index,
16
+ t
17
+ );
18
+ return acc;
19
+ }, {})
20
+ };
21
+ };
22
+ export {
23
+ nested
24
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@thi.ng/ramp",
3
- "version": "2.1.102",
4
- "description": "Parametric (non-)linearly interpolated 1D lookup tables for remapping values",
3
+ "version": "3.0.0",
4
+ "description": "Extensible keyframe interpolation/tweening of arbitrary, nested types",
5
5
  "type": "module",
6
6
  "module": "./index.js",
7
7
  "typings": "./index.d.ts",
@@ -35,22 +35,29 @@
35
35
  "test": "bun test"
36
36
  },
37
37
  "dependencies": {
38
- "@thi.ng/arrays": "^2.7.19",
39
- "@thi.ng/compare": "^2.2.18",
40
- "@thi.ng/math": "^5.9.0",
41
- "@thi.ng/transducers": "^8.9.0",
42
- "@thi.ng/vectors": "^7.10.5"
38
+ "@thi.ng/api": "^8.9.23",
39
+ "@thi.ng/arrays": "^2.7.20",
40
+ "@thi.ng/compare": "^2.2.19",
41
+ "@thi.ng/errors": "^2.4.16",
42
+ "@thi.ng/math": "^5.9.1",
43
+ "@thi.ng/transducers": "^8.9.1",
44
+ "@thi.ng/vectors": "^7.10.6"
43
45
  },
44
46
  "devDependencies": {
45
- "@microsoft/api-extractor": "^7.39.0",
46
- "esbuild": "^0.19.10",
47
+ "@microsoft/api-extractor": "^7.40.1",
48
+ "esbuild": "^0.20.0",
47
49
  "rimraf": "^5.0.5",
48
- "typedoc": "^0.25.4",
50
+ "typedoc": "^0.25.7",
49
51
  "typescript": "^5.3.3"
50
52
  },
51
53
  "keywords": [
52
54
  "1d",
55
+ "2d",
56
+ "3d",
57
+ "4d",
58
+ "nd",
53
59
  "animation",
60
+ "cubic",
54
61
  "curve",
55
62
  "datastructure",
56
63
  "envelope",
@@ -60,8 +67,12 @@
60
67
  "keyframe",
61
68
  "linear",
62
69
  "lut",
70
+ "nested",
71
+ "object",
63
72
  "ramp",
73
+ "spline",
64
74
  "timeline",
75
+ "tween",
65
76
  "typescript"
66
77
  ],
67
78
  "publishConfig": {
@@ -81,18 +92,21 @@
81
92
  "./api": {
82
93
  "default": "./api.js"
83
94
  },
84
- "./aramp": {
85
- "default": "./aramp.js"
86
- },
87
95
  "./hermite": {
88
96
  "default": "./hermite.js"
89
97
  },
90
98
  "./linear": {
91
99
  "default": "./linear.js"
100
+ },
101
+ "./nested": {
102
+ "default": "./nested.js"
103
+ },
104
+ "./ramp": {
105
+ "default": "./ramp.js"
92
106
  }
93
107
  },
94
108
  "thi.ng": {
95
109
  "year": 2019
96
110
  },
97
- "gitHead": "ce8202c237a367c4038d41919a8acf75e1122507\n"
111
+ "gitHead": "e304d8e10f3446a22666ba75aaa2fb1d32752ae0\n"
98
112
  }
package/ramp.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { ICopy, IEmpty } from "@thi.ng/api";
2
+ import type { Frame, IRamp, RampBounds, RampDomain, RampImpl, RampOpts } from "./api.js";
3
+ /**
4
+ * Syntax sugar for {@link Ramp} constructor using the given ramp interpolation
5
+ * `impl`, keyframes `stops` (minimum 2) and options.
6
+ *
7
+ * @param impl
8
+ * @param stops
9
+ * @param opts
10
+ */
11
+ export declare const ramp: <T>(impl: RampImpl<T>, stops: Frame<T>[], opts?: Partial<RampOpts>) => Ramp<T>;
12
+ export declare class Ramp<T> implements ICopy<IRamp<T>>, IEmpty<IRamp<T>>, IRamp<T> {
13
+ impl: RampImpl<T>;
14
+ stops: Frame<T>[];
15
+ domain: RampDomain;
16
+ constructor(impl: RampImpl<T>, stops: Frame<T>[], opts?: Partial<RampOpts>);
17
+ copy(): Ramp<T>;
18
+ empty(): Ramp<T>;
19
+ /**
20
+ * Samples the ramp at given time `t` and returns interpolated value.
21
+ *
22
+ * @remarks
23
+ * The given `t` is first processed by the configured time
24
+ * {@link Ramp.domain} function.
25
+ *
26
+ * @param t
27
+ */
28
+ at(t: number): T;
29
+ samples(n?: number, start?: number, end?: number): IterableIterator<Frame<T>>;
30
+ bounds(): RampBounds<T>;
31
+ timeBounds(): [number, number];
32
+ setStopAt(t: number, val: T, eps?: number): boolean;
33
+ removeStopAt(t: number, eps?: number): boolean;
34
+ closestIndex(t: number, eps?: number): number;
35
+ clampedIndexTime(i: number, t: number, eps?: number): number;
36
+ sort(): void;
37
+ uniform(): void;
38
+ protected timeIndex(t: number): number;
39
+ }
40
+ //# sourceMappingURL=ramp.d.ts.map
package/ramp.js ADDED
@@ -0,0 +1,141 @@
1
+ import { binarySearch } from "@thi.ng/arrays/binary-search";
2
+ import { peek } from "@thi.ng/arrays/peek";
3
+ import { compareNumAsc } from "@thi.ng/compare/numeric";
4
+ import { assert } from "@thi.ng/errors/assert";
5
+ import { absDiff } from "@thi.ng/math/abs";
6
+ import { clamp } from "@thi.ng/math/interval";
7
+ import { mix } from "@thi.ng/math/mix";
8
+ import { map } from "@thi.ng/transducers/map";
9
+ import { normRange } from "@thi.ng/transducers/norm-range";
10
+ import { unconstrained } from "./domain.js";
11
+ const ramp = (impl, stops, opts) => new Ramp(impl, stops, opts);
12
+ class Ramp {
13
+ constructor(impl, stops, opts) {
14
+ this.impl = impl;
15
+ this.stops = stops;
16
+ assert(stops.length >= 2, `require at least 2 keyframes/stops`);
17
+ const $opts = { domain: unconstrained, ...opts };
18
+ this.domain = $opts.domain;
19
+ this.sort();
20
+ }
21
+ domain;
22
+ copy() {
23
+ return new Ramp(
24
+ this.impl,
25
+ this.stops.map((x) => x.slice())
26
+ );
27
+ }
28
+ empty() {
29
+ return new Ramp(this.impl, []);
30
+ }
31
+ /**
32
+ * Samples the ramp at given time `t` and returns interpolated value.
33
+ *
34
+ * @remarks
35
+ * The given `t` is first processed by the configured time
36
+ * {@link Ramp.domain} function.
37
+ *
38
+ * @param t
39
+ */
40
+ at(t) {
41
+ const { domain, impl, stops } = this;
42
+ const n = stops.length - 1;
43
+ const first = stops[0];
44
+ const last = stops[n];
45
+ t = domain(t, first[0], last[0]);
46
+ const i = this.timeIndex(t);
47
+ return i < 0 ? first[1] : i >= n ? last[1] : impl.at(stops, i, t);
48
+ }
49
+ samples(n = 100, start, end) {
50
+ if (start == void 0 || end == void 0) {
51
+ const bounds = this.timeBounds();
52
+ start = start ?? bounds[0];
53
+ end = end ?? bounds[1];
54
+ }
55
+ return map((t) => {
56
+ t = mix(start, end, t);
57
+ return [t, this.at(t)];
58
+ }, normRange(n));
59
+ }
60
+ bounds() {
61
+ const { impl, stops } = this;
62
+ const n = stops.length;
63
+ let min = null;
64
+ let max = null;
65
+ for (let i = n; i-- > 0; ) {
66
+ const val = stops[i][1];
67
+ min = impl.min(min, val);
68
+ max = impl.max(max, val);
69
+ }
70
+ return {
71
+ min,
72
+ max,
73
+ minT: stops[0][0],
74
+ maxT: stops[n - 1][0]
75
+ };
76
+ }
77
+ timeBounds() {
78
+ return [this.stops[0][0], peek(this.stops)[0]];
79
+ }
80
+ setStopAt(t, val, eps = 0.01) {
81
+ const idx = this.closestIndex(t, eps);
82
+ if (idx < 0) {
83
+ this.stops.push([t, val]);
84
+ this.sort();
85
+ return true;
86
+ }
87
+ this.stops[idx][1] = val;
88
+ return false;
89
+ }
90
+ removeStopAt(t, eps = 0.01) {
91
+ if (this.stops.length > 2) {
92
+ const i = this.closestIndex(t, eps);
93
+ if (i !== -1) {
94
+ this.stops.splice(i, 1);
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ closestIndex(t, eps = 0.01) {
101
+ const stops = this.stops;
102
+ for (let i = stops.length; i-- > 0; ) {
103
+ if (absDiff(t, stops[i][0]) < eps)
104
+ return i;
105
+ }
106
+ return -1;
107
+ }
108
+ clampedIndexTime(i, t, eps = 0.01) {
109
+ const stops = this.stops;
110
+ const n = stops.length - 1;
111
+ 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);
112
+ }
113
+ sort() {
114
+ this.stops.sort((a, b) => a[0] - b[0]);
115
+ }
116
+ uniform() {
117
+ const n = this.stops.length - 1;
118
+ this.stops.forEach((p, i) => p[0] = i / n);
119
+ }
120
+ timeIndex(t) {
121
+ const stops = this.stops;
122
+ const n = stops.length;
123
+ if (n < 256) {
124
+ for (let i = n; i-- > 0; ) {
125
+ if (t >= stops[i][0])
126
+ return i;
127
+ }
128
+ return -1;
129
+ }
130
+ return binarySearch(
131
+ stops,
132
+ [t, null],
133
+ (x) => x[0],
134
+ compareNumAsc
135
+ );
136
+ }
137
+ }
138
+ export {
139
+ Ramp,
140
+ ramp
141
+ };
package/aramp.d.ts DELETED
@@ -1,17 +0,0 @@
1
- import type { ReadonlyVec, Vec } from "@thi.ng/vectors";
2
- import type { IRamp, RampBounds } from "./api.js";
3
- export declare abstract class ARamp implements IRamp {
4
- stops: Vec[];
5
- constructor(stops?: Vec[]);
6
- abstract at(t: number): number;
7
- abstract interpolatedPoints(): Iterable<ReadonlyVec>;
8
- bounds(): RampBounds;
9
- addStopAt(t: number, y: number, eps?: number): boolean;
10
- removeStopAt(t: number, eps?: number): boolean;
11
- closestIndex(t: number, eps?: number): number;
12
- clampedIndexTime(i: number, t: number, eps?: number): number;
13
- sort(): void;
14
- uniform(): void;
15
- protected timeIndex(t: number): number;
16
- }
17
- //# sourceMappingURL=aramp.d.ts.map
package/aramp.js DELETED
@@ -1,80 +0,0 @@
1
- import { binarySearch } from "@thi.ng/arrays/binary-search";
2
- import { compareNumAsc } from "@thi.ng/compare/numeric";
3
- import { absDiff } from "@thi.ng/math/abs";
4
- import { clamp } from "@thi.ng/math/interval";
5
- class ARamp {
6
- stops;
7
- constructor(stops = [
8
- [0, 0],
9
- [1, 1]
10
- ]) {
11
- this.stops = stops;
12
- }
13
- bounds() {
14
- const stops = this.stops;
15
- const n = stops.length;
16
- if (!n)
17
- return { min: 0, max: 0, minT: 0, maxT: 0 };
18
- let min = Infinity;
19
- let max = -Infinity;
20
- for (let i = n; i-- > 0; ) {
21
- const y = stops[i][1];
22
- min = Math.min(min, y);
23
- max = Math.max(max, y);
24
- }
25
- return { min, max, minT: stops[0][0], maxT: stops[n - 1][0] };
26
- }
27
- addStopAt(t, y, eps = 0.01) {
28
- if (this.closestIndex(t, eps) !== -1) {
29
- this.stops.push([t, y]);
30
- this.sort();
31
- return true;
32
- }
33
- return false;
34
- }
35
- removeStopAt(t, eps = 0.01) {
36
- if (this.stops.length > 2) {
37
- const i = this.closestIndex(t, eps);
38
- if (i !== -1) {
39
- this.stops.splice(i, 1);
40
- return true;
41
- }
42
- }
43
- return false;
44
- }
45
- closestIndex(t, eps = 0.01) {
46
- const stops = this.stops;
47
- for (let i = stops.length; i-- > 0; ) {
48
- if (absDiff(t, stops[i][0]) < eps)
49
- return i;
50
- }
51
- return -1;
52
- }
53
- clampedIndexTime(i, t, eps = 0.01) {
54
- const stops = this.stops;
55
- const n = stops.length - 1;
56
- 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);
57
- }
58
- sort() {
59
- this.stops.sort((a, b) => a[0] - b[0]);
60
- }
61
- uniform() {
62
- const n = this.stops.length - 1;
63
- this.stops.forEach((p, i) => p[0] = i / n);
64
- }
65
- timeIndex(t) {
66
- const stops = this.stops;
67
- const n = stops.length;
68
- if (n < 256) {
69
- for (let i = n; i-- > 0; ) {
70
- if (t >= stops[i][0])
71
- return i;
72
- }
73
- return -1;
74
- }
75
- return binarySearch(stops, [t], (x) => x[0], compareNumAsc);
76
- }
77
- }
78
- export {
79
- ARamp
80
- };