@khanacademy/kmath 0.0.0-PR443-20230328215601
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/.babelrc.js +8 -0
- package/.eslintrc.js +12 -0
- package/CHANGELOG.md +37 -0
- package/README.md +504 -0
- package/dist/es/index.js +499 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.js +511 -0
- package/dist/index.js.map +1 -0
- package/logo.svg +1 -0
- package/package.json +32 -0
- package/src/__tests__/line.test.ts +117 -0
- package/src/__tests__/number.test.ts +119 -0
- package/src/__tests__/point.test.ts +48 -0
- package/src/__tests__/vector.test.ts +111 -0
- package/src/index.ts +5 -0
- package/src/line.ts +42 -0
- package/src/logo.ts +39 -0
- package/src/number.ts +104 -0
- package/src/point.ts +104 -0
- package/src/ray.ts +24 -0
- package/src/vector.ts +241 -0
- package/tsconfig.json +9 -0
package/src/ray.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ray Utils
|
|
3
|
+
* A ray (→) is an array of an endpoint and another point along the ray.
|
|
4
|
+
* For example, [[0, 0], [1, 0]] is the ray starting at the origin and
|
|
5
|
+
* traveling along the positive x-axis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as kpoint from "./point";
|
|
9
|
+
import * as kvector from "./vector";
|
|
10
|
+
|
|
11
|
+
import type {Point} from "./point";
|
|
12
|
+
|
|
13
|
+
export type Ray = [Point, Point];
|
|
14
|
+
|
|
15
|
+
export function equal(ray1: Ray, ray2: Ray, tolerance?: number): boolean {
|
|
16
|
+
// Compare the directions of the rays
|
|
17
|
+
const v1 = kvector.subtract(ray1[1], ray1[0]);
|
|
18
|
+
const v2 = kvector.subtract(ray2[1], ray2[0]);
|
|
19
|
+
|
|
20
|
+
const sameOrigin = kpoint.equal(ray1[0], ray2[0]);
|
|
21
|
+
const codirectional = kvector.codirectional(v1, v2, tolerance);
|
|
22
|
+
|
|
23
|
+
return sameOrigin && codirectional;
|
|
24
|
+
}
|
package/src/vector.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector Utils
|
|
3
|
+
* A vector is an array of numbers e.g. [0, 3, 4].
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import _ from "underscore";
|
|
7
|
+
|
|
8
|
+
import * as knumber from "./number";
|
|
9
|
+
|
|
10
|
+
type Vector = ReadonlyArray<number>;
|
|
11
|
+
|
|
12
|
+
function arraySum(array: ReadonlyArray<number>): number {
|
|
13
|
+
return array.reduce((memo, arg) => memo + arg, 0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function arrayProduct(array: ReadonlyArray<number>): number {
|
|
17
|
+
return array.reduce((memo, arg) => memo * arg, 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if the given vector contains only numbers and, optionally, is of the
|
|
22
|
+
* right dimension (length).
|
|
23
|
+
*
|
|
24
|
+
* is([1, 2, 3]) -> true
|
|
25
|
+
* is([1, "Hello", 3]) -> false
|
|
26
|
+
* is([1, 2, 3], 1) -> false
|
|
27
|
+
*/
|
|
28
|
+
export function is<T>(vec: ReadonlyArray<T>, dimension?: number): boolean {
|
|
29
|
+
if (!_.isArray(vec)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (dimension !== undefined && vec.length !== dimension) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return vec.every(knumber.is);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Normalize to a unit vector
|
|
39
|
+
export function normalize<V extends Vector>(v: V): V {
|
|
40
|
+
return scale(v, 1 / length(v));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Length/magnitude of a vector
|
|
44
|
+
export function length(v: Vector): number {
|
|
45
|
+
return Math.sqrt(dot(v, v));
|
|
46
|
+
}
|
|
47
|
+
// Dot product of two vectors
|
|
48
|
+
export function dot(a: Vector, b: Vector): number {
|
|
49
|
+
const zipped = _.zip(a, b);
|
|
50
|
+
const multiplied = zipped.map(arrayProduct);
|
|
51
|
+
return arraySum(multiplied);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* vector-add multiple [x, y] coords/vectors
|
|
55
|
+
*
|
|
56
|
+
* add([1, 2], [3, 4]) -> [4, 6]
|
|
57
|
+
*/
|
|
58
|
+
export function add<V extends Vector>(...vecs: ReadonlyArray<V>): V {
|
|
59
|
+
const zipped = _.zip(...vecs);
|
|
60
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
61
|
+
return zipped.map(arraySum);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function subtract<V extends Vector>(v1: V, v2: V): V {
|
|
65
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
66
|
+
return _.zip(v1, v2).map((dim) => dim[0] - dim[1]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function negate<V extends Vector>(v: V): V {
|
|
70
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
71
|
+
return v.map((x) => {
|
|
72
|
+
return -x;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Scale a vector
|
|
77
|
+
export function scale<V extends Vector>(v1: V, scalar: number): V {
|
|
78
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
79
|
+
return v1.map((x) => {
|
|
80
|
+
return x * scalar;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function equal(v1: Vector, v2: Vector, tolerance?: number): boolean {
|
|
85
|
+
// _.zip will nicely deal with the lengths, going through
|
|
86
|
+
// the length of the longest vector. knumber.equal then
|
|
87
|
+
// returns false for any number compared to the undefined
|
|
88
|
+
// passed in if one of the vectors is shorter.
|
|
89
|
+
return _.zip(v1, v2).every((pair) =>
|
|
90
|
+
knumber.equal(pair[0], pair[1], tolerance),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function codirectional(
|
|
95
|
+
v1: Vector,
|
|
96
|
+
v2: Vector,
|
|
97
|
+
tolerance?: number,
|
|
98
|
+
): boolean {
|
|
99
|
+
// The origin is trivially codirectional with all other vectors.
|
|
100
|
+
// This gives nice semantics for codirectionality between points when
|
|
101
|
+
// comparing their difference vectors.
|
|
102
|
+
if (
|
|
103
|
+
knumber.equal(length(v1), 0, tolerance) ||
|
|
104
|
+
knumber.equal(length(v2), 0, tolerance)
|
|
105
|
+
) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
v1 = normalize(v1);
|
|
110
|
+
v2 = normalize(v2);
|
|
111
|
+
|
|
112
|
+
return equal(v1, v2, tolerance);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function collinear(v1: Vector, v2: Vector, tolerance?: number): boolean {
|
|
116
|
+
return (
|
|
117
|
+
codirectional(v1, v2, tolerance) ||
|
|
118
|
+
codirectional(v1, negate(v2), tolerance)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// TODO(jeremy) These coordinate conversion functions really only handle 2D points (ie. [number, number])
|
|
123
|
+
|
|
124
|
+
// Convert a cartesian coordinate into a radian polar coordinate
|
|
125
|
+
export function polarRadFromCart(
|
|
126
|
+
v: ReadonlyArray<number>,
|
|
127
|
+
): ReadonlyArray<number> {
|
|
128
|
+
const radius = length(v);
|
|
129
|
+
let theta = Math.atan2(v[1], v[0]);
|
|
130
|
+
|
|
131
|
+
// Convert angle range from [-pi, pi] to [0, 2pi]
|
|
132
|
+
if (theta < 0) {
|
|
133
|
+
theta += 2 * Math.PI;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [radius, theta];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Converts a cartesian coordinate into a degree polar coordinate
|
|
140
|
+
export function polarDegFromCart(
|
|
141
|
+
v: ReadonlyArray<number>,
|
|
142
|
+
): ReadonlyArray<number> /* TODO: convert to tuple/Point */ {
|
|
143
|
+
const polar = polarRadFromCart(v);
|
|
144
|
+
return [polar[0], (polar[1] * 180) / Math.PI];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
148
|
+
*
|
|
149
|
+
* Examples:
|
|
150
|
+
* cartFromPolarRad(5, Math.PI)
|
|
151
|
+
*/
|
|
152
|
+
export function cartFromPolarRad(
|
|
153
|
+
radius: number,
|
|
154
|
+
theta = 0,
|
|
155
|
+
): ReadonlyArray<number> /* TODO: convert to tuple/Point */ {
|
|
156
|
+
return [radius * Math.cos(theta), radius * Math.sin(theta)];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
160
|
+
*
|
|
161
|
+
* Examples:
|
|
162
|
+
* cartFromPolarDeg(5, 30)
|
|
163
|
+
*/
|
|
164
|
+
export function cartFromPolarDeg(
|
|
165
|
+
radius: number,
|
|
166
|
+
theta = 0,
|
|
167
|
+
): ReadonlyArray<number> {
|
|
168
|
+
return cartFromPolarRad(radius, (theta * Math.PI) / 180);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Rotate vector
|
|
172
|
+
export function rotateRad(
|
|
173
|
+
v: ReadonlyArray<number>,
|
|
174
|
+
theta: number,
|
|
175
|
+
): ReadonlyArray<number> {
|
|
176
|
+
const polar = polarRadFromCart(v);
|
|
177
|
+
const angle = polar[1] + theta;
|
|
178
|
+
return cartFromPolarRad(polar[0], angle);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function rotateDeg(
|
|
182
|
+
v: ReadonlyArray<number>,
|
|
183
|
+
theta: number,
|
|
184
|
+
): ReadonlyArray<number> {
|
|
185
|
+
const polar = polarDegFromCart(v);
|
|
186
|
+
const angle = polar[1] + theta;
|
|
187
|
+
return cartFromPolarDeg(polar[0], angle);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Angle between two vectors
|
|
191
|
+
export function angleRad(v1: Vector, v2: Vector): number {
|
|
192
|
+
return Math.acos(dot(v1, v2) / (length(v1) * length(v2)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function angleDeg(v1: Vector, v2: Vector): number {
|
|
196
|
+
return (angleRad(v1, v2) * 180) / Math.PI;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Vector projection of v1 onto v2
|
|
200
|
+
export function projection<V extends Vector>(v1: V, v2: V): V {
|
|
201
|
+
const scalar = dot(v1, v2) / dot(v2, v2);
|
|
202
|
+
return scale(v2, scalar);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Round each number to a certain number of decimal places
|
|
206
|
+
export function round<V extends Vector>(vec: V, precision: V | number): V {
|
|
207
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
208
|
+
return vec.map((elem, i) =>
|
|
209
|
+
// $FlowFixMe[prop-missing]
|
|
210
|
+
// $FlowFixMe[incompatible-call]
|
|
211
|
+
knumber.round(elem, precision[i] || precision),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Round each number to the nearest increment
|
|
216
|
+
export function roundTo<V extends Vector>(vec: V, increment: V | number): V {
|
|
217
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
218
|
+
return vec.map((elem, i) =>
|
|
219
|
+
// $FlowFixMe[prop-missing]
|
|
220
|
+
// $FlowFixMe[incompatible-call]
|
|
221
|
+
knumber.roundTo(elem, increment[i] || increment),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function floorTo<V extends Vector>(vec: V, increment: V | number): V {
|
|
226
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
227
|
+
return vec.map((elem, i) =>
|
|
228
|
+
// $FlowFixMe[prop-missing]
|
|
229
|
+
// $FlowFixMe[incompatible-call]
|
|
230
|
+
knumber.floorTo(elem, increment[i] || increment),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function ceilTo<V extends Vector>(vec: V, increment: V | number): V {
|
|
235
|
+
// @ts-expect-error [FEI-5003] - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
236
|
+
return vec.map((elem, i) =>
|
|
237
|
+
// $FlowFixMe[prop-missing]
|
|
238
|
+
// $FlowFixMe[incompatible-call]
|
|
239
|
+
knumber.ceilTo(elem, increment[i] || increment),
|
|
240
|
+
);
|
|
241
|
+
}
|