@thi.ng/ramp 2.1.103 → 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 +31 -1
- package/README.md +138 -13
- package/api.d.ts +65 -15
- package/domain.d.ts +26 -0
- package/domain.js +11 -0
- package/group.d.ts +50 -0
- package/group.js +60 -0
- package/hermite.d.ts +24 -7
- package/hermite.js +29 -47
- package/index.d.ts +4 -1
- package/index.js +4 -1
- package/linear.d.ts +17 -7
- package/linear.js +21 -21
- package/nested.d.ts +35 -0
- package/nested.js +24 -0
- package/package.json +20 -6
- package/ramp.d.ts +40 -0
- package/ramp.js +141 -0
- package/aramp.d.ts +0 -17
- package/aramp.js +0 -80
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
- **Last updated**: 2024-02-
|
|
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
|
@@ -31,15 +31,24 @@
|
|
|
31
31
|
- [Dependencies](#dependencies)
|
|
32
32
|
- [Usage examples](#usage-examples)
|
|
33
33
|
- [API](#api)
|
|
34
|
+
- [Numeric](#numeric)
|
|
35
|
+
- [nD vectors](#nd-vectors)
|
|
36
|
+
- [Nested objects](#nested-objects)
|
|
37
|
+
- [Grouped & nested ramps](#grouped--nested-ramps)
|
|
34
38
|
- [Authors](#authors)
|
|
35
39
|
- [License](#license)
|
|
36
40
|
|
|
37
41
|
## About
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
Extensible keyframe interpolation/tweening of arbitrary, nested types.
|
|
40
44
|
|
|
41
45
|

|
|
42
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
|
+
|
|
43
52
|
## Status
|
|
44
53
|
|
|
45
54
|
**STABLE** - used in production
|
|
@@ -66,42 +75,44 @@ For Node.js REPL:
|
|
|
66
75
|
const ramp = await import("@thi.ng/ramp");
|
|
67
76
|
```
|
|
68
77
|
|
|
69
|
-
Package sizes (brotli'd, pre-treeshake): ESM: 1.
|
|
78
|
+
Package sizes (brotli'd, pre-treeshake): ESM: 1.67 KB
|
|
70
79
|
|
|
71
80
|
## Dependencies
|
|
72
81
|
|
|
82
|
+
- [@thi.ng/api](https://github.com/thi-ng/umbrella/tree/develop/packages/api)
|
|
73
83
|
- [@thi.ng/arrays](https://github.com/thi-ng/umbrella/tree/develop/packages/arrays)
|
|
74
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)
|
|
75
86
|
- [@thi.ng/math](https://github.com/thi-ng/umbrella/tree/develop/packages/math)
|
|
76
87
|
- [@thi.ng/transducers](https://github.com/thi-ng/umbrella/tree/develop/packages/transducers)
|
|
77
88
|
- [@thi.ng/vectors](https://github.com/thi-ng/umbrella/tree/develop/packages/vectors)
|
|
78
89
|
|
|
79
90
|
## Usage examples
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
Several projects in this repo's
|
|
82
93
|
[/examples](https://github.com/thi-ng/umbrella/tree/develop/examples)
|
|
83
|
-
directory
|
|
94
|
+
directory are using this package:
|
|
84
95
|
|
|
85
|
-
| Screenshot
|
|
86
|
-
|
|
87
|
-
| <img src="https://raw.githubusercontent.com/thi-ng/umbrella/develop/assets/examples/ramp-
|
|
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) |
|
|
88
100
|
|
|
89
101
|
## API
|
|
90
102
|
|
|
91
103
|
[Generated API docs](https://docs.thi.ng/umbrella/ramp/)
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
### Numeric
|
|
106
|
+
|
|
107
|
+
```ts tangle:export/readme.ts
|
|
94
108
|
import { linear, hermite } from "@thi.ng/ramp";
|
|
95
109
|
|
|
96
110
|
const rampL = linear([[0.1, 0], [0.5, 1], [0.9, 0]]);
|
|
97
111
|
const rampH = hermite([[0.1, 0], [0.5, 1], [0.9, 0]]);
|
|
98
112
|
|
|
99
113
|
for(let i = 0; i <= 10; i++) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
rampL.at(i / 10).toFixed(2),
|
|
103
|
-
rampH.at(i / 10).toFixed(2)
|
|
104
|
-
);
|
|
114
|
+
const t = i / 10;
|
|
115
|
+
console.log(t, rampL.at(t).toFixed(2), rampH.at(t).toFixed(2));
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
// 0 0.00 0.00
|
|
@@ -117,6 +128,120 @@ for(let i = 0; i <= 10; i++) {
|
|
|
117
128
|
// 1 0.00 0.00
|
|
118
129
|
```
|
|
119
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
|
+
|
|
120
245
|
## Authors
|
|
121
246
|
|
|
122
247
|
- [Karsten Schmidt](https://thi.ng)
|
package/api.d.ts
CHANGED
|
@@ -1,23 +1,73 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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 "./
|
|
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
package/linear.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import type { Vec } from "@thi.ng/vectors";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
3
|
-
const linear = (stops) => new
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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": "
|
|
4
|
-
"description": "
|
|
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,8 +35,10 @@
|
|
|
35
35
|
"test": "bun test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@thi.ng/api": "^8.9.23",
|
|
38
39
|
"@thi.ng/arrays": "^2.7.20",
|
|
39
40
|
"@thi.ng/compare": "^2.2.19",
|
|
41
|
+
"@thi.ng/errors": "^2.4.16",
|
|
40
42
|
"@thi.ng/math": "^5.9.1",
|
|
41
43
|
"@thi.ng/transducers": "^8.9.1",
|
|
42
44
|
"@thi.ng/vectors": "^7.10.6"
|
|
@@ -50,7 +52,12 @@
|
|
|
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": "
|
|
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
|
-
};
|