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.
@@ -0,0 +1,530 @@
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 { Pt, Group } from "./Pt";
4
+ import { Line } from "./Op";
5
+ import { type PtLike, type GroupLike } from "./Types";
6
+
7
+ /**
8
+ * Vec provides various static functions for vector operations. It's not fully optimized but good enough to use.
9
+ */
10
+ export class Vec {
11
+ /**
12
+ * Add `b` to vector `a`. Unlike `multiply`/`divide`, a shorter `b` is tolerated: missing (or NaN) dimensions are treated as 0.
13
+ * @returns vector `a`
14
+ */
15
+ static add(a: PtLike, b: PtLike | number): PtLike {
16
+ if (typeof b == "number") {
17
+ for (let i = 0, len = a.length; i < len; i++) a[i] += b;
18
+ } else {
19
+ for (let i = 0, len = a.length; i < len; i++) a[i] += b[i] || 0;
20
+ }
21
+ return a;
22
+ }
23
+
24
+ /**
25
+ * Subtract `b` from vector `a`. Unlike `multiply`/`divide`, a shorter `b` is tolerated: missing (or NaN) dimensions are treated as 0.
26
+ * @returns vector `a`
27
+ */
28
+ static subtract(a: PtLike, b: PtLike | number): PtLike {
29
+ if (typeof b == "number") {
30
+ for (let i = 0, len = a.length; i < len; i++) a[i] -= b;
31
+ } else {
32
+ for (let i = 0, len = a.length; i < len; i++) a[i] -= b[i] || 0;
33
+ }
34
+ return a;
35
+ }
36
+
37
+ /**
38
+ * Multiply `b` with vector `a`.
39
+ * @returns vector `a`
40
+ */
41
+ static multiply(a: PtLike, b: PtLike | number): PtLike {
42
+ if (typeof b == "number") {
43
+ for (let i = 0, len = a.length; i < len; i++) a[i] *= b;
44
+ } else {
45
+ if (a.length != b.length) {
46
+ throw new Error(
47
+ `Cannot do element-wise multiply since the array lengths don't match: ${a.toString()} multiply-with ${b.toString()}`,
48
+ );
49
+ }
50
+ for (let i = 0, len = a.length; i < len; i++) a[i] *= b[i];
51
+ }
52
+ return a;
53
+ }
54
+
55
+ /**
56
+ * Divide `a` over `b`.
57
+ * @returns vector `a`
58
+ */
59
+ static divide(a: PtLike, b: PtLike | number): PtLike {
60
+ if (typeof b == "number") {
61
+ if (b === 0) throw new Error("Cannot divide by zero");
62
+ for (let i = 0, len = a.length; i < len; i++) a[i] /= b;
63
+ } else {
64
+ if (a.length != b.length) {
65
+ throw new Error(
66
+ `Cannot do element-wise divide since the array lengths don't match. ${a.toString()} divide-by ${b.toString()}`,
67
+ );
68
+ }
69
+ for (let i = 0, len = a.length; i < len; i++) a[i] /= b[i];
70
+ }
71
+ return a;
72
+ }
73
+
74
+ /**
75
+ * Dot product of `a` and `b`.
76
+ */
77
+ static dot(a: PtLike, b: PtLike): number {
78
+ if (a.length != b.length) throw new Error("Array lengths don't match");
79
+ let d = 0;
80
+ for (let i = 0, len = a.length; i < len; i++) {
81
+ d += a[i] * b[i];
82
+ }
83
+ return d;
84
+ }
85
+
86
+ /**
87
+ * 2D cross product of `a` and `b`.
88
+ */
89
+ static cross2D(a: PtLike, b: PtLike): number {
90
+ return a[0] * b[1] - a[1] * b[0];
91
+ }
92
+
93
+ /**
94
+ * 3D Cross product of `a` and `b`.
95
+ */
96
+ static cross(a: PtLike, b: PtLike): Pt {
97
+ return new Pt(
98
+ a[1] * b[2] - a[2] * b[1],
99
+ a[2] * b[0] - a[0] * b[2],
100
+ a[0] * b[1] - a[1] * b[0],
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Magnitude of `a`.
106
+ */
107
+ static magnitude(a: PtLike): number {
108
+ return Math.sqrt(Vec.dot(a, a));
109
+ }
110
+
111
+ /**
112
+ * Unit vector of `a`. If magnitude of `a` is already known, pass it in the second paramter to optimize calculation.
113
+ */
114
+ static unit(a: PtLike, magnitude: number | undefined = undefined): PtLike {
115
+ const m = magnitude === undefined ? Vec.magnitude(a) : magnitude;
116
+ if (m === 0) return a; // zero vector: values are already zeros
117
+ return Vec.divide(a, m);
118
+ }
119
+
120
+ /**
121
+ * Set `a` to its absolute value in each dimension.
122
+ * @returns vector `a`
123
+ */
124
+ static abs(a: PtLike): PtLike {
125
+ return Vec.map(a, Math.abs);
126
+ }
127
+
128
+ /**
129
+ * Set `a` to its floor value in each dimension.
130
+ * @returns vector `a`
131
+ */
132
+ static floor(a: PtLike): PtLike {
133
+ return Vec.map(a, Math.floor);
134
+ }
135
+
136
+ /**
137
+ * Set `a` to its ceiling value in each dimension.
138
+ * @returns vector `a`
139
+ */
140
+ static ceil(a: PtLike): PtLike {
141
+ return Vec.map(a, Math.ceil);
142
+ }
143
+
144
+ /**
145
+ * Set `a` to its rounded value in each dimension.
146
+ * @returns vector `a`
147
+ */
148
+ static round(a: PtLike): PtLike {
149
+ return Vec.map(a, Math.round);
150
+ }
151
+
152
+ /**
153
+ * Find the max value within a vector's dimensions.
154
+ * @returns an object with `value` and `index` that specifies the max value and its corresponding dimension.
155
+ */
156
+ static max(a: PtLike): { value: number; index: number } {
157
+ // -Infinity, not Number.MIN_VALUE (the smallest positive double), so
158
+ // all-negative vectors report a correct maximum
159
+ let m = -Infinity;
160
+ let index = 0;
161
+ for (let i = 0, len = a.length; i < len; i++) {
162
+ if (a[i] >= m) {
163
+ m = a[i];
164
+ index = i;
165
+ }
166
+ }
167
+ return { value: m, index: index };
168
+ }
169
+
170
+ /**
171
+ * Find the min value within a vector's dimensions.
172
+ * @returns an object with `value` and `index` that specifies the min value and its corresponding dimension.
173
+ */
174
+ static min(a: PtLike): { value: number; index: number } {
175
+ let m = Infinity;
176
+ let index = 0;
177
+ for (let i = 0, len = a.length; i < len; i++) {
178
+ if (a[i] <= m) {
179
+ m = a[i];
180
+ index = i;
181
+ }
182
+ }
183
+ return { value: m, index: index };
184
+ }
185
+
186
+ /**
187
+ * Add up all the dimensions' values and returns a scalar of the sum.
188
+ */
189
+ static sum(a: PtLike): number {
190
+ let s = 0;
191
+ for (let i = 0, len = a.length; i < len; i++) s += a[i];
192
+ return s;
193
+ }
194
+
195
+ /**
196
+ * Given a mapping function, update `a`'s value in each dimension.
197
+ * @returns vector `a`
198
+ */
199
+ static map(
200
+ a: PtLike,
201
+ fn: (n: number, index: number, arr: PtLike) => number,
202
+ ): PtLike {
203
+ for (let i = 0, len = a.length; i < len; i++) {
204
+ a[i] = fn(a[i], i, a);
205
+ }
206
+ return a;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Mat provides various static functions for matrix operations as well as a convenient way to chain a 3x3 transformation matrix. It's not fully optimized but good enough to use.
212
+ */
213
+ export class Mat {
214
+ protected _33!: GroupLike;
215
+
216
+ constructor() {
217
+ this.reset();
218
+ }
219
+
220
+ /**
221
+ * Get the current value of its stored 3x3 matrix
222
+ */
223
+ get value(): GroupLike {
224
+ return this._33;
225
+ }
226
+
227
+ /**
228
+ * Convert the value of its stored 3x3 matrix to a 2D [`DOMMatrix`](https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix) instance
229
+ */
230
+ get domMatrix(): DOMMatrix {
231
+ return new DOMMatrix(Mat.toDOMMatrix(this._33));
232
+ }
233
+
234
+ /**
235
+ * Reset the internal 3x3 matrix to its identity
236
+ */
237
+ reset() {
238
+ this._33 = Mat.scale2DMatrix(1, 1);
239
+ }
240
+
241
+ /**
242
+ * Scale the internal 3x3 matrix. You can chain this function with other related functions.
243
+ * @param val [x, y] scale factors
244
+ * @param at Optional origin location to scale from.
245
+ */
246
+ scale2D(val: PtLike, at: PtLike = [0, 0]): this {
247
+ const m = Mat.scaleAt2DMatrix(val[0] ?? 1, val[1] ?? 1, at);
248
+ this._33 = Mat.multiply(this._33, m);
249
+ return this;
250
+ }
251
+
252
+ /**
253
+ * Scale the internal 3x3 matrix. You can chain this function with other related functions.
254
+ * @param ang Angle of rotation
255
+ * @param at Optional origin location to rotate from.
256
+ */
257
+ rotate2D(ang: number, at: PtLike = [0, 0]): this {
258
+ const m = Mat.rotateAt2DMatrix(Math.cos(ang), Math.sin(ang), at);
259
+ this._33 = Mat.multiply(this._33, m);
260
+ return this;
261
+ }
262
+
263
+ /**
264
+ * Translate the internal 3x3 matrix. You can chain this function with other related functions.
265
+ * @param val [x, y] offset values
266
+ */
267
+ translate2D(val: PtLike): this {
268
+ const m = Mat.translate2DMatrix(val[0] || 0, val[1] || 0);
269
+ this._33 = Mat.multiply(this._33, m);
270
+ return this;
271
+ }
272
+
273
+ /**
274
+ * Shear the internal 3x3 matrix. You can chain this function with other related functions.
275
+ * @param val [x, y] shear factors (before tan() operation)
276
+ * @param at Optional origin location to scale from.
277
+ */
278
+ shear2D(val: PtLike, at: PtLike = [0, 0]): this {
279
+ const m = Mat.shearAt2DMatrix(
280
+ Math.tan(val[0] ?? 0),
281
+ Math.tan(val[1] ?? 0),
282
+ at,
283
+ );
284
+ this._33 = Mat.multiply(this._33, m);
285
+ return this;
286
+ }
287
+
288
+ /**
289
+ * Matrix addition. Matrices should have the same rows and columns.
290
+ * @param a a group of Pt
291
+ * @param b a scalar number, an array of numeric arrays, or a group of Pt
292
+ * @returns a new group with the same rows and columns as a and b
293
+ */
294
+ static add(a: GroupLike, b: GroupLike | number[][] | number): Group {
295
+ if (typeof b != "number") {
296
+ if (a[0].length != b[0].length)
297
+ throw new Error(
298
+ "Cannot add matrix if rows' and columns' size don't match.",
299
+ );
300
+ if (a.length != b.length)
301
+ throw new Error(
302
+ "Cannot add matrix if rows' and columns' size don't match.",
303
+ );
304
+ }
305
+
306
+ const g = new Group();
307
+ const isNum = typeof b == "number";
308
+ for (let i = 0, len = a.length; i < len; i++) {
309
+ g.push(a[i].$add(isNum ? b : b[i]));
310
+ }
311
+
312
+ return g;
313
+ }
314
+
315
+ /**
316
+ * Matrix multiplication.
317
+ * @param a a Group of M Pts, each with K dimensions (M-rows, K-columns)
318
+ * @param b a scalar number, an array of numeric arrays, or a Group of K Pts, each with N dimensions (K-rows, N-columns) -- or if transposed is true, then N Pts with K dimensions
319
+ * @param transposed (Only applicable if it's not elementwise multiplication) If true, then a and b's columns should match (ie, each Pt should have the same dimensions). Default is `false`.
320
+ * @param elementwise if true, then the multiplication is done element-wise. Default is `false`.
321
+ * @returns If not elementwise, this will return a new group with M Pt, each with N dimensions (M-rows, N-columns).
322
+ */
323
+ static multiply(
324
+ a: GroupLike,
325
+ b: GroupLike | number[][] | number,
326
+ transposed: boolean = false,
327
+ elementwise: boolean = false,
328
+ ): Group {
329
+ const g = new Group();
330
+
331
+ if (typeof b != "number") {
332
+ if (elementwise) {
333
+ if (a.length != b.length)
334
+ throw new Error(
335
+ "Cannot multiply matrix element-wise because the matrices' sizes don't match.",
336
+ );
337
+ for (let ai = 0, alen = a.length; ai < alen; ai++) {
338
+ g.push(a[ai].$multiply(b[ai]));
339
+ }
340
+ } else {
341
+ if (!transposed && a[0].length != b.length)
342
+ throw new Error(
343
+ "Cannot multiply matrix if rows in matrix-a don't match columns in matrix-b.",
344
+ );
345
+ if (transposed && a[0].length != b[0].length)
346
+ throw new Error(
347
+ "Cannot multiply matrix if transposed and the columns in both matrices don't match.",
348
+ );
349
+
350
+ if (!transposed) b = Mat.transpose(b);
351
+
352
+ for (let ai = 0, alen = a.length; ai < alen; ai++) {
353
+ const p = Pt.make(b.length, 0);
354
+ for (let bi = 0, blen = b.length; bi < blen; bi++) {
355
+ p[bi] = Vec.dot(a[ai], b[bi]);
356
+ }
357
+ g.push(p);
358
+ }
359
+ }
360
+ } else {
361
+ for (let ai = 0, alen = a.length; ai < alen; ai++) {
362
+ g.push(a[ai].$multiply(b));
363
+ }
364
+ }
365
+
366
+ return g;
367
+ }
368
+
369
+ /**
370
+ * Zip one slice of an array of Pts. For example, if the input `g` are organized in rows, then this function will take the values in a specific column.
371
+ * @param g a group of Pt
372
+ * @param index index to zip at
373
+ * @param defaultValue a default value to fill if index out of bound. If not provided, it will throw an error instead.
374
+ */
375
+ static zipSlice(
376
+ g: GroupLike | number[][],
377
+ index: number,
378
+ defaultValue: number | boolean = false,
379
+ ): Pt {
380
+ const z: number[] = [];
381
+ for (let i = 0, len = g.length; i < len; i++) {
382
+ if (g[i].length - 1 < index) {
383
+ if (defaultValue === false)
384
+ throw new Error(`Index ${index} is out of bounds`);
385
+ z.push(defaultValue as number);
386
+ } else {
387
+ z.push(g[i][index]);
388
+ }
389
+ }
390
+ return new Pt(z);
391
+ }
392
+
393
+ /**
394
+ * Zip a group of Pt. For example, `[[1,2],[3,4],[5,6]]` will become `[[1,3,5],[2,4,6]]`.
395
+ * @param g a group of Pt
396
+ * @param defaultValue a default value to fill if index out of bound. If not provided, it will throw an error instead.
397
+ * @param useLongest If true, find the longest list of values in a Pt and use its length for zipping. Default is false, which uses the first item's length for zipping.
398
+ */
399
+ static zip(
400
+ g: GroupLike | number[][],
401
+ defaultValue: number | boolean = false,
402
+ useLongest = false,
403
+ ): Group {
404
+ const ps = new Group();
405
+ const len: number = useLongest
406
+ ? (g as Array<number[] | Pt>).reduce((a, b) => Math.max(a, b.length), 0)
407
+ : g[0].length;
408
+ for (let i = 0; i < len; i++) {
409
+ ps.push(Mat.zipSlice(g, i, defaultValue));
410
+ }
411
+ return ps;
412
+ }
413
+
414
+ /**
415
+ * Same as `zip` function.
416
+ */
417
+ static transpose(
418
+ g: GroupLike | number[][],
419
+ defaultValue: number | boolean = false,
420
+ useLongest = false,
421
+ ): Group {
422
+ return Mat.zip(g, defaultValue, useLongest);
423
+ }
424
+
425
+ static toDOMMatrix(m: GroupLike | number[][]) {
426
+ return [m[0][0], m[0][1], m[1][0], m[1][1], m[2][0], m[2][1]];
427
+ }
428
+
429
+ /**
430
+ * Transform a 2D point given a 2x3 or 3x3 matrix.
431
+ * @param pt a Pt to be transformed
432
+ * @param m 2x3 or 3x3 matrix
433
+ * @returns a new transformed Pt
434
+ */
435
+ static transform2D(pt: PtLike, m: GroupLike | number[][]): Pt {
436
+ const x = pt[0] * m[0][0] + pt[1] * m[1][0] + m[2][0];
437
+ const y = pt[0] * m[0][1] + pt[1] * m[1][1] + m[2][1];
438
+ return new Pt(x, y);
439
+ }
440
+
441
+ /**
442
+ * Get a scale matrix for use in `transform2D`.
443
+ */
444
+ static scale2DMatrix(x: number, y: number): GroupLike {
445
+ return new Group(new Pt(x, 0, 0), new Pt(0, y, 0), new Pt(0, 0, 1));
446
+ }
447
+
448
+ /**
449
+ * Get a rotate matrix for use in `transform2D`.
450
+ */
451
+ static rotate2DMatrix(cosA: number, sinA: number): GroupLike {
452
+ return new Group(
453
+ new Pt(cosA, sinA, 0),
454
+ new Pt(-sinA, cosA, 0),
455
+ new Pt(0, 0, 1),
456
+ );
457
+ }
458
+
459
+ /**
460
+ * Get a shear matrix for use in `transform2D`.
461
+ */
462
+ static shear2DMatrix(tanX: number, tanY: number): GroupLike {
463
+ return new Group(new Pt(1, tanX, 0), new Pt(tanY, 1, 0), new Pt(0, 0, 1));
464
+ }
465
+
466
+ /**
467
+ * Get a translate matrix for use in `transform2D`.
468
+ */
469
+ static translate2DMatrix(x: number, y: number): GroupLike {
470
+ return new Group(new Pt(1, 0, 0), new Pt(0, 1, 0), new Pt(x, y, 1));
471
+ }
472
+
473
+ /**
474
+ * Get a matrix to scale a point from an origin point. For use in `transform2D`.
475
+ */
476
+ static scaleAt2DMatrix(sx: number, sy: number, at: PtLike): GroupLike {
477
+ const m = Mat.scale2DMatrix(sx, sy);
478
+ m[2][0] = -at[0] * sx + at[0];
479
+ m[2][1] = -at[1] * sy + at[1];
480
+ return m;
481
+ }
482
+
483
+ /**
484
+ * Get a matrix to rotate a point from an origin point. For use in `transform2D`.
485
+ */
486
+ static rotateAt2DMatrix(cosA: number, sinA: number, at: PtLike): GroupLike {
487
+ const m = Mat.rotate2DMatrix(cosA, sinA);
488
+ m[2][0] = at[0] * (1 - cosA) + at[1] * sinA;
489
+ m[2][1] = at[1] * (1 - cosA) - at[0] * sinA;
490
+ return m;
491
+ }
492
+
493
+ /**
494
+ * Get a matrix to shear a point from an origin point. For use in `transform2D`.
495
+ */
496
+ static shearAt2DMatrix(tanX: number, tanY: number, at: PtLike): GroupLike {
497
+ const m = Mat.shear2DMatrix(tanX, tanY);
498
+ m[2][0] = -at[1] * tanY;
499
+ m[2][1] = -at[0] * tanX;
500
+ return m;
501
+ }
502
+
503
+ /**
504
+ * Get a matrix to reflect a point along a line. For use in `transform2D`.
505
+ * @param p1 first end point to define the reflection line
506
+ * @param p1 second end point to define the reflection line
507
+ */
508
+ static reflectAt2DMatrix(p1: PtLike, p2: PtLike) {
509
+ const intercept = Line.intercept(p1, p2);
510
+
511
+ if (intercept == undefined) {
512
+ return [
513
+ new Pt([-1, 0, 0]),
514
+ new Pt([0, 1, 0]),
515
+ new Pt([p1[0] + p2[0], 0, 1]),
516
+ ];
517
+ } else {
518
+ const yi = intercept.yi;
519
+ const ang2 = Math.atan(intercept.slope) * 2;
520
+ const cosA = Math.cos(ang2);
521
+ const sinA = Math.sin(ang2);
522
+
523
+ return [
524
+ new Pt([cosA, sinA, 0]),
525
+ new Pt([sinA, -cosA, 0]),
526
+ new Pt([-yi * sinA, yi + yi * cosA, 1]),
527
+ ];
528
+ }
529
+ }
530
+ }