@khanacademy/kmath 1.0.0 → 2.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/dist/es/index.js +1138 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.js +375 -49
- package/dist/index.js.map +1 -1
- package/package.json +6 -13
package/dist/index.js
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var perseusUtils = require('@khanacademy/perseus-utils');
|
|
6
|
+
var _ = require('underscore');
|
|
7
|
+
var perseusCore = require('@khanacademy/perseus-core');
|
|
8
|
+
var $ = require('jquery');
|
|
9
|
+
|
|
10
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var ___default = /*#__PURE__*/_interopDefaultCompat(_);
|
|
13
|
+
var $__default = /*#__PURE__*/_interopDefaultCompat($);
|
|
14
|
+
|
|
15
|
+
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
16
|
+
// version number during the release build.
|
|
17
|
+
// In dev, you'll never see the version number.
|
|
5
18
|
|
|
6
19
|
const libName = "@khanacademy/kmath";
|
|
7
|
-
const libVersion = "
|
|
8
|
-
addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
20
|
+
const libVersion = "2.0.0";
|
|
21
|
+
perseusUtils.addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Number Utils
|
|
25
|
+
* A number is a js-number, e.g. 5.12
|
|
26
|
+
*/
|
|
9
27
|
|
|
10
28
|
const DEFAULT_TOLERANCE = 1e-9;
|
|
29
|
+
|
|
30
|
+
// TODO: Should this just be Number.Epsilon
|
|
11
31
|
const EPSILON = Math.pow(2, -42);
|
|
12
32
|
function is$2(x) {
|
|
13
|
-
return
|
|
33
|
+
return ___default.default.isNumber(x) && !___default.default.isNaN(x);
|
|
14
34
|
}
|
|
15
35
|
function equal$4(x, y, tolerance) {
|
|
36
|
+
// Checking for undefined makes this function behave nicely
|
|
37
|
+
// with vectors of different lengths that are _.zip'd together
|
|
16
38
|
if (x == null || y == null) {
|
|
17
39
|
return x === y;
|
|
18
40
|
}
|
|
41
|
+
// We check === here so that +/-Infinity comparisons work correctly
|
|
19
42
|
if (x === y) {
|
|
20
43
|
return true;
|
|
21
44
|
}
|
|
@@ -24,16 +47,21 @@ function equal$4(x, y, tolerance) {
|
|
|
24
47
|
}
|
|
25
48
|
return Math.abs(x - y) < tolerance;
|
|
26
49
|
}
|
|
27
|
-
function sign$1(x, tolerance) {
|
|
50
|
+
function sign$1(x, tolerance) /* Should be: 0 | 1 | -1 */{
|
|
28
51
|
return equal$4(x, 0, tolerance) ? 0 : Math.abs(x) / x;
|
|
29
52
|
}
|
|
30
53
|
function isInteger(num, tolerance) {
|
|
31
54
|
return equal$4(Math.round(num), num, tolerance);
|
|
32
55
|
}
|
|
56
|
+
|
|
57
|
+
// Round a number to a certain number of decimal places
|
|
33
58
|
function round$2(num, precision) {
|
|
34
59
|
const factor = Math.pow(10, precision);
|
|
35
60
|
return Math.round(num * factor) / factor;
|
|
36
61
|
}
|
|
62
|
+
|
|
63
|
+
// Round num to the nearest multiple of increment
|
|
64
|
+
// i.e. roundTo(83, 5) -> 85
|
|
37
65
|
function roundTo$2(num, increment) {
|
|
38
66
|
return Math.round(num / increment) * increment;
|
|
39
67
|
}
|
|
@@ -43,7 +71,26 @@ function floorTo$2(num, increment) {
|
|
|
43
71
|
function ceilTo$2(num, increment) {
|
|
44
72
|
return Math.ceil(num / increment) * increment;
|
|
45
73
|
}
|
|
46
|
-
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* toFraction
|
|
77
|
+
*
|
|
78
|
+
* Returns a [numerator, denominator] array rational representation
|
|
79
|
+
* of `decimal`
|
|
80
|
+
*
|
|
81
|
+
* See http://en.wikipedia.org/wiki/Continued_fraction for implementation
|
|
82
|
+
* details
|
|
83
|
+
*
|
|
84
|
+
* toFraction(4/8) => [1, 2]
|
|
85
|
+
* toFraction(0.66) => [33, 50]
|
|
86
|
+
* toFraction(0.66, 0.01) => [2/3]
|
|
87
|
+
* toFraction(283 + 1/3) => [850, 3]
|
|
88
|
+
*/
|
|
89
|
+
function toFraction(decimal) {
|
|
90
|
+
let tolerance = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : EPSILON;
|
|
91
|
+
let maxDenominator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000;
|
|
92
|
+
// Initialize everything to compute successive terms of
|
|
93
|
+
// continued-fraction approximations via recurrence relation
|
|
47
94
|
let n = [1, 0];
|
|
48
95
|
let d = [0, 1];
|
|
49
96
|
let a = Math.floor(decimal);
|
|
@@ -57,6 +104,9 @@ function toFraction(decimal, tolerance = EPSILON, maxDenominator = 1000) {
|
|
|
57
104
|
a = Math.floor(1 / rem);
|
|
58
105
|
rem = 1 / rem - a;
|
|
59
106
|
}
|
|
107
|
+
|
|
108
|
+
// We failed to find a nice rational representation,
|
|
109
|
+
// so return an irrational "fraction"
|
|
60
110
|
return [decimal, 1];
|
|
61
111
|
}
|
|
62
112
|
|
|
@@ -75,13 +125,21 @@ var number = /*#__PURE__*/Object.freeze({
|
|
|
75
125
|
toFraction: toFraction
|
|
76
126
|
});
|
|
77
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Vector Utils
|
|
130
|
+
* A vector is an array of numbers e.g. [0, 3, 4].
|
|
131
|
+
*/
|
|
132
|
+
|
|
78
133
|
function arraySum(array) {
|
|
79
134
|
return array.reduce((memo, arg) => memo + arg, 0);
|
|
80
135
|
}
|
|
81
136
|
function arrayProduct(array) {
|
|
82
137
|
return array.reduce((memo, arg) => memo * arg, 1);
|
|
83
138
|
}
|
|
84
|
-
function zip(
|
|
139
|
+
function zip() {
|
|
140
|
+
for (var _len = arguments.length, arrays = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
141
|
+
arrays[_key] = arguments[_key];
|
|
142
|
+
}
|
|
85
143
|
const n = Math.min(...arrays.map(a => a.length));
|
|
86
144
|
const results = [];
|
|
87
145
|
for (let i = 0; i < n; i++) {
|
|
@@ -92,6 +150,16 @@ function zip(...arrays) {
|
|
|
92
150
|
function map(pair, f) {
|
|
93
151
|
return [f(pair[0], 0), f(pair[1], 1)];
|
|
94
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Checks if the given vector contains only numbers and, optionally, is of the
|
|
156
|
+
* right dimension (length).
|
|
157
|
+
*
|
|
158
|
+
* is([1, 2, 3]) -> true
|
|
159
|
+
* is([1, "Hello", 3]) -> false
|
|
160
|
+
* is([1, 2, 3], 1) -> false
|
|
161
|
+
*/
|
|
162
|
+
|
|
95
163
|
function is$1(vec, dimension) {
|
|
96
164
|
if (!Array.isArray(vec)) {
|
|
97
165
|
return false;
|
|
@@ -101,30 +169,46 @@ function is$1(vec, dimension) {
|
|
|
101
169
|
}
|
|
102
170
|
return vec.every(is$2);
|
|
103
171
|
}
|
|
172
|
+
|
|
173
|
+
// Normalize to a unit vector
|
|
104
174
|
function normalize(v) {
|
|
105
175
|
return scale(v, 1 / length(v));
|
|
106
176
|
}
|
|
177
|
+
|
|
178
|
+
// Length/magnitude of a vector
|
|
107
179
|
function length(v) {
|
|
108
180
|
return Math.sqrt(dot(v, v));
|
|
109
181
|
}
|
|
182
|
+
// Dot product of two vectors
|
|
110
183
|
function dot(a, b) {
|
|
111
184
|
const zipped = zip(a, b);
|
|
112
185
|
const multiplied = zipped.map(arrayProduct);
|
|
113
186
|
return arraySum(multiplied);
|
|
114
187
|
}
|
|
115
|
-
|
|
116
|
-
|
|
188
|
+
|
|
189
|
+
/* vector-add multiple [x, y] coords/vectors
|
|
190
|
+
*
|
|
191
|
+
* add([1, 2], [3, 4]) -> [4, 6]
|
|
192
|
+
*/
|
|
193
|
+
function add$1() {
|
|
194
|
+
const zipped = zip(...arguments);
|
|
195
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
117
196
|
return zipped.map(arraySum);
|
|
118
197
|
}
|
|
119
198
|
function subtract(v1, v2) {
|
|
199
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
120
200
|
return zip(v1, v2).map(dim => dim[0] - dim[1]);
|
|
121
201
|
}
|
|
122
202
|
function negate(v) {
|
|
203
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
123
204
|
return v.map(x => {
|
|
124
205
|
return -x;
|
|
125
206
|
});
|
|
126
207
|
}
|
|
208
|
+
|
|
209
|
+
// Scale a vector
|
|
127
210
|
function scale(v1, scalar) {
|
|
211
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
128
212
|
return v1.map(x => {
|
|
129
213
|
return x * scalar;
|
|
130
214
|
});
|
|
@@ -133,6 +217,9 @@ function equal$3(v1, v2, tolerance) {
|
|
|
133
217
|
return v1.length === v2.length && zip(v1, v2).every(pair => equal$4(pair[0], pair[1], tolerance));
|
|
134
218
|
}
|
|
135
219
|
function codirectional(v1, v2, tolerance) {
|
|
220
|
+
// The origin is trivially codirectional with all other vectors.
|
|
221
|
+
// This gives nice semantics for codirectionality between points when
|
|
222
|
+
// comparing their difference vectors.
|
|
136
223
|
if (equal$4(length(v1), 0, tolerance) || equal$4(length(v2), 0, tolerance)) {
|
|
137
224
|
return true;
|
|
138
225
|
}
|
|
@@ -143,24 +230,48 @@ function codirectional(v1, v2, tolerance) {
|
|
|
143
230
|
function collinear$1(v1, v2, tolerance) {
|
|
144
231
|
return codirectional(v1, v2, tolerance) || codirectional(v1, negate(v2), tolerance);
|
|
145
232
|
}
|
|
233
|
+
|
|
234
|
+
// TODO(jeremy) These coordinate conversion functions really only handle 2D points (ie. [number, number])
|
|
235
|
+
|
|
236
|
+
// Convert a cartesian coordinate into a radian polar coordinate
|
|
146
237
|
function polarRadFromCart$1(v) {
|
|
147
238
|
const radius = length(v);
|
|
148
239
|
let theta = Math.atan2(v[1], v[0]);
|
|
240
|
+
|
|
241
|
+
// Convert angle range from [-pi, pi] to [0, 2pi]
|
|
149
242
|
if (theta < 0) {
|
|
150
243
|
theta += 2 * Math.PI;
|
|
151
244
|
}
|
|
152
245
|
return [radius, theta];
|
|
153
246
|
}
|
|
154
|
-
|
|
247
|
+
|
|
248
|
+
// Converts a cartesian coordinate into a degree polar coordinate
|
|
249
|
+
function polarDegFromCart$1(v) /* TODO: convert to tuple/Point */{
|
|
155
250
|
const polar = polarRadFromCart$1(v);
|
|
156
251
|
return [polar[0], polar[1] * 180 / Math.PI];
|
|
157
252
|
}
|
|
158
|
-
|
|
253
|
+
|
|
254
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
255
|
+
*
|
|
256
|
+
* Examples:
|
|
257
|
+
* cartFromPolarRad(5, Math.PI)
|
|
258
|
+
*/
|
|
259
|
+
function cartFromPolarRad$1(radius) /* TODO: convert to tuple/Point */{
|
|
260
|
+
let theta = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
|
159
261
|
return [radius * Math.cos(theta), radius * Math.sin(theta)];
|
|
160
262
|
}
|
|
161
|
-
|
|
263
|
+
|
|
264
|
+
/* Convert a polar coordinate into a cartesian coordinate
|
|
265
|
+
*
|
|
266
|
+
* Examples:
|
|
267
|
+
* cartFromPolarDeg(5, 30)
|
|
268
|
+
*/
|
|
269
|
+
function cartFromPolarDeg$1(radius) {
|
|
270
|
+
let theta = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
|
162
271
|
return cartFromPolarRad$1(radius, theta * Math.PI / 180);
|
|
163
272
|
}
|
|
273
|
+
|
|
274
|
+
// Rotate vector
|
|
164
275
|
function rotateRad$1(v, theta) {
|
|
165
276
|
const polar = polarRadFromCart$1(v);
|
|
166
277
|
const angle = polar[1] + theta;
|
|
@@ -171,26 +282,39 @@ function rotateDeg$1(v, theta) {
|
|
|
171
282
|
const angle = polar[1] + theta;
|
|
172
283
|
return cartFromPolarDeg$1(polar[0], angle);
|
|
173
284
|
}
|
|
285
|
+
|
|
286
|
+
// Angle between two vectors
|
|
174
287
|
function angleRad(v1, v2) {
|
|
175
288
|
return Math.acos(dot(v1, v2) / (length(v1) * length(v2)));
|
|
176
289
|
}
|
|
177
290
|
function angleDeg(v1, v2) {
|
|
178
291
|
return angleRad(v1, v2) * 180 / Math.PI;
|
|
179
292
|
}
|
|
293
|
+
|
|
294
|
+
// Vector projection of v1 onto v2
|
|
180
295
|
function projection(v1, v2) {
|
|
181
296
|
const scalar = dot(v1, v2) / dot(v2, v2);
|
|
182
297
|
return scale(v2, scalar);
|
|
183
298
|
}
|
|
299
|
+
|
|
300
|
+
// Round each number to a certain number of decimal places
|
|
184
301
|
function round$1(vec, precision) {
|
|
302
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
185
303
|
return vec.map((elem, i) => round$2(elem, precision[i] || precision));
|
|
186
304
|
}
|
|
305
|
+
|
|
306
|
+
// Round each number to the nearest increment
|
|
307
|
+
|
|
187
308
|
function roundTo$1(vec, increment) {
|
|
309
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
188
310
|
return vec.map((elem, i) => roundTo$2(elem, increment[i] || increment));
|
|
189
311
|
}
|
|
190
312
|
function floorTo$1(vec, increment) {
|
|
313
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
191
314
|
return vec.map((elem, i) => floorTo$2(elem, increment[i] || increment));
|
|
192
315
|
}
|
|
193
316
|
function ceilTo$1(vec, increment) {
|
|
317
|
+
// @ts-expect-error - TS2322 - Type 'number[]' is not assignable to type 'V'.
|
|
194
318
|
return vec.map((elem, i) => ceilTo$2(elem, increment[i] || increment));
|
|
195
319
|
}
|
|
196
320
|
|
|
@@ -224,6 +348,15 @@ var vector$1 = /*#__PURE__*/Object.freeze({
|
|
|
224
348
|
zip: zip
|
|
225
349
|
});
|
|
226
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Point Utils
|
|
353
|
+
* A point is an array of two numbers e.g. [0, 0].
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
// A point, in 2D, 3D, or nD space.
|
|
358
|
+
|
|
359
|
+
// Rotate point (around origin unless a center is specified)
|
|
227
360
|
function rotateRad(point, theta, center) {
|
|
228
361
|
if (center === undefined) {
|
|
229
362
|
return rotateRad$1(point, theta);
|
|
@@ -238,9 +371,13 @@ function rotateDeg(point, theta, center) {
|
|
|
238
371
|
return add$1(center, rotateDeg$1(subtract(point, center), theta));
|
|
239
372
|
}
|
|
240
373
|
}
|
|
374
|
+
|
|
375
|
+
// Distance between two points
|
|
241
376
|
function distanceToPoint$1(point1, point2) {
|
|
242
377
|
return length(subtract(point1, point2));
|
|
243
378
|
}
|
|
379
|
+
|
|
380
|
+
// Distance between point and line
|
|
244
381
|
function distanceToLine(point, line) {
|
|
245
382
|
const lv = subtract(line[1], line[0]);
|
|
246
383
|
const pv = subtract(point, line[0]);
|
|
@@ -248,6 +385,8 @@ function distanceToLine(point, line) {
|
|
|
248
385
|
const distancePv = subtract(projectedPv, pv);
|
|
249
386
|
return length(distancePv);
|
|
250
387
|
}
|
|
388
|
+
|
|
389
|
+
// Reflect point over line
|
|
251
390
|
function reflectOverLine(point, line) {
|
|
252
391
|
const lv = subtract(line[1], line[0]);
|
|
253
392
|
const pv = subtract(point, line[0]);
|
|
@@ -255,7 +394,17 @@ function reflectOverLine(point, line) {
|
|
|
255
394
|
const reflectedPv = subtract(scale(projectedPv, 2), pv);
|
|
256
395
|
return add$1(line[0], reflectedPv);
|
|
257
396
|
}
|
|
258
|
-
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Compares two points, returning -1, 0, or 1, for use with
|
|
400
|
+
* Array.prototype.sort
|
|
401
|
+
*
|
|
402
|
+
* Note: This technically doesn't satisfy the total-ordering
|
|
403
|
+
* requirements of Array.prototype.sort unless equalityTolerance
|
|
404
|
+
* is 0. In some cases very close points that compare within a
|
|
405
|
+
* few equalityTolerances could appear in the wrong order.
|
|
406
|
+
*/
|
|
407
|
+
function compare(point1, point2, equalityTolerance) /* TODO: convert to -1 | 0 | 1 type */{
|
|
259
408
|
if (point1.length !== point2.length) {
|
|
260
409
|
return point1.length - point2.length;
|
|
261
410
|
}
|
|
@@ -266,15 +415,23 @@ function compare(point1, point2, equalityTolerance) {
|
|
|
266
415
|
}
|
|
267
416
|
return 0;
|
|
268
417
|
}
|
|
418
|
+
|
|
419
|
+
// Check if a value is a point
|
|
269
420
|
const is = is$1;
|
|
421
|
+
|
|
422
|
+
// Add and subtract vector(s)
|
|
270
423
|
const addVector = add$1;
|
|
271
424
|
const addVectors = add$1;
|
|
272
425
|
const subtractVector = subtract;
|
|
273
426
|
const equal$2 = equal$3;
|
|
427
|
+
|
|
428
|
+
// Convert from cartesian to polar and back
|
|
274
429
|
const polarRadFromCart = polarRadFromCart$1;
|
|
275
430
|
const polarDegFromCart = polarDegFromCart$1;
|
|
276
431
|
const cartFromPolarRad = cartFromPolarRad$1;
|
|
277
432
|
const cartFromPolarDeg = cartFromPolarDeg$1;
|
|
433
|
+
|
|
434
|
+
// Rounding
|
|
278
435
|
const round = round$1;
|
|
279
436
|
const roundTo = roundTo$1;
|
|
280
437
|
const floorTo = floorTo$1;
|
|
@@ -303,6 +460,11 @@ var point = /*#__PURE__*/Object.freeze({
|
|
|
303
460
|
subtractVector: subtractVector
|
|
304
461
|
});
|
|
305
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Line Utils
|
|
465
|
+
* A line is an array of two points e.g. [[-5, 0], [5, 0]].
|
|
466
|
+
*/
|
|
467
|
+
|
|
306
468
|
function distanceToPoint(line, point$1) {
|
|
307
469
|
return distanceToLine(point$1, line);
|
|
308
470
|
}
|
|
@@ -313,14 +475,20 @@ function midpoint(line) {
|
|
|
313
475
|
return [(line[0][0] + line[1][0]) / 2, (line[0][1] + line[1][1]) / 2];
|
|
314
476
|
}
|
|
315
477
|
function equal$1(line1, line2, tolerance) {
|
|
478
|
+
// TODO: A nicer implementation might just check collinearity of
|
|
479
|
+
// vectors using underscore magick
|
|
480
|
+
// Compare the directions of the lines
|
|
316
481
|
const v1 = subtract(line1[1], line1[0]);
|
|
317
482
|
const v2 = subtract(line2[1], line2[0]);
|
|
318
483
|
if (!collinear$1(v1, v2, tolerance)) {
|
|
319
484
|
return false;
|
|
320
485
|
}
|
|
486
|
+
// If the start point is the same for the two lines, then they are the same
|
|
321
487
|
if (equal$2(line1[0], line2[0])) {
|
|
322
488
|
return true;
|
|
323
489
|
}
|
|
490
|
+
// Make sure that the direction to get from line1 to
|
|
491
|
+
// line2 is the same as the direction of the lines
|
|
324
492
|
const line1ToLine2Vector = subtract(line2[0], line1[0]);
|
|
325
493
|
return collinear$1(v1, line1ToLine2Vector, tolerance);
|
|
326
494
|
}
|
|
@@ -333,7 +501,15 @@ var line = /*#__PURE__*/Object.freeze({
|
|
|
333
501
|
reflectPoint: reflectPoint
|
|
334
502
|
});
|
|
335
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Ray Utils
|
|
506
|
+
* A ray (→) is an array of an endpoint and another point along the ray.
|
|
507
|
+
* For example, [[0, 0], [1, 0]] is the ray starting at the origin and
|
|
508
|
+
* traveling along the positive x-axis.
|
|
509
|
+
*/
|
|
510
|
+
|
|
336
511
|
function equal(ray1, ray2, tolerance) {
|
|
512
|
+
// Compare the directions of the rays
|
|
337
513
|
const v1 = subtract(ray1[1], ray1[0]);
|
|
338
514
|
const v2 = subtract(ray2[1], ray2[0]);
|
|
339
515
|
const sameOrigin = equal$2(ray1[0], ray2[0]);
|
|
@@ -347,9 +523,11 @@ var ray = /*#__PURE__*/Object.freeze({
|
|
|
347
523
|
});
|
|
348
524
|
|
|
349
525
|
const KhanMath = {
|
|
526
|
+
// Simplify formulas before display
|
|
350
527
|
cleanMath: function (expr) {
|
|
351
528
|
return typeof expr === "string" ? expr.replace(/\+\s*-/g, "- ").replace(/-\s*-/g, "+ ").replace(/\^1/g, "") : expr;
|
|
352
529
|
},
|
|
530
|
+
// Bound a number by 1e-6 and 1e20 to avoid exponents after toString
|
|
353
531
|
bound: function (num) {
|
|
354
532
|
if (num === 0) {
|
|
355
533
|
return num;
|
|
@@ -367,12 +545,17 @@ const KhanMath = {
|
|
|
367
545
|
},
|
|
368
546
|
getGCD: function (a, b) {
|
|
369
547
|
if (arguments.length > 2) {
|
|
548
|
+
// TODO(kevinb): rewrite using rest args instead of arguments
|
|
549
|
+
// eslint-disable-next-line prefer-rest-params
|
|
370
550
|
const rest = [].slice.call(arguments, 1);
|
|
551
|
+
// @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
|
|
371
552
|
return KhanMath.getGCD(a, KhanMath.getGCD(...rest));
|
|
372
553
|
}
|
|
373
554
|
let mod;
|
|
374
555
|
a = Math.abs(a);
|
|
375
556
|
b = Math.abs(b);
|
|
557
|
+
|
|
558
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
376
559
|
while (b) {
|
|
377
560
|
mod = a % b;
|
|
378
561
|
a = b;
|
|
@@ -382,7 +565,10 @@ const KhanMath = {
|
|
|
382
565
|
},
|
|
383
566
|
getLCM: function (a, b) {
|
|
384
567
|
if (arguments.length > 2) {
|
|
568
|
+
// TODO(kevinb): rewrite using rest args instead of arguments
|
|
569
|
+
// eslint-disable-next-line prefer-rest-params
|
|
385
570
|
const rest = [].slice.call(arguments, 1);
|
|
571
|
+
// @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
|
|
386
572
|
return KhanMath.getLCM(a, KhanMath.getLCM(...rest));
|
|
387
573
|
}
|
|
388
574
|
return Math.abs(a * b) / KhanMath.getGCD(a, b);
|
|
@@ -393,7 +579,8 @@ const KhanMath = {
|
|
|
393
579
|
return false;
|
|
394
580
|
}
|
|
395
581
|
if (n < 101) {
|
|
396
|
-
|
|
582
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
583
|
+
return !!$__default.default.grep(KhanMath.primes, function (p, i) {
|
|
397
584
|
return Math.abs(p - n) <= 0.5;
|
|
398
585
|
}).length;
|
|
399
586
|
}
|
|
@@ -407,6 +594,7 @@ const KhanMath = {
|
|
|
407
594
|
}
|
|
408
595
|
return true;
|
|
409
596
|
},
|
|
597
|
+
// @ts-expect-error - TS2366 - Function lacks ending return statement and return type does not include 'undefined'.
|
|
410
598
|
getPrimeFactorization: function (number) {
|
|
411
599
|
if (number === 1) {
|
|
412
600
|
return [];
|
|
@@ -417,24 +605,39 @@ const KhanMath = {
|
|
|
417
605
|
const maxf = Math.sqrt(number);
|
|
418
606
|
for (let f = 2; f <= maxf; f++) {
|
|
419
607
|
if (number % f === 0) {
|
|
420
|
-
return
|
|
608
|
+
return $__default.default.merge(KhanMath.getPrimeFactorization(f), KhanMath.getPrimeFactorization(number / f));
|
|
421
609
|
}
|
|
422
610
|
}
|
|
423
611
|
},
|
|
612
|
+
// Round a number to the nearest increment
|
|
613
|
+
// E.g., if increment = 30 and num = 40, return 30. if increment = 30 and
|
|
614
|
+
// num = 45, return 60.
|
|
424
615
|
roundToNearest: function (increment, num) {
|
|
425
616
|
return Math.round(num / increment) * increment;
|
|
426
617
|
},
|
|
618
|
+
// Round a number to a certain number of decimal places
|
|
427
619
|
roundTo: function (precision, num) {
|
|
428
620
|
const factor = Math.pow(10, precision).toFixed(5);
|
|
621
|
+
// @ts-expect-error - TS2345 - Argument of type 'string' is not assignable to parameter of type 'number'. | TS2363 - The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. | TS2363 - The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
|
|
429
622
|
return Math.round((num * factor).toFixed(5)) / factor;
|
|
430
623
|
},
|
|
624
|
+
/**
|
|
625
|
+
* Return a string of num rounded to a fixed precision decimal places,
|
|
626
|
+
* with an approx symbol if num had to be rounded, and trailing 0s
|
|
627
|
+
*/
|
|
431
628
|
toFixedApprox: function (num, precision) {
|
|
629
|
+
// TODO(aria): Make this locale-dependent
|
|
432
630
|
const fixedStr = num.toFixed(precision);
|
|
433
631
|
if (equal$4(+fixedStr, num)) {
|
|
434
632
|
return fixedStr;
|
|
435
633
|
}
|
|
436
634
|
return "\\approx " + fixedStr;
|
|
437
635
|
},
|
|
636
|
+
/**
|
|
637
|
+
* Return a string of num rounded to precision decimal places, with an
|
|
638
|
+
* approx symbol if num had to be rounded, but no trailing 0s if it was
|
|
639
|
+
* not rounded.
|
|
640
|
+
*/
|
|
438
641
|
roundToApprox: function (num, precision) {
|
|
439
642
|
const fixed = KhanMath.roundTo(precision, num);
|
|
440
643
|
if (equal$4(fixed, num)) {
|
|
@@ -442,6 +645,11 @@ const KhanMath = {
|
|
|
442
645
|
}
|
|
443
646
|
return KhanMath.toFixedApprox(num, precision);
|
|
444
647
|
},
|
|
648
|
+
// toFraction(4/8) => [1, 2]
|
|
649
|
+
// toFraction(0.666) => [333, 500]
|
|
650
|
+
// toFraction(0.666, 0.001) => [2, 3]
|
|
651
|
+
//
|
|
652
|
+
// tolerance can't be bigger than 1, sorry
|
|
445
653
|
toFraction: function (decimal, tolerance) {
|
|
446
654
|
if (tolerance == null) {
|
|
447
655
|
tolerance = Math.pow(2, -46);
|
|
@@ -462,6 +670,8 @@ const KhanMath = {
|
|
|
462
670
|
let hiD = 1;
|
|
463
671
|
let midN = 1;
|
|
464
672
|
let midD = 2;
|
|
673
|
+
|
|
674
|
+
// eslint-disable-next-line no-constant-condition
|
|
465
675
|
while (true) {
|
|
466
676
|
if (Math.abs(Number(midN / midD) - decimal) <= tolerance) {
|
|
467
677
|
return [midN, midD];
|
|
@@ -477,8 +687,11 @@ const KhanMath = {
|
|
|
477
687
|
midD = loD + hiD;
|
|
478
688
|
}
|
|
479
689
|
},
|
|
690
|
+
// Returns the format (string) of a given numeric string
|
|
691
|
+
// Note: purposively more inclusive than answer-types' predicate.forms
|
|
692
|
+
// That is, it is not necessarily true that interpreted input are numeric
|
|
480
693
|
getNumericFormat: function (text) {
|
|
481
|
-
text =
|
|
694
|
+
text = $__default.default.trim(text);
|
|
482
695
|
text = text.replace(/\u2212/, "-").replace(/([+-])\s+/g, "$1");
|
|
483
696
|
if (text.match(/^[+-]?\d+$/)) {
|
|
484
697
|
return "integer";
|
|
@@ -498,12 +711,13 @@ const KhanMath = {
|
|
|
498
711
|
}
|
|
499
712
|
return null;
|
|
500
713
|
},
|
|
714
|
+
// Returns a string of the number in a specified format
|
|
501
715
|
toNumericString: function (number$1, format) {
|
|
502
716
|
if (number$1 == null) {
|
|
503
717
|
return "";
|
|
504
718
|
}
|
|
505
719
|
if (number$1 === 0) {
|
|
506
|
-
return "0";
|
|
720
|
+
return "0"; // otherwise it might end up as 0% or 0pi
|
|
507
721
|
}
|
|
508
722
|
if (format === "percent") {
|
|
509
723
|
return number$1 * 100 + "%";
|
|
@@ -518,21 +732,25 @@ const KhanMath = {
|
|
|
518
732
|
return sign + (numerator === 1 ? "" : numerator) + pi + (denominator === 1 ? "" : "/" + denominator);
|
|
519
733
|
}
|
|
520
734
|
}
|
|
521
|
-
if (
|
|
735
|
+
if (___default.default(["proper", "improper", "mixed", "fraction"]).contains(format)) {
|
|
522
736
|
const fraction = toFraction(number$1);
|
|
523
737
|
const numerator = Math.abs(fraction[0]);
|
|
524
738
|
const denominator = fraction[1];
|
|
525
739
|
const sign = number$1 < 0 ? "-" : "";
|
|
526
740
|
if (denominator === 1) {
|
|
527
|
-
return sign + numerator;
|
|
741
|
+
return sign + numerator; // for integers, irrational, d > 1000
|
|
528
742
|
}
|
|
529
743
|
if (format === "mixed") {
|
|
530
744
|
const modulus = numerator % denominator;
|
|
531
745
|
const integer = (numerator - modulus) / denominator;
|
|
532
|
-
return sign + (
|
|
533
|
-
|
|
746
|
+
return sign + (
|
|
747
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
748
|
+
integer ? integer + " " : "") + modulus + "/" + denominator;
|
|
749
|
+
} // otherwise proper, improper, or fraction
|
|
534
750
|
return sign + numerator + "/" + denominator;
|
|
535
751
|
}
|
|
752
|
+
|
|
753
|
+
// otherwise (decimal, float, long long)
|
|
536
754
|
return String(number$1);
|
|
537
755
|
}
|
|
538
756
|
};
|
|
@@ -543,24 +761,40 @@ function add(a, b) {
|
|
|
543
761
|
return a + b;
|
|
544
762
|
}
|
|
545
763
|
|
|
764
|
+
/**
|
|
765
|
+
* A collection of geomtry-related utility functions
|
|
766
|
+
*/
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
// This should really be a readonly tuple of [number, number]
|
|
770
|
+
|
|
771
|
+
// Given a number, return whether it is positive (1), negative (-1), or zero (0)
|
|
546
772
|
function sign(val) {
|
|
547
|
-
if (approximateEqual(val, 0)) {
|
|
773
|
+
if (perseusCore.approximateEqual(val, 0)) {
|
|
548
774
|
return 0;
|
|
549
775
|
}
|
|
550
776
|
return val > 0 ? 1 : -1;
|
|
551
777
|
}
|
|
778
|
+
|
|
779
|
+
// Determine whether three points are collinear (0), for a clockwise turn (negative),
|
|
780
|
+
// or counterclockwise turn (positive)
|
|
552
781
|
function ccw(a, b, c) {
|
|
553
782
|
return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
554
783
|
}
|
|
555
784
|
function collinear(a, b, c) {
|
|
556
|
-
return approximateEqual(ccw(a, b, c), 0);
|
|
785
|
+
return perseusCore.approximateEqual(ccw(a, b, c), 0);
|
|
557
786
|
}
|
|
787
|
+
|
|
788
|
+
// Given rect bounding points A and B, whether point C is inside the rect
|
|
558
789
|
function pointInRect(a, b, c) {
|
|
559
790
|
return c[0] <= Math.max(a[0], b[0]) && c[0] >= Math.min(a[0], b[0]) && c[1] <= Math.max(a[1], b[1]) && c[1] >= Math.min(a[1], b[1]);
|
|
560
791
|
}
|
|
792
|
+
|
|
793
|
+
// Whether line segment AB intersects line segment CD
|
|
794
|
+
// http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
|
|
561
795
|
function intersects(ab, cd) {
|
|
562
796
|
const triplets = [[ab[0], ab[1], cd[0]], [ab[0], ab[1], cd[1]], [cd[0], cd[1], ab[0]], [cd[0], cd[1], ab[1]]];
|
|
563
|
-
const orientations =
|
|
797
|
+
const orientations = ___default.default.map(triplets, function (triplet) {
|
|
564
798
|
return sign(ccw(...triplet));
|
|
565
799
|
});
|
|
566
800
|
if (orientations[0] !== orientations[1] && orientations[2] !== orientations[3]) {
|
|
@@ -573,14 +807,21 @@ function intersects(ab, cd) {
|
|
|
573
807
|
}
|
|
574
808
|
return false;
|
|
575
809
|
}
|
|
810
|
+
|
|
811
|
+
// Whether any two sides of a polygon intersect each other
|
|
576
812
|
function polygonSidesIntersect(vertices) {
|
|
577
813
|
for (let i = 0; i < vertices.length; i++) {
|
|
578
814
|
for (let k = i + 1; k < vertices.length; k++) {
|
|
815
|
+
// If any two vertices are the same point, sides overlap
|
|
579
816
|
if (equal$2(vertices[i], vertices[k])) {
|
|
580
817
|
return true;
|
|
581
818
|
}
|
|
819
|
+
|
|
820
|
+
// Find the other end of the sides starting at vertices i and k
|
|
582
821
|
const iNext = (i + 1) % vertices.length;
|
|
583
822
|
const kNext = (k + 1) % vertices.length;
|
|
823
|
+
|
|
824
|
+
// Adjacent sides always intersect (at the vertex); skip those
|
|
584
825
|
if (iNext === k || kNext === i) {
|
|
585
826
|
continue;
|
|
586
827
|
}
|
|
@@ -594,16 +835,20 @@ function polygonSidesIntersect(vertices) {
|
|
|
594
835
|
return false;
|
|
595
836
|
}
|
|
596
837
|
function vector(a, b) {
|
|
597
|
-
return
|
|
838
|
+
return ___default.default.map(___default.default.zip(a, b), function (pair) {
|
|
598
839
|
return pair[0] - pair[1];
|
|
599
840
|
});
|
|
600
841
|
}
|
|
601
842
|
function reverseVector(vector) {
|
|
602
843
|
return [-vector[0], -vector[1]];
|
|
603
844
|
}
|
|
845
|
+
|
|
846
|
+
// Returns whether connecting the given sequence of `points` forms a clockwise
|
|
847
|
+
// path (assuming a closed loop, where the last point connects back to the
|
|
848
|
+
// first).
|
|
604
849
|
function clockwise(points) {
|
|
605
|
-
const segments =
|
|
606
|
-
const areas =
|
|
850
|
+
const segments = ___default.default.zip(points, points.slice(1).concat(points.slice(0, 1)));
|
|
851
|
+
const areas = ___default.default.map(segments, function (segment) {
|
|
607
852
|
const p1 = segment[0];
|
|
608
853
|
const p2 = segment[1];
|
|
609
854
|
return (p2[0] - p1[0]) * (p2[1] + p1[1]);
|
|
@@ -611,36 +856,44 @@ function clockwise(points) {
|
|
|
611
856
|
return sum(areas) > 0;
|
|
612
857
|
}
|
|
613
858
|
function magnitude(v) {
|
|
614
|
-
return Math.sqrt(
|
|
859
|
+
return Math.sqrt(___default.default.reduce(v, function (memo, el) {
|
|
860
|
+
// @ts-expect-error - TS2345 - Argument of type 'Coord' is not assignable to parameter of type 'number'.
|
|
615
861
|
return memo + Math.pow(el, 2);
|
|
616
862
|
}, 0));
|
|
617
863
|
}
|
|
618
864
|
function dotProduct(a, b) {
|
|
619
|
-
return
|
|
865
|
+
return ___default.default.reduce(___default.default.zip(a, b), function (memo, pair) {
|
|
620
866
|
return memo + pair[0] * pair[1];
|
|
621
867
|
}, 0);
|
|
622
868
|
}
|
|
623
869
|
function sideLengths(coords) {
|
|
624
|
-
const segments =
|
|
870
|
+
const segments = ___default.default.zip(coords, rotate(coords));
|
|
625
871
|
return segments.map(function (segment) {
|
|
872
|
+
// @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
|
|
626
873
|
return magnitude(vector(...segment));
|
|
627
874
|
});
|
|
628
875
|
}
|
|
876
|
+
|
|
877
|
+
// Based on http://math.stackexchange.com/a/151149
|
|
629
878
|
function angleMeasures(coords) {
|
|
630
|
-
const triplets =
|
|
631
|
-
const offsets =
|
|
879
|
+
const triplets = ___default.default.zip(rotate(coords, -1), coords, rotate(coords, 1));
|
|
880
|
+
const offsets = ___default.default.map(triplets, function (triplet) {
|
|
632
881
|
const p = vector(triplet[1], triplet[0]);
|
|
633
882
|
const q = vector(triplet[2], triplet[1]);
|
|
883
|
+
// @ts-expect-error - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'Coord'. | TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
|
|
634
884
|
const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
|
|
885
|
+
// @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
|
|
635
886
|
return sign(ccw(...triplet)) > 0 ? raw : -raw;
|
|
636
887
|
});
|
|
637
|
-
const sum =
|
|
888
|
+
const sum = ___default.default.reduce(offsets, function (memo, arg) {
|
|
638
889
|
return memo + arg;
|
|
639
890
|
}, 0);
|
|
640
|
-
return
|
|
891
|
+
return ___default.default.map(offsets, function (offset) {
|
|
641
892
|
return sum > 0 ? Math.PI - offset : Math.PI + offset;
|
|
642
893
|
});
|
|
643
894
|
}
|
|
895
|
+
|
|
896
|
+
// Whether two polygons are similar (or if specified, congruent)
|
|
644
897
|
function similar(coords1, coords2, tolerance) {
|
|
645
898
|
if (coords1.length !== coords2.length) {
|
|
646
899
|
return false;
|
|
@@ -653,22 +906,30 @@ function similar(coords1, coords2, tolerance) {
|
|
|
653
906
|
for (let i = 0; i < 2 * n; i++) {
|
|
654
907
|
let angles = angles2.slice();
|
|
655
908
|
let sides = sides2.slice();
|
|
909
|
+
|
|
910
|
+
// Reverse angles and sides to allow matching reflected polygons
|
|
656
911
|
if (i >= n) {
|
|
657
912
|
angles.reverse();
|
|
658
913
|
sides.reverse();
|
|
914
|
+
// Since sides are calculated from two coordinates,
|
|
915
|
+
// simply reversing results in an off by one error
|
|
916
|
+
// @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
|
|
659
917
|
sides = rotate(sides, 1);
|
|
660
918
|
}
|
|
919
|
+
|
|
920
|
+
// @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
|
|
661
921
|
angles = rotate(angles, i);
|
|
922
|
+
// @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
|
|
662
923
|
sides = rotate(sides, i);
|
|
663
|
-
if (approximateDeepEqual(angles1, angles)) {
|
|
664
|
-
const sidePairs =
|
|
665
|
-
const factors =
|
|
924
|
+
if (perseusCore.approximateDeepEqual(angles1, angles)) {
|
|
925
|
+
const sidePairs = ___default.default.zip(sides1, sides);
|
|
926
|
+
const factors = ___default.default.map(sidePairs, function (pair) {
|
|
666
927
|
return pair[0] / pair[1];
|
|
667
928
|
});
|
|
668
|
-
const same =
|
|
669
|
-
return approximateEqual(factors[0], factor);
|
|
929
|
+
const same = ___default.default.all(factors, function (factor) {
|
|
930
|
+
return perseusCore.approximateEqual(factors[0], factor);
|
|
670
931
|
});
|
|
671
|
-
const congruentEnough =
|
|
932
|
+
const congruentEnough = ___default.default.all(sidePairs, function (pair) {
|
|
672
933
|
return equal$4(pair[0], pair[1], tolerance);
|
|
673
934
|
});
|
|
674
935
|
if (same && congruentEnough) {
|
|
@@ -678,21 +939,32 @@ function similar(coords1, coords2, tolerance) {
|
|
|
678
939
|
}
|
|
679
940
|
return false;
|
|
680
941
|
}
|
|
942
|
+
|
|
943
|
+
// Given triangle with sides ABC return angle opposite side C in degrees
|
|
681
944
|
function lawOfCosines(a, b, c) {
|
|
682
945
|
return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180 / Math.PI;
|
|
683
946
|
}
|
|
684
|
-
function canonicalSineCoefficients(
|
|
947
|
+
function canonicalSineCoefficients(_ref) {
|
|
948
|
+
let [amplitude, angularFrequency, phase, verticalOffset] = _ref;
|
|
949
|
+
// For a curve of the form f(x) = a * Sin(b * x - c) + d,
|
|
950
|
+
// this function ensures that a, b > 0, and c is its
|
|
951
|
+
// smallest possible positive value.
|
|
952
|
+
|
|
953
|
+
// Guarantee a > 0
|
|
685
954
|
if (amplitude < 0) {
|
|
686
955
|
amplitude *= -1;
|
|
687
956
|
angularFrequency *= -1;
|
|
688
957
|
phase *= -1;
|
|
689
958
|
}
|
|
690
959
|
const period = 2 * Math.PI;
|
|
960
|
+
// Guarantee b > 0
|
|
691
961
|
if (angularFrequency < 0) {
|
|
692
962
|
angularFrequency *= -1;
|
|
693
963
|
phase *= -1;
|
|
694
964
|
phase += period / 2;
|
|
695
965
|
}
|
|
966
|
+
|
|
967
|
+
// Guarantee c is smallest possible positive value
|
|
696
968
|
while (phase > 0) {
|
|
697
969
|
phase -= period;
|
|
698
970
|
}
|
|
@@ -701,19 +973,26 @@ function canonicalSineCoefficients([amplitude, angularFrequency, phase, vertical
|
|
|
701
973
|
}
|
|
702
974
|
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
703
975
|
}
|
|
976
|
+
|
|
977
|
+
// e.g. rotate([1, 2, 3]) -> [2, 3, 1]
|
|
704
978
|
function rotate(array, n) {
|
|
705
979
|
n = typeof n === "undefined" ? 1 : n % array.length;
|
|
706
980
|
return array.slice(n).concat(array.slice(0, n));
|
|
707
981
|
}
|
|
708
982
|
function getLineEquation(first, second) {
|
|
709
|
-
if (approximateEqual(first[0], second[0])) {
|
|
983
|
+
if (perseusCore.approximateEqual(first[0], second[0])) {
|
|
710
984
|
return "x = " + first[0].toFixed(3);
|
|
711
985
|
}
|
|
712
986
|
const m = (second[1] - first[1]) / (second[0] - first[0]);
|
|
713
987
|
const b = first[1] - m * first[0];
|
|
714
988
|
return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
|
|
715
989
|
}
|
|
716
|
-
|
|
990
|
+
|
|
991
|
+
// Stolen from the wikipedia article
|
|
992
|
+
// http://en.wikipedia.org/wiki/Line-line_intersection
|
|
993
|
+
function getLineIntersection(
|
|
994
|
+
// TODO(LP-10725): update these to be 2-tuples
|
|
995
|
+
firstPoints, secondPoints) {
|
|
717
996
|
const x1 = firstPoints[0][0];
|
|
718
997
|
const y1 = firstPoints[0][1];
|
|
719
998
|
const x2 = firstPoints[1][0];
|
|
@@ -724,6 +1003,7 @@ function getLineIntersection(firstPoints, secondPoints) {
|
|
|
724
1003
|
const y4 = secondPoints[1][1];
|
|
725
1004
|
const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
726
1005
|
if (Math.abs(determinant) < 1e-9) {
|
|
1006
|
+
// Lines are parallel
|
|
727
1007
|
return null;
|
|
728
1008
|
}
|
|
729
1009
|
const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / determinant;
|
|
@@ -760,16 +1040,25 @@ var geometry = /*#__PURE__*/Object.freeze({
|
|
|
760
1040
|
vector: vector
|
|
761
1041
|
});
|
|
762
1042
|
|
|
1043
|
+
// This file contains helper functions for working with angles.
|
|
1044
|
+
|
|
763
1045
|
function convertDegreesToRadians(degrees) {
|
|
764
1046
|
return degrees / 180 * Math.PI;
|
|
765
1047
|
}
|
|
766
1048
|
function convertRadiansToDegrees(radians) {
|
|
767
1049
|
const degree = radians / Math.PI * 180;
|
|
1050
|
+
// Account for floating point errors.
|
|
768
1051
|
return Number(degree.toPrecision(15));
|
|
769
1052
|
}
|
|
770
|
-
|
|
1053
|
+
|
|
1054
|
+
// Returns a value between -180 and 180, inclusive. The angle is measured
|
|
1055
|
+
// between the positive x-axis and the given vector.
|
|
1056
|
+
function calculateAngleInDegrees(_ref) {
|
|
1057
|
+
let [x, y] = _ref;
|
|
771
1058
|
return Math.atan2(y, x) * 180 / Math.PI;
|
|
772
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
// Converts polar coordinates to cartesian. The th(eta) parameter is in degrees.
|
|
773
1062
|
function polar(r, th) {
|
|
774
1063
|
if (typeof r === "number") {
|
|
775
1064
|
r = [r, r];
|
|
@@ -777,19 +1066,37 @@ function polar(r, th) {
|
|
|
777
1066
|
th = th * Math.PI / 180;
|
|
778
1067
|
return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
|
|
779
1068
|
}
|
|
1069
|
+
// This function calculates the angle between two points and an optional vertex.
|
|
1070
|
+
// If the vertex is not provided, the angle is measured between the two points.
|
|
1071
|
+
// This does not account for reflex angles or clockwise position.
|
|
780
1072
|
const getAngleFromVertex = (point, vertex) => {
|
|
781
1073
|
const x = point[0] - vertex[0];
|
|
782
1074
|
const y = point[1] - vertex[1];
|
|
1075
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
783
1076
|
if (!x && !y) {
|
|
784
1077
|
return 0;
|
|
785
1078
|
}
|
|
786
1079
|
return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
|
|
787
1080
|
};
|
|
788
|
-
|
|
1081
|
+
|
|
1082
|
+
// This function calculates the clockwise angle between three points,
|
|
1083
|
+
// and is used to generate the labels and equation strings of the
|
|
1084
|
+
// current angle for the interactive graph.
|
|
1085
|
+
const getClockwiseAngle = function (coords) {
|
|
1086
|
+
let allowReflexAngles = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
789
1087
|
const coordsCopy = [...coords];
|
|
1088
|
+
// The coords are saved as [point1, vertex, point2] in the interactive graph
|
|
790
1089
|
const areClockwise = clockwise([coordsCopy[0], coordsCopy[2], coordsCopy[1]]);
|
|
1090
|
+
|
|
1091
|
+
// We may need to reverse the coordinates if we allow
|
|
1092
|
+
// reflex angles and the points are not in clockwise order.
|
|
791
1093
|
const shouldReverseCoords = !areClockwise || allowReflexAngles;
|
|
1094
|
+
|
|
1095
|
+
// Reverse the coordinates accordingly to ensure the angle is calculated correctly
|
|
792
1096
|
const clockwiseCoords = shouldReverseCoords ? coordsCopy.reverse() : coordsCopy;
|
|
1097
|
+
|
|
1098
|
+
// Calculate the angles between the two points and get the difference
|
|
1099
|
+
// between the two angles to get the clockwise angle.
|
|
793
1100
|
const startAngle = getAngleFromVertex(clockwiseCoords[0], clockwiseCoords[1]);
|
|
794
1101
|
const endAngle = getAngleFromVertex(clockwiseCoords[2], clockwiseCoords[1]);
|
|
795
1102
|
const angle = (startAngle + 360 - endAngle) % 360;
|
|
@@ -806,21 +1113,30 @@ var angles = /*#__PURE__*/Object.freeze({
|
|
|
806
1113
|
polar: polar
|
|
807
1114
|
});
|
|
808
1115
|
|
|
1116
|
+
// TODO: there's another, very similar getSinusoidCoefficients function
|
|
1117
|
+
// they should probably be merged
|
|
809
1118
|
function getSinusoidCoefficients(coords) {
|
|
1119
|
+
// It's assumed that p1 is the root and p2 is the first peak
|
|
810
1120
|
const p1 = coords[0];
|
|
811
1121
|
const p2 = coords[1];
|
|
1122
|
+
|
|
1123
|
+
// Resulting coefficients are canonical for this sine curve
|
|
812
1124
|
const amplitude = p2[1] - p1[1];
|
|
813
1125
|
const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
|
|
814
1126
|
const phase = p1[0] * angularFrequency;
|
|
815
1127
|
const verticalOffset = p1[1];
|
|
816
1128
|
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
817
1129
|
}
|
|
1130
|
+
// TODO: there's another, very similar getQuadraticCoefficients function
|
|
1131
|
+
// they should probably be merged
|
|
818
1132
|
function getQuadraticCoefficients(coords) {
|
|
819
1133
|
const p1 = coords[0];
|
|
820
1134
|
const p2 = coords[1];
|
|
821
1135
|
const p3 = coords[2];
|
|
822
1136
|
const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
|
|
823
1137
|
if (denom === 0) {
|
|
1138
|
+
// Many of the callers assume that the return value is always defined.
|
|
1139
|
+
// @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
|
|
824
1140
|
return;
|
|
825
1141
|
}
|
|
826
1142
|
const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
|
|
@@ -835,5 +1151,15 @@ var coefficients = /*#__PURE__*/Object.freeze({
|
|
|
835
1151
|
getSinusoidCoefficients: getSinusoidCoefficients
|
|
836
1152
|
});
|
|
837
1153
|
|
|
838
|
-
|
|
1154
|
+
exports.KhanMath = KhanMath;
|
|
1155
|
+
exports.angles = angles;
|
|
1156
|
+
exports.coefficients = coefficients;
|
|
1157
|
+
exports.geometry = geometry;
|
|
1158
|
+
exports.libVersion = libVersion;
|
|
1159
|
+
exports.line = line;
|
|
1160
|
+
exports.number = number;
|
|
1161
|
+
exports.point = point;
|
|
1162
|
+
exports.ray = ray;
|
|
1163
|
+
exports.sum = sum;
|
|
1164
|
+
exports.vector = vector$1;
|
|
839
1165
|
//# sourceMappingURL=index.js.map
|