@khanacademy/kmath 0.0.3
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/README.md +465 -0
- package/dist/es/index.js +2 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.flow +2 -0
- package/dist/index.js.map +1 -0
- package/khanacademy-kmath-v0.0.3.tgz +0 -0
- package/logo.svg +1 -0
- package/package.json +24 -0
- package/src/__tests__/line.test.js +119 -0
- package/src/__tests__/number.test.js +119 -0
- package/src/__tests__/point.test.js +50 -0
- package/src/__tests__/vector.test.js +113 -0
- package/src/index.js +7 -0
- package/src/line.js +46 -0
- package/src/logo.js +41 -0
- package/src/number.js +104 -0
- package/src/point.js +108 -0
- package/src/ray.js +25 -0
- package/src/vector.js +267 -0
package/src/point.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Point Utils
|
|
4
|
+
* A point is an array of two numbers e.g. [0, 0].
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import _ from "underscore";
|
|
8
|
+
|
|
9
|
+
import * as kvector from "./vector.js";
|
|
10
|
+
import * as knumber from "./number.js";
|
|
11
|
+
|
|
12
|
+
// A point, in 2D, 3D, or nD space.
|
|
13
|
+
export type Point = $ReadOnlyArray<number>;
|
|
14
|
+
|
|
15
|
+
// Rotate point (around origin unless a center is specified)
|
|
16
|
+
export function rotateRad(point: Point, theta: number, center: Point): Point {
|
|
17
|
+
if (center === undefined) {
|
|
18
|
+
return kvector.rotateRad(point, theta);
|
|
19
|
+
} else {
|
|
20
|
+
return kvector.add(
|
|
21
|
+
center,
|
|
22
|
+
kvector.rotateRad(kvector.subtract(point, center), theta),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rotateDeg(point: Point, theta: number, center: Point): Point {
|
|
28
|
+
if (center === undefined) {
|
|
29
|
+
return kvector.rotateDeg(point, theta);
|
|
30
|
+
} else {
|
|
31
|
+
return kvector.add(
|
|
32
|
+
center,
|
|
33
|
+
kvector.rotateDeg(kvector.subtract(point, center), theta),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Distance between two points
|
|
39
|
+
export function distanceToPoint(point1: Point, point2: Point): number {
|
|
40
|
+
return kvector.length(kvector.subtract(point1, point2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Distance between point and line
|
|
44
|
+
export function distanceToLine(point: Point, line: [Point, Point]): number {
|
|
45
|
+
const lv = kvector.subtract(line[1], line[0]);
|
|
46
|
+
const pv = kvector.subtract(point, line[0]);
|
|
47
|
+
const projectedPv = kvector.projection(pv, lv);
|
|
48
|
+
const distancePv = kvector.subtract(projectedPv, pv);
|
|
49
|
+
return kvector.length(distancePv);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Reflect point over line
|
|
53
|
+
export function reflectOverLine(
|
|
54
|
+
point: Point,
|
|
55
|
+
line: [Point, Point],
|
|
56
|
+
): $ReadOnlyArray<number> /* TODO: convert to Point */ {
|
|
57
|
+
const lv = kvector.subtract(line[1], line[0]);
|
|
58
|
+
const pv = kvector.subtract(point, line[0]);
|
|
59
|
+
const projectedPv = kvector.projection(pv, lv);
|
|
60
|
+
const reflectedPv = kvector.subtract(kvector.scale(projectedPv, 2), pv);
|
|
61
|
+
return kvector.add(line[0], reflectedPv);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compares two points, returning -1, 0, or 1, for use with
|
|
66
|
+
* Array.prototype.sort
|
|
67
|
+
*
|
|
68
|
+
* Note: This technically doesn't satisfy the total-ordering
|
|
69
|
+
* requirements of Array.prototype.sort unless equalityTolerance
|
|
70
|
+
* is 0. In some cases very close points that compare within a
|
|
71
|
+
* few equalityTolerances could appear in the wrong order.
|
|
72
|
+
*/
|
|
73
|
+
export function compare(
|
|
74
|
+
point1: Point,
|
|
75
|
+
point2: Point,
|
|
76
|
+
equalityTolerance?: number,
|
|
77
|
+
): number /* TODO: convert to -1 | 0 | 1 type */ {
|
|
78
|
+
if (point1.length !== point2.length) {
|
|
79
|
+
return point1.length - point2.length;
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i < point1.length; i++) {
|
|
82
|
+
if (!knumber.equal(point1[i], point2[i], equalityTolerance)) {
|
|
83
|
+
return point1[i] - point2[i];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if a value is a point
|
|
90
|
+
export const is = kvector.is;
|
|
91
|
+
|
|
92
|
+
// Add and subtract vector(s)
|
|
93
|
+
export const addVector = kvector.add;
|
|
94
|
+
export const addVectors = kvector.add;
|
|
95
|
+
export const subtractVector = kvector.subtract;
|
|
96
|
+
export const equal = kvector.equal;
|
|
97
|
+
|
|
98
|
+
// Convert from cartesian to polar and back
|
|
99
|
+
export const polarRadFromCart = kvector.polarRadFromCart;
|
|
100
|
+
export const polarDegFromCart = kvector.polarDegFromCart;
|
|
101
|
+
export const cartFromPolarRad = kvector.cartFromPolarRad;
|
|
102
|
+
export const cartFromPolarDeg = kvector.cartFromPolarDeg;
|
|
103
|
+
|
|
104
|
+
// Rounding
|
|
105
|
+
export const round = kvector.round;
|
|
106
|
+
export const roundTo = kvector.roundTo;
|
|
107
|
+
export const floorTo = kvector.floorTo;
|
|
108
|
+
export const ceilTo = kvector.ceilTo;
|
package/src/ray.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Ray Utils
|
|
4
|
+
* A ray (→) is an array of an endpoint and another point along the ray.
|
|
5
|
+
* For example, [[0, 0], [1, 0]] is the ray starting at the origin and
|
|
6
|
+
* traveling along the positive x-axis.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as kvector from "./vector.js";
|
|
10
|
+
import * as kpoint from "./point.js";
|
|
11
|
+
|
|
12
|
+
import type {Point} from "./point";
|
|
13
|
+
|
|
14
|
+
export type Ray = [Point, Point];
|
|
15
|
+
|
|
16
|
+
export function equal(ray1: Ray, ray2: Ray, tolerance: number): boolean {
|
|
17
|
+
// Compare the directions of the rays
|
|
18
|
+
const v1 = kvector.subtract(ray1[1], ray1[0]);
|
|
19
|
+
const v2 = kvector.subtract(ray2[1], ray2[0]);
|
|
20
|
+
|
|
21
|
+
const sameOrigin = kpoint.equal(ray1[0], ray2[0]);
|
|
22
|
+
const codirectional = kvector.codirectional(v1, v2, tolerance);
|
|
23
|
+
|
|
24
|
+
return sameOrigin && codirectional;
|
|
25
|
+
}
|
package/src/vector.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
/**
|
|
3
|
+
* Vector Utils
|
|
4
|
+
* A vector is an array of numbers e.g. [0, 3, 4].
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import _ from "underscore";
|
|
8
|
+
import * as knumber from "./number.js";
|
|
9
|
+
|
|
10
|
+
function arraySum(array: $ReadOnlyArray<number>): number {
|
|
11
|
+
return array.reduce((memo, arg) => memo + arg, 0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function arrayProduct(array: $ReadOnlyArray<number>): number {
|
|
15
|
+
return array.reduce((memo, arg) => memo * arg, 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function is<T>(vec: $ReadOnlyArray<T>, dimension: number): boolean {
|
|
19
|
+
if (!_.isArray(vec)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (dimension !== undefined && vec.length !== dimension) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return vec.every(knumber.is);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Normalize to a unit vector
|
|
29
|
+
export function normalize(v: $ReadOnlyArray<number>): $ReadOnlyArray<number> {
|
|
30
|
+
return scale(v, 1 / length(v));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Length/magnitude of a vector
|
|
34
|
+
export function length(v: $ReadOnlyArray<number>): number {
|
|
35
|
+
return Math.sqrt(dot(v, v));
|
|
36
|
+
}
|
|
37
|
+
// Dot product of two vectors
|
|
38
|
+
export function dot(
|
|
39
|
+
a: $ReadOnlyArray<number>,
|
|
40
|
+
b: $ReadOnlyArray<number>,
|
|
41
|
+
): number {
|
|
42
|
+
// $FlowFixMe[incompatible-call] underscore doesn't like $ReadOnlyArray
|
|
43
|
+
const zipped = _.zip(a, b);
|
|
44
|
+
const multiplied = zipped.map(arrayProduct);
|
|
45
|
+
return arraySum(multiplied);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* vector-add multiple [x, y] coords/vectors
|
|
49
|
+
*
|
|
50
|
+
* add([1, 2], [3, 4]) -> [4, 6]
|
|
51
|
+
*/
|
|
52
|
+
export function add(
|
|
53
|
+
...vecs: $ReadOnlyArray<$ReadOnlyArray<number>>
|
|
54
|
+
): $ReadOnlyArray<number> {
|
|
55
|
+
// $FlowFixMe[incompatible-call] underscore doesn't like $ReadOnlyArray
|
|
56
|
+
const zipped = _.zip(...vecs);
|
|
57
|
+
return zipped.map(arraySum);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function subtract(
|
|
61
|
+
v1: $ReadOnlyArray<number>,
|
|
62
|
+
v2: $ReadOnlyArray<number>,
|
|
63
|
+
): $ReadOnlyArray<number> {
|
|
64
|
+
// $FlowFixMe[incompatible-call] underscore doesn't like $ReadOnlyArray
|
|
65
|
+
return _.zip(v1, v2).map((dim) => dim[0] - dim[1]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function negate(v: $ReadOnlyArray<number>): $ReadOnlyArray<number> {
|
|
69
|
+
return v.map((x) => {
|
|
70
|
+
return -x;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Scale a vector
|
|
75
|
+
export function scale(
|
|
76
|
+
v1: $ReadOnlyArray<number>,
|
|
77
|
+
scalar: number,
|
|
78
|
+
): $ReadOnlyArray<number> {
|
|
79
|
+
return v1.map((x) => {
|
|
80
|
+
return x * scalar;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function equal(
|
|
85
|
+
v1: $ReadOnlyArray<number>,
|
|
86
|
+
v2: $ReadOnlyArray<number>,
|
|
87
|
+
tolerance?: number,
|
|
88
|
+
): boolean {
|
|
89
|
+
// _.zip will nicely deal with the lengths, going through
|
|
90
|
+
// the length of the longest vector. knumber.equal then
|
|
91
|
+
// returns false for any number compared to the undefined
|
|
92
|
+
// passed in if one of the vectors is shorter.
|
|
93
|
+
// $FlowFixMe[incompatible-call] underscore doesn't like $ReadOnlyArray
|
|
94
|
+
return _.zip(v1, v2).every((pair) =>
|
|
95
|
+
knumber.equal(pair[0], pair[1], tolerance),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function codirectional(
|
|
100
|
+
v1: $ReadOnlyArray<number>,
|
|
101
|
+
v2: $ReadOnlyArray<number>,
|
|
102
|
+
tolerance?: number,
|
|
103
|
+
): boolean {
|
|
104
|
+
// The origin is trivially codirectional with all other vectors.
|
|
105
|
+
// This gives nice semantics for codirectionality between points when
|
|
106
|
+
// comparing their difference vectors.
|
|
107
|
+
if (
|
|
108
|
+
knumber.equal(length(v1), 0, tolerance) ||
|
|
109
|
+
knumber.equal(length(v2), 0, tolerance)
|
|
110
|
+
) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
v1 = normalize(v1);
|
|
115
|
+
v2 = normalize(v2);
|
|
116
|
+
|
|
117
|
+
return equal(v1, v2, tolerance);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function collinear(
|
|
121
|
+
v1: $ReadOnlyArray<number>,
|
|
122
|
+
v2: $ReadOnlyArray<number>,
|
|
123
|
+
tolerance?: number,
|
|
124
|
+
): boolean {
|
|
125
|
+
return (
|
|
126
|
+
codirectional(v1, v2, tolerance) ||
|
|
127
|
+
codirectional(v1, negate(v2), tolerance)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Convert a cartesian coordinate into a radian polar coordinate
|
|
132
|
+
export function polarRadFromCart(
|
|
133
|
+
v: $ReadOnlyArray<number>,
|
|
134
|
+
): $ReadOnlyArray<number> {
|
|
135
|
+
const radius = length(v);
|
|
136
|
+
let theta = Math.atan2(v[1], v[0]);
|
|
137
|
+
|
|
138
|
+
// Convert angle range from [-pi, pi] to [0, 2pi]
|
|
139
|
+
if (theta < 0) {
|
|
140
|
+
theta += 2 * Math.PI;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [radius, theta];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Converts a cartesian coordinate into a degree polar coordinate
|
|
147
|
+
export function polarDegFromCart(
|
|
148
|
+
v: $ReadOnlyArray<number>,
|
|
149
|
+
): $ReadOnlyArray<number> /* TODO: convert to tuple/Point */ {
|
|
150
|
+
const polar = polarRadFromCart(v);
|
|
151
|
+
return [polar[0], (polar[1] * 180) / Math.PI];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
155
|
+
*
|
|
156
|
+
* Examples:
|
|
157
|
+
* cartFromPolarRad(5, Math.PI)
|
|
158
|
+
* cartFromPolarRad([5, Math.PI])
|
|
159
|
+
*/
|
|
160
|
+
export function cartFromPolarRad(
|
|
161
|
+
radius: number,
|
|
162
|
+
theta?: number = 0,
|
|
163
|
+
): $ReadOnlyArray<number> /* TODO: convert to tuple/Point */ {
|
|
164
|
+
return [radius * Math.cos(theta), radius * Math.sin(theta)];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
168
|
+
*
|
|
169
|
+
* Examples:
|
|
170
|
+
* cartFromPolarDeg(5, 30)
|
|
171
|
+
* cartFromPolarDeg([5, 30])
|
|
172
|
+
*/
|
|
173
|
+
export function cartFromPolarDeg(
|
|
174
|
+
radius: number,
|
|
175
|
+
theta?: number = 0,
|
|
176
|
+
): $ReadOnlyArray<number> {
|
|
177
|
+
return cartFromPolarRad(radius, (theta * Math.PI) / 180);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Rotate vector
|
|
181
|
+
export function rotateRad(
|
|
182
|
+
v: $ReadOnlyArray<number>,
|
|
183
|
+
theta: number,
|
|
184
|
+
): $ReadOnlyArray<number> {
|
|
185
|
+
const polar = polarRadFromCart(v);
|
|
186
|
+
const angle = polar[1] + theta;
|
|
187
|
+
return cartFromPolarRad(polar[0], angle);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function rotateDeg(
|
|
191
|
+
v: $ReadOnlyArray<number>,
|
|
192
|
+
theta: number,
|
|
193
|
+
): $ReadOnlyArray<number> {
|
|
194
|
+
const polar = polarDegFromCart(v);
|
|
195
|
+
const angle = polar[1] + theta;
|
|
196
|
+
return cartFromPolarDeg(polar[0], angle);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Angle between two vectors
|
|
200
|
+
export function angleRad(
|
|
201
|
+
v1: $ReadOnlyArray<number>,
|
|
202
|
+
v2: $ReadOnlyArray<number>,
|
|
203
|
+
): number {
|
|
204
|
+
return Math.acos(dot(v1, v2) / (length(v1) * length(v2)));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function angleDeg(
|
|
208
|
+
v1: $ReadOnlyArray<number>,
|
|
209
|
+
v2: $ReadOnlyArray<number>,
|
|
210
|
+
): number {
|
|
211
|
+
return (angleRad(v1, v2) * 180) / Math.PI;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Vector projection of v1 onto v2
|
|
215
|
+
export function projection(
|
|
216
|
+
v1: $ReadOnlyArray<number>,
|
|
217
|
+
v2: $ReadOnlyArray<number>,
|
|
218
|
+
): $ReadOnlyArray<number> {
|
|
219
|
+
const scalar = dot(v1, v2) / dot(v2, v2);
|
|
220
|
+
return scale(v2, scalar);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Round each number to a certain number of decimal places
|
|
224
|
+
export function round(
|
|
225
|
+
vec: $ReadOnlyArray<number>,
|
|
226
|
+
precision: $ReadOnlyArray<number> | number,
|
|
227
|
+
): $ReadOnlyArray<number> {
|
|
228
|
+
return vec.map((elem, i) =>
|
|
229
|
+
// $FlowFixMe[prop-missing]
|
|
230
|
+
// $FlowFixMe[incompatible-call]
|
|
231
|
+
knumber.round(elem, precision[i] || precision),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Round each number to the nearest increment
|
|
236
|
+
export function roundTo(
|
|
237
|
+
vec: $ReadOnlyArray<number>,
|
|
238
|
+
increment: $ReadOnlyArray<number> | number,
|
|
239
|
+
): $ReadOnlyArray<number> {
|
|
240
|
+
return vec.map((elem, i) =>
|
|
241
|
+
// $FlowFixMe[prop-missing]
|
|
242
|
+
// $FlowFixMe[incompatible-call]
|
|
243
|
+
knumber.roundTo(elem, increment[i] || increment),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function floorTo(
|
|
248
|
+
vec: $ReadOnlyArray<number>,
|
|
249
|
+
increment: $ReadOnlyArray<number> | number,
|
|
250
|
+
): $ReadOnlyArray<number> {
|
|
251
|
+
return vec.map((elem, i) =>
|
|
252
|
+
// $FlowFixMe[prop-missing]
|
|
253
|
+
// $FlowFixMe[incompatible-call]
|
|
254
|
+
knumber.floorTo(elem, increment[i] || increment),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function ceilTo(
|
|
259
|
+
vec: $ReadOnlyArray<number>,
|
|
260
|
+
increment: $ReadOnlyArray<number> | number,
|
|
261
|
+
): $ReadOnlyArray<number> {
|
|
262
|
+
return vec.map((elem, i) =>
|
|
263
|
+
// $FlowFixMe[prop-missing]
|
|
264
|
+
// $FlowFixMe[incompatible-call]
|
|
265
|
+
knumber.ceilTo(elem, increment[i] || increment),
|
|
266
|
+
);
|
|
267
|
+
}
|