@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/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
+ }