pts 0.12.8 → 1.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/README.md +92 -80
- package/dist/index.d.mts +6208 -1254
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6208 -1254
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9314 -10680
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +9266 -10611
- package/dist/index.mjs.map +1 -0
- package/dist/pts.js +9446 -10777
- package/dist/pts.js.map +1 -0
- package/dist/pts.min.js +3 -5
- package/dist/pts.min.js.map +1 -0
- package/package.json +91 -31
- package/src/Canvas.ts +1642 -0
- package/src/Color.ts +1109 -0
- package/src/Create.ts +1547 -0
- package/src/Dom.ts +940 -0
- package/src/Form.ts +312 -0
- package/src/Image.ts +722 -0
- package/src/LinearAlgebra.ts +530 -0
- package/src/Num.ts +1091 -0
- package/src/Op.ts +2127 -0
- package/src/Physics.ts +1233 -0
- package/src/Play.ts +861 -0
- package/src/Pt.ts +1303 -0
- package/src/Space.ts +898 -0
- package/src/Svg.ts +1573 -0
- package/src/Types.ts +301 -0
- package/src/Typography.ts +228 -0
- package/src/UI.ts +757 -0
- package/src/Util.ts +454 -0
- package/src/_module.ts +18 -0
- package/src/_script.ts +94 -0
- package/src/_triangulate.ts +884 -0
- package/src/uheprng.ts +153 -0
package/src/Num.ts
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
/*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
|
|
2
|
+
|
|
3
|
+
import { Const, Util } from "./Util";
|
|
4
|
+
import { Curve } from "./Op";
|
|
5
|
+
import { Pt, Group } from "./Pt";
|
|
6
|
+
import { Vec, Mat } from "./LinearAlgebra";
|
|
7
|
+
import {
|
|
8
|
+
type PtLike,
|
|
9
|
+
type GroupLike,
|
|
10
|
+
type PtLikeIterable,
|
|
11
|
+
type PtIterable,
|
|
12
|
+
} from "./Types";
|
|
13
|
+
|
|
14
|
+
import generator from "./uheprng";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Num class provides static helper functions for basic numeric operations.
|
|
18
|
+
*/
|
|
19
|
+
export class Num {
|
|
20
|
+
static generator: any;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if two numbers are equal or almost equal within a threshold.
|
|
24
|
+
* @param a number a
|
|
25
|
+
* @param b number b
|
|
26
|
+
* @param threshold threshold value that specifies the minimum difference within which the two numbers are considered equal
|
|
27
|
+
*/
|
|
28
|
+
static equals(a: number, b: number, threshold = 0.00001): boolean {
|
|
29
|
+
return Math.abs(a - b) < threshold;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calculate linear interpolation between 2 values.
|
|
34
|
+
* @param a start value
|
|
35
|
+
* @param b end value
|
|
36
|
+
* @param t an interpolation value, usually between 0 to 1
|
|
37
|
+
*/
|
|
38
|
+
static lerp(a: number, b: number, t: number): number {
|
|
39
|
+
return (1 - t) * a + t * b;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Clamp values between min and max.
|
|
44
|
+
* @param val value to clamp
|
|
45
|
+
* @param min min value
|
|
46
|
+
* @param max max value
|
|
47
|
+
*/
|
|
48
|
+
static clamp(val: number, min: number, max: number): number {
|
|
49
|
+
return Math.max(min, Math.min(max, val));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Different from [`Num.clamp`](#link) in that the value out-of-bound will be "looped back" to the other end.
|
|
54
|
+
* @param val value to bound
|
|
55
|
+
* @param min min value
|
|
56
|
+
* @param max max value
|
|
57
|
+
* @example `boundValue(361, 0, 360)` will return 1
|
|
58
|
+
*/
|
|
59
|
+
static boundValue(val: number, min: number, max: number): number {
|
|
60
|
+
const len = Math.abs(max - min);
|
|
61
|
+
let a = (val - min) % len;
|
|
62
|
+
if (a < 0) a += len;
|
|
63
|
+
return a + min;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a value is within two other values
|
|
68
|
+
* @param p value to check
|
|
69
|
+
* @param a first bounding value
|
|
70
|
+
* @param b second bounding value
|
|
71
|
+
*/
|
|
72
|
+
static within(p: number, a: number, b: number): boolean {
|
|
73
|
+
return p >= Math.min(a, b) && p <= Math.max(a, b);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get a random number within a range.
|
|
78
|
+
* @param a range value 1
|
|
79
|
+
* @param b range value 2
|
|
80
|
+
*/
|
|
81
|
+
static randomRange(a: number, b: number = 0): number {
|
|
82
|
+
const r = a > b ? a - b : b - a;
|
|
83
|
+
return Math.min(a, b) + Num.random() * r;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get a random Pt within the range defined by either 1 or 2 Pt
|
|
88
|
+
* @param a the range if only one Pt is used, or the start of the range if two Pt were used
|
|
89
|
+
* @param b optional Pt to define the end of the range
|
|
90
|
+
*/
|
|
91
|
+
static randomPt(a: PtLike, b?: PtLike): Pt {
|
|
92
|
+
const p = new Pt(a.length);
|
|
93
|
+
const range = b ? Vec.subtract(b.slice(), a) : a;
|
|
94
|
+
const start = b ? a : new Pt(a.length).fill(0);
|
|
95
|
+
for (let i = 0, len = p.length; i < len; i++) {
|
|
96
|
+
p[i] = Num.random() * range[i] + start[i];
|
|
97
|
+
}
|
|
98
|
+
return p;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalize a value within a range.
|
|
103
|
+
* @param n the value to normalize
|
|
104
|
+
* @param a range value 1
|
|
105
|
+
* @param b range value 1
|
|
106
|
+
*/
|
|
107
|
+
static normalizeValue(n: number, a: number, b: number): number {
|
|
108
|
+
const min = Math.min(a, b);
|
|
109
|
+
const max = Math.max(a, b);
|
|
110
|
+
return (n - min) / (max - min);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Sum a group of numeric arrays.
|
|
115
|
+
* @param pts a Group or an Iterable<PtLike>
|
|
116
|
+
* @returns a Pt of the dimensional sums
|
|
117
|
+
*/
|
|
118
|
+
static sum(pts: PtLikeIterable): Pt {
|
|
119
|
+
const _pts = Util.iterToArray(pts);
|
|
120
|
+
const c = new Pt(_pts[0]);
|
|
121
|
+
for (let i = 1, len = _pts.length; i < len; i++) {
|
|
122
|
+
Vec.add(c, _pts[i]);
|
|
123
|
+
}
|
|
124
|
+
return c;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Average a group of numeric arrays
|
|
129
|
+
* @param pts a Group or an Iterable<PtLike>
|
|
130
|
+
* @returns a Pt of averages
|
|
131
|
+
*/
|
|
132
|
+
static average(pts: PtLikeIterable): Pt {
|
|
133
|
+
const _pts = Util.iterToArray(pts);
|
|
134
|
+
return Num.sum(_pts).divide(_pts.length);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Given a value between 0 to 1, returns a value that cycles between 0 -> 1 -> 0 using the provided shaping method.
|
|
139
|
+
* @param t a value between 0 to 1
|
|
140
|
+
* @param method a shaping method. Default to [`Shaping.sineInOut`](#link).
|
|
141
|
+
* @return a value between 0 to 1
|
|
142
|
+
*/
|
|
143
|
+
static cycle(
|
|
144
|
+
t: number,
|
|
145
|
+
method: (t: number) => number = Shaping.sineInOut,
|
|
146
|
+
): number {
|
|
147
|
+
return method(t > 0.5 ? 2 - t * 2 : t * 2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Map a value from one range to another.
|
|
152
|
+
* @param n a value in the first range
|
|
153
|
+
* @param currA first endpoint of the input range
|
|
154
|
+
* @param currB second endpoint of the input range
|
|
155
|
+
* @param targetA first endpoint of the output range
|
|
156
|
+
* @param targetB second endpoint of the output range
|
|
157
|
+
* @returns a remapped value in the second range
|
|
158
|
+
*/
|
|
159
|
+
static mapToRange(
|
|
160
|
+
n: number,
|
|
161
|
+
currA: number,
|
|
162
|
+
currB: number,
|
|
163
|
+
targetA: number,
|
|
164
|
+
targetB: number,
|
|
165
|
+
) {
|
|
166
|
+
if (currA == currB)
|
|
167
|
+
throw new Error(
|
|
168
|
+
"[currMin, currMax] must define a range that is not zero",
|
|
169
|
+
);
|
|
170
|
+
// signed normalization so that reversed source or target ranges map
|
|
171
|
+
// directionally instead of being silently reoriented
|
|
172
|
+
const t = (n - currA) / (currB - currA);
|
|
173
|
+
return targetA + t * (targetB - targetA);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Seed the pseudorandom generator for reproducible [`Num.random`](#link) sequences.
|
|
178
|
+
* The seed is hashed by its *effective* key: leading/trailing whitespace and embedded
|
|
179
|
+
* control characters are stripped first, so seeds differing only in those collide.
|
|
180
|
+
* An empty (or whitespace-only) seed yields a fixed default sequence. This generator
|
|
181
|
+
* is deterministic and statistically strong, but not cryptographically secure.
|
|
182
|
+
* @param seed seed string
|
|
183
|
+
*/
|
|
184
|
+
static seed(seed: string): void {
|
|
185
|
+
this.generator = generator(seed);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Return a random number between 0 and 1. If a seed was set via [`Num.seed`](#link),
|
|
190
|
+
* draws come from the seeded generator with 32-bit resolution (exact multiples of 2^-32);
|
|
191
|
+
* otherwise it uses `Math.random`.
|
|
192
|
+
* @returns a number between 0 and 1
|
|
193
|
+
*/
|
|
194
|
+
static random(): number {
|
|
195
|
+
return this.generator ? this.generator.random() : Math.random();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Geom class provides static helper functions for basic geometric operations.
|
|
201
|
+
*/
|
|
202
|
+
export class Geom {
|
|
203
|
+
/**
|
|
204
|
+
* Bound an angle between 0 to 360 degrees.
|
|
205
|
+
* @param angle angle value
|
|
206
|
+
*/
|
|
207
|
+
static boundAngle(angle: number): number {
|
|
208
|
+
return Num.boundValue(angle, 0, 360);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Bound a radian between 0 to two PI.
|
|
213
|
+
* @param radian radian value
|
|
214
|
+
*/
|
|
215
|
+
static boundRadian(radian: number): number {
|
|
216
|
+
return Num.boundValue(radian, 0, Const.two_pi);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Convert an angle in degree to radian.
|
|
221
|
+
* @param angle angle value
|
|
222
|
+
*/
|
|
223
|
+
static toRadian(angle: number): number {
|
|
224
|
+
return angle * Const.deg_to_rad;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Convert an angle in radian to degree.
|
|
229
|
+
* @param radian radian value
|
|
230
|
+
*/
|
|
231
|
+
static toDegree(radian: number): number {
|
|
232
|
+
return radian * Const.rad_to_deg;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get a bounding box for a set of Pts.
|
|
237
|
+
* @param pts a Group or an Iterable<Pt>
|
|
238
|
+
* @return a Group of two Pts, representing the top-left and bottom-right corners
|
|
239
|
+
*/
|
|
240
|
+
static boundingBox(pts: PtIterable): Group {
|
|
241
|
+
let minPt: Pt | undefined, maxPt: Pt | undefined;
|
|
242
|
+
for (const p of pts) {
|
|
243
|
+
if (minPt == undefined) {
|
|
244
|
+
minPt = p.clone();
|
|
245
|
+
maxPt = p.clone();
|
|
246
|
+
} else {
|
|
247
|
+
// in-place equivalent of `$min` / `$max`, without 2 clones per point
|
|
248
|
+
for (let i = 0, len = Math.min(minPt.length, p.length); i < len; i++) {
|
|
249
|
+
minPt[i] = Math.min(minPt[i], p[i]);
|
|
250
|
+
maxPt![i] = Math.max(maxPt![i], p[i]);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return new Group(minPt!, maxPt!);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get a centroid (the average middle point) for a set of Pts.
|
|
259
|
+
* @param pts a Group or an Iterable<PtLike>
|
|
260
|
+
* @return a centroid Pt
|
|
261
|
+
*/
|
|
262
|
+
static centroid(pts: PtLikeIterable): Pt {
|
|
263
|
+
return Num.average(pts);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Given an anchor Pt, rebase all Pts in this group either to or from this anchor base.
|
|
268
|
+
* @param pts a Group or an Iterable<PtLike>
|
|
269
|
+
* @param ptOrIndex an index for the Pt array, or an external Pt
|
|
270
|
+
* @param direction a string either "to" (subtract all Pt with this anchor base), or "from" (add all Pt from this anchor base)
|
|
271
|
+
*/
|
|
272
|
+
static anchor(
|
|
273
|
+
pts: PtLikeIterable,
|
|
274
|
+
ptOrIndex: PtLike | number = 0,
|
|
275
|
+
direction: "to" | "from" = "to",
|
|
276
|
+
) {
|
|
277
|
+
const method = direction == "to" ? "subtract" : "add";
|
|
278
|
+
let i = 0;
|
|
279
|
+
for (const p of pts) {
|
|
280
|
+
if (typeof ptOrIndex == "number") {
|
|
281
|
+
if (ptOrIndex !== i) (p as any)[method]((pts as any)[ptOrIndex]);
|
|
282
|
+
} else {
|
|
283
|
+
(p as any)[method](ptOrIndex);
|
|
284
|
+
}
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get an interpolated (or extrapolated) value between two Pts. For linear interpolation between 2 scalar values, use [`Num.lerp`](#link).
|
|
291
|
+
* @param a first Pt
|
|
292
|
+
* @param b second Pt
|
|
293
|
+
* @param t a value between 0 to 1 to interpolate, or any other value to extrapolate
|
|
294
|
+
* @returns interpolated point as a new Pt
|
|
295
|
+
*/
|
|
296
|
+
static interpolate(a: PtLike, b: PtLike, t: number = 0.5): Pt {
|
|
297
|
+
const len = Math.min(a.length, b.length);
|
|
298
|
+
const d = Pt.make(len);
|
|
299
|
+
for (let i = 0; i < len; i++) {
|
|
300
|
+
d[i] = a[i] * (1 - t) + b[i] * t;
|
|
301
|
+
}
|
|
302
|
+
return d;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Find two Pts that are perpendicular to this Pt (2D only).
|
|
307
|
+
* @param axis a string such as "xy" (use Const.xy) or an array to specify index for two dimensions
|
|
308
|
+
* @returns an array of two Pt that are perpendicular to this Pt
|
|
309
|
+
*/
|
|
310
|
+
static perpendicular(pt: PtLike, axis: string | PtLike = Const.xy): Group {
|
|
311
|
+
const y = axis[1];
|
|
312
|
+
const x = axis[0];
|
|
313
|
+
|
|
314
|
+
const p = new Pt(pt);
|
|
315
|
+
const pa = new Pt(p);
|
|
316
|
+
(pa as any)[x] = -(p as any)[y];
|
|
317
|
+
(pa as any)[y] = (p as any)[x];
|
|
318
|
+
const pb = new Pt(p);
|
|
319
|
+
(pb as any)[x] = (p as any)[y];
|
|
320
|
+
(pb as any)[y] = -(p as any)[x];
|
|
321
|
+
|
|
322
|
+
return new Group(pa, pb);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if two Pts are perpendicular to each other (2D only).
|
|
327
|
+
*/
|
|
328
|
+
static isPerpendicular(p1: PtLike, p2: PtLike): boolean {
|
|
329
|
+
// relative epsilon: |a·b| <= ε·|a|·|b| (zero vectors keep returning true)
|
|
330
|
+
let dot = 0;
|
|
331
|
+
let ma = 0;
|
|
332
|
+
let mb = 0;
|
|
333
|
+
for (let i = 0, len = Math.min(p1.length, p2.length); i < len; i++) {
|
|
334
|
+
dot += p1[i] * p2[i];
|
|
335
|
+
ma += p1[i] * p1[i];
|
|
336
|
+
mb += p2[i] * p2[i];
|
|
337
|
+
}
|
|
338
|
+
return Math.abs(dot) <= Const.epsilon * Math.sqrt(ma * mb);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if a Pt is within the rectangular boundary defined by two Pts.
|
|
343
|
+
* @param pt the Pt to check
|
|
344
|
+
* @param boundPt1 boundary Pt 1
|
|
345
|
+
* @param boundPt2 boundary Pt 2
|
|
346
|
+
*/
|
|
347
|
+
static withinBound(pt: PtLike, boundPt1: PtLike, boundPt2: PtLike): boolean {
|
|
348
|
+
for (
|
|
349
|
+
let i = 0, len = Math.min(pt.length, boundPt1.length, boundPt2.length);
|
|
350
|
+
i < len;
|
|
351
|
+
i++
|
|
352
|
+
) {
|
|
353
|
+
if (!Num.within(pt[i], boundPt1[i], boundPt2[i])) return false;
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Sort the Pts so that their edges will form a non-overlapping polygon. ([Reference](https://stackoverflow.com/questions/6989100/sort-points-in-clockwise-order))
|
|
360
|
+
* @param pts a Group or an Iterable<Pt>
|
|
361
|
+
*/
|
|
362
|
+
static sortEdges(pts: PtIterable): GroupLike {
|
|
363
|
+
const _pts = Util.iterToArray(pts);
|
|
364
|
+
const bounds = Geom.boundingBox(_pts);
|
|
365
|
+
const center = bounds[1].add(bounds[0]).divide(2);
|
|
366
|
+
const cx = center[0];
|
|
367
|
+
const cy = center[1];
|
|
368
|
+
|
|
369
|
+
// same ordering as before, with scalar math instead of two Pt
|
|
370
|
+
// allocations per comparison
|
|
371
|
+
const fn = (a: Pt, b: Pt): number => {
|
|
372
|
+
if (a.length < 2 || b.length < 2)
|
|
373
|
+
throw new Error("Pt dimension cannot be less than 2");
|
|
374
|
+
|
|
375
|
+
const dax = a[0] - cx;
|
|
376
|
+
const day = a[1] - cy;
|
|
377
|
+
const dbx = b[0] - cx;
|
|
378
|
+
const dby = b[1] - cy;
|
|
379
|
+
|
|
380
|
+
if (dax >= 0 && dbx < 0) return 1;
|
|
381
|
+
if (dax < 0 && dbx >= 0) return -1;
|
|
382
|
+
if (dax == 0 && dbx == 0) {
|
|
383
|
+
if (day >= 0 || dby >= 0) return day > dby ? 1 : -1;
|
|
384
|
+
return dby > day ? 1 : -1;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// cross product of vectors (center -> a) x (center -> b)
|
|
388
|
+
const det = dax * dby - day * dbx;
|
|
389
|
+
if (det < 0) return 1;
|
|
390
|
+
if (det > 0) return -1;
|
|
391
|
+
|
|
392
|
+
// points a and b are on the same line from the center
|
|
393
|
+
// check which point is closer to the center
|
|
394
|
+
return dax * dax + day * day > dbx * dbx + dby * dby ? 1 : -1;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
return _pts.sort(fn);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Scale a Pt or a Group of Pts. You may also use [`Pt.scale`](#link) instance method.
|
|
402
|
+
* @param ps either a single Pt, or a Group or an Iterable<Pt>
|
|
403
|
+
* @param scale scale value
|
|
404
|
+
* @param anchor optional anchor point to scale from
|
|
405
|
+
*/
|
|
406
|
+
static scale(
|
|
407
|
+
ps: Pt | PtIterable,
|
|
408
|
+
scale: number | PtLike,
|
|
409
|
+
anchor?: PtLike,
|
|
410
|
+
): Geom {
|
|
411
|
+
const pts = Util.iterToArray(
|
|
412
|
+
(ps as any)[0] !== undefined && typeof (ps as any)[0] == "number"
|
|
413
|
+
? [ps]
|
|
414
|
+
: ps,
|
|
415
|
+
);
|
|
416
|
+
const scs =
|
|
417
|
+
typeof scale == "number" ? Pt.make(pts[0].length, scale) : scale;
|
|
418
|
+
if (!anchor) anchor = Pt.make(pts[0].length, 0);
|
|
419
|
+
|
|
420
|
+
for (let i = 0, len = pts.length; i < len; i++) {
|
|
421
|
+
const p = pts[i];
|
|
422
|
+
for (let k = 0, lenP = p.length; k < lenP; k++) {
|
|
423
|
+
p[k] =
|
|
424
|
+
anchor && anchor[k]
|
|
425
|
+
? anchor[k] + (p[k] - anchor[k]) * scs[k]
|
|
426
|
+
: p[k] * scs[k];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return Geom;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Rotate a Pt or a Group of Pts in 2D space. You may also use [`Pt.rotate2D`](#link) instance method.
|
|
435
|
+
* @param ps either a single Pt, or a Group or an Iterable<Pt>
|
|
436
|
+
* @param angle rotate angle
|
|
437
|
+
* @param anchor optional anchor point to rotate from
|
|
438
|
+
* @param axis optional axis such as "xy" (use Const.xy) to define a 2D plane, or a number array to specify indices
|
|
439
|
+
*/
|
|
440
|
+
static rotate2D(
|
|
441
|
+
ps: Pt | PtIterable,
|
|
442
|
+
angle: number,
|
|
443
|
+
anchor?: PtLike,
|
|
444
|
+
axis?: string | PtLike,
|
|
445
|
+
): Geom {
|
|
446
|
+
const pts = Util.iterToArray(
|
|
447
|
+
(ps as any)[0] !== undefined && typeof (ps as any)[0] == "number"
|
|
448
|
+
? [ps]
|
|
449
|
+
: ps,
|
|
450
|
+
);
|
|
451
|
+
const fn = anchor ? Mat.rotateAt2DMatrix : Mat.rotate2DMatrix;
|
|
452
|
+
if (!anchor) anchor = Pt.make(pts[0].length, 0);
|
|
453
|
+
const cos = Math.cos(angle);
|
|
454
|
+
const sin = Math.sin(angle);
|
|
455
|
+
// the anchor is a fixed point of the rotation even when it aliases one
|
|
456
|
+
// of the transformed points, so the matrix can be built once
|
|
457
|
+
const mat = fn(cos, sin, anchor);
|
|
458
|
+
|
|
459
|
+
for (let i = 0, len = pts.length; i < len; i++) {
|
|
460
|
+
const p = axis ? pts[i].$take(axis) : pts[i];
|
|
461
|
+
p.to(Mat.transform2D(p, mat));
|
|
462
|
+
if (axis) {
|
|
463
|
+
for (let k = 0; k < axis.length; k++) {
|
|
464
|
+
pts[i][axis[k]] = p[k];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return Geom;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Shear a Pt or a Group of Pts in 2D space. You may also use [`Pt.shear2D`](#link) instance method.
|
|
474
|
+
* @param ps either a single Pt, or a Group or an Iterable<Pt>
|
|
475
|
+
* @param scale shearing value which can be a number or an array of 2 numbers
|
|
476
|
+
* @param anchor optional anchor point to shear from
|
|
477
|
+
* @param axis optional axis such as "xy" (use Const.xy) to define a 2D plane, or a number array to specify indices
|
|
478
|
+
*/
|
|
479
|
+
static shear2D(
|
|
480
|
+
ps: Pt | PtIterable,
|
|
481
|
+
scale: number | PtLike,
|
|
482
|
+
anchor?: PtLike,
|
|
483
|
+
axis?: string | PtLike,
|
|
484
|
+
): Geom {
|
|
485
|
+
const pts = Util.iterToArray(
|
|
486
|
+
(ps as any)[0] !== undefined && typeof (ps as any)[0] == "number"
|
|
487
|
+
? [ps]
|
|
488
|
+
: ps,
|
|
489
|
+
);
|
|
490
|
+
const s = typeof scale == "number" ? [scale, scale] : scale;
|
|
491
|
+
if (!anchor) anchor = Pt.make(pts[0].length, 0);
|
|
492
|
+
const fn = anchor ? Mat.shearAt2DMatrix : Mat.shear2DMatrix;
|
|
493
|
+
const tanx = Math.tan(s[0]);
|
|
494
|
+
const tany = Math.tan(s[1]);
|
|
495
|
+
// like rotate2D, the anchor is invariant under its own shear
|
|
496
|
+
const mat = fn(tanx, tany, anchor);
|
|
497
|
+
|
|
498
|
+
for (let i = 0, len = pts.length; i < len; i++) {
|
|
499
|
+
const p = axis ? pts[i].$take(axis) : pts[i];
|
|
500
|
+
p.to(Mat.transform2D(p, mat));
|
|
501
|
+
if (axis) {
|
|
502
|
+
for (let k = 0; k < axis.length; k++) {
|
|
503
|
+
pts[i][axis[k]] = p[k];
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return Geom;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Reflect a Pt or a Group of Pts along a 2D line. You may also use [`Pt.reflect2D`](#link) instance method.
|
|
513
|
+
* @param ps either a single Pt, or a Group or an Iterable<Pt>
|
|
514
|
+
* @param line a Group or an Iterable<PtLike> that defines a line for reflection
|
|
515
|
+
* @param axis optional axis such as "xy" (use Const.xy) to define a 2D plane, or a number array to specify indices
|
|
516
|
+
*/
|
|
517
|
+
static reflect2D(
|
|
518
|
+
ps: Pt | PtIterable,
|
|
519
|
+
line: PtLikeIterable,
|
|
520
|
+
axis?: string | PtLike,
|
|
521
|
+
): Geom {
|
|
522
|
+
const pts = Util.iterToArray(
|
|
523
|
+
(ps as any)[0] !== undefined && typeof (ps as any)[0] == "number"
|
|
524
|
+
? [ps]
|
|
525
|
+
: ps,
|
|
526
|
+
);
|
|
527
|
+
const _line = Util.iterToArray(line);
|
|
528
|
+
const mat = Mat.reflectAt2DMatrix(_line[0], _line[1]);
|
|
529
|
+
|
|
530
|
+
for (let i = 0, len = pts.length; i < len; i++) {
|
|
531
|
+
const p = axis ? pts[i].$take(axis) : pts[i];
|
|
532
|
+
p.to(Mat.transform2D(p, mat));
|
|
533
|
+
if (axis) {
|
|
534
|
+
for (let k = 0; k < axis.length; k++) {
|
|
535
|
+
pts[i][axis[k]] = p[k];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return Geom;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Generate a cosine lookup table.
|
|
545
|
+
* @returns an object with a cosine tables (array of 360 values) and a function to get cosine given a radian input.
|
|
546
|
+
*/
|
|
547
|
+
static cosTable() {
|
|
548
|
+
const cos = new Float64Array(360);
|
|
549
|
+
|
|
550
|
+
for (let i = 0; i < 360; i++) cos[i] = Math.cos((i * Math.PI) / 180);
|
|
551
|
+
const find = (rad: number) =>
|
|
552
|
+
cos[Math.floor(Geom.boundAngle(Geom.toDegree(rad)))];
|
|
553
|
+
|
|
554
|
+
return { table: cos, cos: find };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Generate a sine lookup table.
|
|
559
|
+
* @returns an object with a sine tables (array of 360 values) and a function to get sine value given a radian input.
|
|
560
|
+
*/
|
|
561
|
+
static sinTable() {
|
|
562
|
+
const sin = new Float64Array(360);
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < 360; i++) sin[i] = Math.sin((i * Math.PI) / 180);
|
|
565
|
+
const find = (rad: number) =>
|
|
566
|
+
sin[Math.floor(Geom.boundAngle(Geom.toDegree(rad)))];
|
|
567
|
+
|
|
568
|
+
return { table: sin, sin: find };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Shaping provides shaping functions to interpolate a value. These are useful for easing and transitions.
|
|
574
|
+
*/
|
|
575
|
+
export class Shaping {
|
|
576
|
+
/**
|
|
577
|
+
* Linear mapping.
|
|
578
|
+
* @param t a value between 0 to 1
|
|
579
|
+
* @param c the value to shape, default is 1
|
|
580
|
+
*/
|
|
581
|
+
static linear(t: number, c: number = 1): number {
|
|
582
|
+
return c * t;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Quadratic in, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
587
|
+
* @param t a value between 0 to 1
|
|
588
|
+
* @param c the value to shape, default is 1
|
|
589
|
+
*/
|
|
590
|
+
static quadraticIn(t: number, c: number = 1): number {
|
|
591
|
+
return c * t * t;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Quadratic out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
596
|
+
* @param t a value between 0 to 1
|
|
597
|
+
* @param c the value to shape, default is 1
|
|
598
|
+
*/
|
|
599
|
+
static quadraticOut(t: number, c: number = 1): number {
|
|
600
|
+
return -c * t * (t - 2);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Quadratic in-out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
605
|
+
* @param t a value between 0 to 1
|
|
606
|
+
* @param c the value to shape, default is 1
|
|
607
|
+
*/
|
|
608
|
+
static quadraticInOut(t: number, c: number = 1): number {
|
|
609
|
+
const dt = t * 2;
|
|
610
|
+
return t < 0.5 ? (c / 2) * t * t * 4 : (-c / 2) * ((dt - 1) * (dt - 3) - 1);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Cubic in, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
615
|
+
* @param t a value between 0 to 1
|
|
616
|
+
* @param c the value to shape, default is 1
|
|
617
|
+
*/
|
|
618
|
+
static cubicIn(t: number, c: number = 1): number {
|
|
619
|
+
return c * t * t * t;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Cubic out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
624
|
+
* @param t a value between 0 to 1
|
|
625
|
+
* @param c the value to shape, default is 1
|
|
626
|
+
*/
|
|
627
|
+
static cubicOut(t: number, c: number = 1): number {
|
|
628
|
+
const dt = t - 1;
|
|
629
|
+
return c * (dt * dt * dt + 1);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Cubic in-out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
634
|
+
* @param t a value between 0 to 1
|
|
635
|
+
* @param c the value to shape, default is 1
|
|
636
|
+
*/
|
|
637
|
+
static cubicInOut(t: number, c: number = 1): number {
|
|
638
|
+
const dt = t * 2;
|
|
639
|
+
return t < 0.5
|
|
640
|
+
? (c / 2) * dt * dt * dt
|
|
641
|
+
: (c / 2) * ((dt - 2) * (dt - 2) * (dt - 2) + 2);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Exponential ease in, adapted from Golan Levin's [polynomial shapers](http://www.flong.com/texts/code/shapers_poly/).
|
|
646
|
+
* @param t a value between 0 to 1
|
|
647
|
+
* @param c the value to shape, default is 1
|
|
648
|
+
* @param p a value between 0 to 1 to control the curve. Default is 0.25.
|
|
649
|
+
*/
|
|
650
|
+
static exponentialIn(t: number, c: number = 1, p: number = 0.25): number {
|
|
651
|
+
return c * Math.pow(t, 1 / p);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Exponential ease out, adapted from Golan Levin's [polynomial shapers](http://www.flong.com/texts/code/shapers_poly/).
|
|
656
|
+
* @param t a value between 0 to 1
|
|
657
|
+
* @param c the value to shape, default is 1
|
|
658
|
+
* @param p a value between 0 to 1 to control the curve. Default is 0.25.
|
|
659
|
+
*/
|
|
660
|
+
static exponentialOut(t: number, c: number = 1, p: number = 0.25): number {
|
|
661
|
+
return c * Math.pow(t, p);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Sinuous in, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
666
|
+
* @param t a value between 0 to 1
|
|
667
|
+
* @param c the value to shape, default is 1
|
|
668
|
+
*/
|
|
669
|
+
static sineIn(t: number, c: number = 1): number {
|
|
670
|
+
return -c * Math.cos(t * Const.half_pi) + c;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Sinuous out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
675
|
+
* @param t a value between 0 to 1
|
|
676
|
+
* @param c the value to shape, default is 1
|
|
677
|
+
*/
|
|
678
|
+
static sineOut(t: number, c: number = 1): number {
|
|
679
|
+
return c * Math.sin(t * Const.half_pi);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Sinuous in-out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
684
|
+
* @param t a value between 0 to 1
|
|
685
|
+
* @param c the value to shape, default is 1
|
|
686
|
+
*/
|
|
687
|
+
static sineInOut(t: number, c: number = 1): number {
|
|
688
|
+
return (-c / 2) * (Math.cos(Math.PI * t) - 1);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* A faster way to approximate cosine ease in-out using Blinn-Wyvill Approximation. Adapated from Golan Levin's [polynomial shaping](http://www.flong.com/texts/code/shapers_poly/).
|
|
693
|
+
* @param t a value between 0 to 1
|
|
694
|
+
* @param c the value to shape, default is 1
|
|
695
|
+
*/
|
|
696
|
+
static cosineApprox(t: number, c: number = 1) {
|
|
697
|
+
const t2 = t * t;
|
|
698
|
+
const t4 = t2 * t2;
|
|
699
|
+
const t6 = t4 * t2;
|
|
700
|
+
return c * ((4 * t6) / 9 - (17 * t4) / 9 + (22 * t2) / 9);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Circular in, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
705
|
+
* @param t a value between 0 to 1
|
|
706
|
+
* @param c the value to shape, default is 1
|
|
707
|
+
*/
|
|
708
|
+
static circularIn(t: number, c: number = 1): number {
|
|
709
|
+
return -c * (Math.sqrt(1 - t * t) - 1);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Circular out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
714
|
+
* @param t a value between 0 to 1
|
|
715
|
+
* @param c the value to shape, default is 1
|
|
716
|
+
*/
|
|
717
|
+
static circularOut(t: number, c: number = 1): number {
|
|
718
|
+
const dt = t - 1;
|
|
719
|
+
return c * Math.sqrt(1 - dt * dt);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Circular in-out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
724
|
+
* @param t a value between 0 to 1
|
|
725
|
+
* @param c the value to shape, default is 1
|
|
726
|
+
*/
|
|
727
|
+
static circularInOut(t: number, c: number = 1): number {
|
|
728
|
+
const dt = t * 2;
|
|
729
|
+
return t < 0.5
|
|
730
|
+
? (-c / 2) * (Math.sqrt(1 - dt * dt) - 1)
|
|
731
|
+
: (c / 2) * (Math.sqrt(1 - (dt - 2) * (dt - 2)) + 1);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Elastic in, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
736
|
+
* @param t a value between 0 to 1
|
|
737
|
+
* @param c the value to shape, default is 1
|
|
738
|
+
* @param p elastic parmeter between 0 to 1. The lower the number, the more elastic it will be. Default is 0.7.
|
|
739
|
+
*/
|
|
740
|
+
static elasticIn(t: number, c: number = 1, p: number = 0.7): number {
|
|
741
|
+
const dt = t - 1;
|
|
742
|
+
const s = (p / Const.two_pi) * 1.5707963267948966;
|
|
743
|
+
return (
|
|
744
|
+
c * (-Math.pow(2, 10 * dt) * Math.sin(((dt - s) * Const.two_pi) / p))
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Elastic out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
750
|
+
* @param t a value between 0 to 1
|
|
751
|
+
* @param c the value to shape, default is 1
|
|
752
|
+
* @param p elastic parmeter between 0 to 1. The lower the number, the more elastic it will be. Default is 0.7.
|
|
753
|
+
*/
|
|
754
|
+
static elasticOut(t: number, c: number = 1, p: number = 0.7): number {
|
|
755
|
+
const s = (p / Const.two_pi) * 1.5707963267948966;
|
|
756
|
+
return (
|
|
757
|
+
c * (Math.pow(2, -10 * t) * Math.sin(((t - s) * Const.two_pi) / p)) + c
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Elastic in-out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
763
|
+
* @param t a value between 0 to 1
|
|
764
|
+
* @param c the value to shape, default is 1
|
|
765
|
+
* @param p elastic parmeter between 0 to 1. The lower the number, the more elastic it will be. Default is 0.6.
|
|
766
|
+
*/
|
|
767
|
+
static elasticInOut(t: number, c: number = 1, p: number = 0.6): number {
|
|
768
|
+
let dt = t * 2;
|
|
769
|
+
const s = (p / Const.two_pi) * 1.5707963267948966;
|
|
770
|
+
if (t < 0.5) {
|
|
771
|
+
dt -= 1;
|
|
772
|
+
return (
|
|
773
|
+
c *
|
|
774
|
+
(-0.5 *
|
|
775
|
+
(Math.pow(2, 10 * dt) * Math.sin(((dt - s) * Const.two_pi) / p)))
|
|
776
|
+
);
|
|
777
|
+
} else {
|
|
778
|
+
dt -= 1;
|
|
779
|
+
return (
|
|
780
|
+
c *
|
|
781
|
+
(0.5 *
|
|
782
|
+
(Math.pow(2, -10 * dt) * Math.sin(((dt - s) * Const.two_pi) / p))) +
|
|
783
|
+
c
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Bounce in, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
790
|
+
* @param t a value between 0 to 1
|
|
791
|
+
* @param c the value to shape, default is 1
|
|
792
|
+
*/
|
|
793
|
+
static bounceIn(t: number, c: number = 1): number {
|
|
794
|
+
return c - Shaping.bounceOut(1 - t, c);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Bounce out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
799
|
+
* @param t a value between 0 to 1
|
|
800
|
+
* @param c the value to shape, default is 1
|
|
801
|
+
*/
|
|
802
|
+
static bounceOut(t: number, c: number = 1) {
|
|
803
|
+
if (t < 1 / 2.75) {
|
|
804
|
+
return c * (7.5625 * t * t);
|
|
805
|
+
} else if (t < 2 / 2.75) {
|
|
806
|
+
t -= 1.5 / 2.75;
|
|
807
|
+
return c * (7.5625 * t * t + 0.75);
|
|
808
|
+
} else if (t < 2.5 / 2.75) {
|
|
809
|
+
t -= 2.25 / 2.75;
|
|
810
|
+
return c * (7.5625 * t * t + 0.9375);
|
|
811
|
+
} else {
|
|
812
|
+
t -= 2.625 / 2.75;
|
|
813
|
+
return c * (7.5625 * t * t + 0.984375);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Bounce in-out, adapted from Robert Penner's [easing functions](http://robertpenner.com/easing/).
|
|
819
|
+
* @param t a value between 0 to 1
|
|
820
|
+
* @param c the value to shape, default is 1
|
|
821
|
+
*/
|
|
822
|
+
static bounceInOut(t: number, c: number = 1): number {
|
|
823
|
+
return t < 0.5
|
|
824
|
+
? Shaping.bounceIn(t * 2, c) / 2
|
|
825
|
+
: Shaping.bounceOut(t * 2 - 1, c) / 2 + c / 2;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Sigmoid curve changes its shape adapted from the input value, but always returns a value between 0 to 1.
|
|
830
|
+
* @param t a value between 0 to 1
|
|
831
|
+
* @param c the value to shape, default is 1
|
|
832
|
+
* @param p the larger the value, the "steeper" the curve will be. Default is 10.
|
|
833
|
+
*/
|
|
834
|
+
static sigmoid(t: number, c: number = 1, p: number = 10): number {
|
|
835
|
+
const d = p * (t - 0.5);
|
|
836
|
+
return c / (1 + Math.exp(-d));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Logistic sigmoid, adapted from Golan Levin's [shaping function](http://www.flong.com/texts/code/shapers_exp/).
|
|
841
|
+
* @param t a value between 0 to 1
|
|
842
|
+
* @param c the value to shape, default is 1
|
|
843
|
+
* @param p a parameter between 0 to 1 to control the steepness of the curve. Higher is steeper. Default is 0.7.
|
|
844
|
+
*/
|
|
845
|
+
static logSigmoid(t: number, c: number = 1, p: number = 0.7): number {
|
|
846
|
+
p = Math.max(Const.epsilon, Math.min(1 - Const.epsilon, p));
|
|
847
|
+
p = 1 / (1 - p);
|
|
848
|
+
|
|
849
|
+
const A = 1 / (1 + Math.exp((t - 0.5) * p * -2));
|
|
850
|
+
const B = 1 / (1 + Math.exp(p));
|
|
851
|
+
const C = 1 / (1 + Math.exp(-p));
|
|
852
|
+
return (c * (A - B)) / (C - B);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Exponential seat curve, adapted from Golan Levin's [shaping functions](http://www.flong.com/texts/code/shapers_exp/).
|
|
857
|
+
* @param t a value between 0 to 1
|
|
858
|
+
* @param c the value to shape, default is 1
|
|
859
|
+
* @param p a parameter between 0 to 1 to control the steepness of the curve. Higher is steeper. Default is 0.5.
|
|
860
|
+
*/
|
|
861
|
+
static seat(t: number, c: number = 1, p: number = 0.5): number {
|
|
862
|
+
if (t < 0.5) {
|
|
863
|
+
return (c * Math.pow(2 * t, 1 - p)) / 2;
|
|
864
|
+
} else {
|
|
865
|
+
return c * (1 - Math.pow(2 * (1 - t), 1 - p) / 2);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Quadratic bezier curve, adapted from Golan Levin's [shaping functions](http://www.flong.com/texts/code/shapers_exp/).
|
|
871
|
+
* @param t a value between 0 to 1
|
|
872
|
+
* @param c the value to shape, default is 1
|
|
873
|
+
* @param p a Pt object specifying the control Pt, or a value specifying its x position (its y position will default to 0.5). Default is `[0.05, 0.95]`.
|
|
874
|
+
*/
|
|
875
|
+
static quadraticBezier(
|
|
876
|
+
t: number,
|
|
877
|
+
c: number = 1,
|
|
878
|
+
p: number | PtLike = [0.05, 0.95],
|
|
879
|
+
): number {
|
|
880
|
+
const a: number = typeof p != "number" ? p[0] : p;
|
|
881
|
+
const b: number = typeof p != "number" ? p[1] : 0.5;
|
|
882
|
+
let om2a = 1 - 2 * a;
|
|
883
|
+
if (om2a === 0) {
|
|
884
|
+
om2a = Const.epsilon;
|
|
885
|
+
}
|
|
886
|
+
const d = (Math.sqrt(a * a + om2a * t) - a) / om2a;
|
|
887
|
+
return c * ((1 - 2 * b) * (d * d) + 2 * b * d);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Cubic bezier curve. This reuses the bezier functions in Curve class. Note that `t` is the curve parameter, not the x position: unlike CSS `cubic-bezier(...)`, this returns the curve's y value at parameter `t` rather than solving y at x = t.
|
|
892
|
+
* @param t a value between 0 to 1
|
|
893
|
+
* @param c the value to shape, default is 1
|
|
894
|
+
* @param p1` a Pt object specifying the first control Pt. Default is `Pt(0.1, 0.7).
|
|
895
|
+
* @param p2` a Pt object specifying the second control Pt. Default is `Pt(0.9, 0.2).
|
|
896
|
+
*/
|
|
897
|
+
static cubicBezier(
|
|
898
|
+
t: number,
|
|
899
|
+
c: number = 1,
|
|
900
|
+
p1: PtLike = [0.1, 0.7],
|
|
901
|
+
p2: PtLike = [0.9, 0.2],
|
|
902
|
+
): number {
|
|
903
|
+
const curve = new Group(new Pt(0, 0), new Pt(p1), new Pt(p2), new Pt(1, 1));
|
|
904
|
+
return (
|
|
905
|
+
c *
|
|
906
|
+
Curve.bezierStep(
|
|
907
|
+
new Pt(t * t * t, t * t, t, 1),
|
|
908
|
+
Curve.controlPoints(curve),
|
|
909
|
+
).y
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Give a Pt, draw a quadratic curve that will pass through that Pt as closely as possible. Adapted from Golan Levin's [shaping functions](http://www.flong.com/texts/code/shapers_poly/).
|
|
915
|
+
* @param t a value between 0 to 1
|
|
916
|
+
* @param c the value to shape, default is 1
|
|
917
|
+
* @param p1` a Pt object specifying the Pt to pass through. Default is `Pt(0.2, 0.35)
|
|
918
|
+
*/
|
|
919
|
+
static quadraticTarget(
|
|
920
|
+
t: number,
|
|
921
|
+
c: number = 1,
|
|
922
|
+
p1: PtLike = [0.2, 0.35],
|
|
923
|
+
): number {
|
|
924
|
+
const a = Math.min(1 - Const.epsilon, Math.max(Const.epsilon, p1[0]));
|
|
925
|
+
const b = Math.min(1, Math.max(0, p1[1]));
|
|
926
|
+
const A = (1 - b) / (1 - a) - b / a;
|
|
927
|
+
const B = (A * (a * a) - b) / a;
|
|
928
|
+
const y = A * (t * t) - B * t;
|
|
929
|
+
return c * Math.min(1, Math.max(0, y));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Step function is a simple jump from 0 to 1 at a specific Pt in time.
|
|
934
|
+
* @param t a value between 0 to 1
|
|
935
|
+
* @param c the value to shape, default is 1
|
|
936
|
+
* @param p usually a value between 0 to 1, which specify the Pt to "jump". Default is 0.5 which is in the middle.
|
|
937
|
+
*/
|
|
938
|
+
static cliff(t: number, c: number = 1, p: number = 0.5): number {
|
|
939
|
+
return t > p ? c : 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Convert any shaping functions into a series of steps.
|
|
944
|
+
* @param fn the original shaping function
|
|
945
|
+
* @param steps the number of steps
|
|
946
|
+
* @param t a value between 0 to 1
|
|
947
|
+
* @param c the value to shape, default is 1
|
|
948
|
+
* @param args optional paramters to pass to original function
|
|
949
|
+
*/
|
|
950
|
+
static step(
|
|
951
|
+
fn: (t: number, c: number, ...args: any[]) => number,
|
|
952
|
+
steps: number,
|
|
953
|
+
t: number,
|
|
954
|
+
c: number,
|
|
955
|
+
...args: any[]
|
|
956
|
+
) {
|
|
957
|
+
const s = 1 / steps;
|
|
958
|
+
const tt = Math.floor(t / s) * s;
|
|
959
|
+
return fn(tt, c, ...args);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Range object keeps track of a Group of n-dimensional Pts to provide its minimum, maximum, and magnitude in each dimension.
|
|
965
|
+
* It also provides convenient functions such as mapping the Group to another range. This class may be useful for visualizing data in charts.
|
|
966
|
+
*/
|
|
967
|
+
export class Range {
|
|
968
|
+
protected _source: Group;
|
|
969
|
+
protected _max!: Pt;
|
|
970
|
+
protected _min!: Pt;
|
|
971
|
+
protected _mag!: Pt;
|
|
972
|
+
protected _dims: number = 0;
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Construct a Range instance for a Group of Pts.
|
|
976
|
+
* @param g a Group or an Iterable<Pt>
|
|
977
|
+
*/
|
|
978
|
+
constructor(g: PtIterable) {
|
|
979
|
+
this._source = Group.fromPtArray(g);
|
|
980
|
+
this.calc();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Get this Range's maximum values per dimension.
|
|
985
|
+
*/
|
|
986
|
+
get max(): Pt {
|
|
987
|
+
return this._max.clone();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Get this Range's minimum values per dimension.
|
|
992
|
+
*/
|
|
993
|
+
get min(): Pt {
|
|
994
|
+
return this._min.clone();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Get this Range's magnitude in each dimension.
|
|
999
|
+
*/
|
|
1000
|
+
get magnitude(): Pt {
|
|
1001
|
+
return this._mag.clone();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Go through the group and find its min and max values. Usually you don't need to call this function directly.
|
|
1006
|
+
*/
|
|
1007
|
+
calc(): this | undefined {
|
|
1008
|
+
if (!this._source) return;
|
|
1009
|
+
const dims = this._source[0].length;
|
|
1010
|
+
this._dims = dims;
|
|
1011
|
+
const max = new Pt(dims);
|
|
1012
|
+
const min = new Pt(dims);
|
|
1013
|
+
const mag = new Pt(dims);
|
|
1014
|
+
|
|
1015
|
+
for (let i = 0; i < dims; i++) {
|
|
1016
|
+
// Infinity, not MAX_VALUE/MIN_VALUE: MIN_VALUE is the smallest positive
|
|
1017
|
+
// double (flushes to 0 in the Float32 Pt), which would cap the maximum
|
|
1018
|
+
// of all-negative data at 0
|
|
1019
|
+
max[i] = -Infinity;
|
|
1020
|
+
min[i] = Infinity;
|
|
1021
|
+
mag[i] = 0;
|
|
1022
|
+
|
|
1023
|
+
const s = this._source.zipSlice(i);
|
|
1024
|
+
for (let k = 0, len = s.length; k < len; k++) {
|
|
1025
|
+
max[i] = Math.max(max[i], s[k]);
|
|
1026
|
+
min[i] = Math.min(min[i], s[k]);
|
|
1027
|
+
mag[i] = max[i] - min[i];
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
this._max = max;
|
|
1032
|
+
this._min = min;
|
|
1033
|
+
this._mag = mag;
|
|
1034
|
+
return this;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Map this Range to another range of values.
|
|
1039
|
+
* @param min target range's minimum value
|
|
1040
|
+
* @param max target range's maximum value
|
|
1041
|
+
* @param exclude Optional boolean array where `true` means excluding the conversion in that specific dimension.
|
|
1042
|
+
*/
|
|
1043
|
+
mapTo(min: number, max: number, exclude?: boolean[]): Group {
|
|
1044
|
+
const target = new Group();
|
|
1045
|
+
for (let i = 0, len = this._source.length; i < len; i++) {
|
|
1046
|
+
const g = this._source[i];
|
|
1047
|
+
const n = new Pt(this._dims);
|
|
1048
|
+
for (let k = 0; k < this._dims; k++) {
|
|
1049
|
+
n[k] =
|
|
1050
|
+
exclude && exclude[k]
|
|
1051
|
+
? g[k]
|
|
1052
|
+
: Num.mapToRange(g[k], this._min[k], this._max[k], min, max);
|
|
1053
|
+
}
|
|
1054
|
+
target.push(n);
|
|
1055
|
+
}
|
|
1056
|
+
return target;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Add more Pts to this Range and recalculate its min and max values.
|
|
1061
|
+
* @param pts a Group or an Iterable<PtLike> to append to this Range
|
|
1062
|
+
* @param update Optional. Set the parameter to `false` if you want to append without immediately updating this Range's min and max values. Default is `true`.
|
|
1063
|
+
*/
|
|
1064
|
+
append(pts: PtLikeIterable, update: boolean = true): this {
|
|
1065
|
+
const _pts = Util.iterToArray(pts);
|
|
1066
|
+
if (_pts[0].length !== this._dims)
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`Dimensions don't match. ${this._dims} dimensions in Range and ${_pts[0].length} provided in parameter. `,
|
|
1069
|
+
);
|
|
1070
|
+
this._source = this._source.concat(_pts) as Group;
|
|
1071
|
+
if (update) this.calc();
|
|
1072
|
+
return this;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Create a number of evenly spaced "ticks" that span this Range's min and max value.
|
|
1077
|
+
* @param count number of subdivision. For example, 10 subdivision will return 11 tick values, which include first(min) and last(max) values.
|
|
1078
|
+
*/
|
|
1079
|
+
ticks(count: number): Group {
|
|
1080
|
+
const g = new Group();
|
|
1081
|
+
for (let i = 0; i <= count; i++) {
|
|
1082
|
+
const p = new Pt(this._dims);
|
|
1083
|
+
const t = count > 0 ? i / count : 0;
|
|
1084
|
+
for (let k = 0, len = this._max.length; k < len; k++) {
|
|
1085
|
+
p[k] = Num.lerp(this._min[k], this._max[k], t);
|
|
1086
|
+
}
|
|
1087
|
+
g.push(p);
|
|
1088
|
+
}
|
|
1089
|
+
return g;
|
|
1090
|
+
}
|
|
1091
|
+
}
|