@khanacademy/kmath 0.2.0 → 0.3.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,6 @@
1
+ import type { Coord } from "@khanacademy/perseus-core";
2
+ export declare function convertDegreesToRadians(degrees: number): number;
3
+ export declare function calculateAngleInDegrees([x, y]: Coord): number;
4
+ export declare function polar(r: number | Coord, th: number): Coord;
5
+ export declare const getAngleFromVertex: (point: Coord, vertex: Coord) => number;
6
+ export declare const getClockwiseAngle: (coords: [Coord, Coord, Coord], allowReflexAngles?: boolean) => number;
@@ -0,0 +1,11 @@
1
+ import type { SineCoefficient } from "./geometry";
2
+ import type { Coord } from "@khanacademy/perseus-core";
3
+ export type NamedSineCoefficient = {
4
+ amplitude: number;
5
+ angularFrequency: number;
6
+ phase: number;
7
+ verticalOffset: number;
8
+ };
9
+ export declare function getSinusoidCoefficients(coords: ReadonlyArray<Coord>): SineCoefficient;
10
+ export type QuadraticCoefficient = [number, number, number];
11
+ export declare function getQuadraticCoefficients(coords: ReadonlyArray<Coord>): QuadraticCoefficient;
package/dist/es/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
1
+ import { addLibraryVersionToPerseusDebug, approximateEqual, approximateDeepEqual } from '@khanacademy/perseus-core';
2
2
  import _ from 'underscore';
3
- import { number as number$1 } from '@khanacademy/kmath';
3
+ import { point as point$1, sum as sum$1, number as number$1 } from '@khanacademy/kmath';
4
4
 
5
5
  // This file is processed by a Rollup plugin (replace) to inject the production
6
6
  const libName = "@khanacademy/kmath";
7
- const libVersion = "0.2.0";
7
+ const libVersion = "0.3.0";
8
8
  addLibraryVersionToPerseusDebug(libName, libVersion);
9
9
 
10
10
  /**
@@ -33,7 +33,7 @@ function equal$4(x, y, tolerance) {
33
33
  }
34
34
  return Math.abs(x - y) < tolerance;
35
35
  }
36
- function sign(x, tolerance) /* Should be: 0 | 1 | -1 */{
36
+ function sign$1(x, tolerance) /* Should be: 0 | 1 | -1 */{
37
37
  return equal$4(x, 0, tolerance) ? 0 : Math.abs(x) / x;
38
38
  }
39
39
  function isInteger(num, tolerance) {
@@ -102,7 +102,7 @@ var number = /*#__PURE__*/Object.freeze({
102
102
  EPSILON: EPSILON,
103
103
  is: is$2,
104
104
  equal: equal$4,
105
- sign: sign,
105
+ sign: sign$1,
106
106
  isInteger: isInteger,
107
107
  round: round$2,
108
108
  roundTo: roundTo$2,
@@ -209,7 +209,7 @@ function codirectional(v1, v2, tolerance) {
209
209
  v2 = normalize(v2);
210
210
  return equal$3(v1, v2, tolerance);
211
211
  }
212
- function collinear(v1, v2, tolerance) {
212
+ function collinear$1(v1, v2, tolerance) {
213
213
  return codirectional(v1, v2, tolerance) || codirectional(v1, negate(v2), tolerance);
214
214
  }
215
215
 
@@ -298,7 +298,7 @@ function ceilTo$1(vec, increment) {
298
298
  return vec.map((elem, i) => ceilTo$2(elem, increment[i] || increment));
299
299
  }
300
300
 
301
- var vector = /*#__PURE__*/Object.freeze({
301
+ var vector$1 = /*#__PURE__*/Object.freeze({
302
302
  __proto__: null,
303
303
  zip: zip,
304
304
  map: map,
@@ -312,7 +312,7 @@ var vector = /*#__PURE__*/Object.freeze({
312
312
  scale: scale,
313
313
  equal: equal$3,
314
314
  codirectional: codirectional,
315
- collinear: collinear,
315
+ collinear: collinear$1,
316
316
  polarRadFromCart: polarRadFromCart$1,
317
317
  polarDegFromCart: polarDegFromCart$1,
318
318
  cartFromPolarRad: cartFromPolarRad$1,
@@ -458,7 +458,7 @@ function equal$1(line1, line2, tolerance) {
458
458
  // Compare the directions of the lines
459
459
  const v1 = subtract(line1[1], line1[0]);
460
460
  const v2 = subtract(line2[1], line2[0]);
461
- if (!collinear(v1, v2, tolerance)) {
461
+ if (!collinear$1(v1, v2, tolerance)) {
462
462
  return false;
463
463
  }
464
464
  // If the start point is the same for the two lines, then they are the same
@@ -468,7 +468,7 @@ function equal$1(line1, line2, tolerance) {
468
468
  // Make sure that the direction to get from line1 to
469
469
  // line2 is the same as the direction of the lines
470
470
  const line1ToLine2Vector = subtract(line2[0], line1[0]);
471
- return collinear(v1, line1ToLine2Vector, tolerance);
471
+ return collinear$1(v1, line1ToLine2Vector, tolerance);
472
472
  }
473
473
 
474
474
  var line = /*#__PURE__*/Object.freeze({
@@ -499,6 +499,374 @@ var ray = /*#__PURE__*/Object.freeze({
499
499
  equal: equal
500
500
  });
501
501
 
502
+ /**
503
+ * A collection of geomtry-related utility functions
504
+ */
505
+
506
+ // This should really be a readonly tuple of [number, number]
507
+
508
+ // Given a number, return whether it is positive (1), negative (-1), or zero (0)
509
+ function sign(val) {
510
+ if (approximateEqual(val, 0)) {
511
+ return 0;
512
+ }
513
+ return val > 0 ? 1 : -1;
514
+ }
515
+
516
+ // Determine whether three points are collinear (0), for a clockwise turn (negative),
517
+ // or counterclockwise turn (positive)
518
+ function ccw(a, b, c) {
519
+ return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
520
+ }
521
+ function collinear(a, b, c) {
522
+ return approximateEqual(ccw(a, b, c), 0);
523
+ }
524
+
525
+ // Given rect bounding points A and B, whether point C is inside the rect
526
+ function pointInRect(a, b, c) {
527
+ 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]);
528
+ }
529
+
530
+ // Whether line segment AB intersects line segment CD
531
+ // http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
532
+ function intersects(ab, cd) {
533
+ 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]]];
534
+ const orientations = _.map(triplets, function (triplet) {
535
+ return sign(ccw(...triplet));
536
+ });
537
+ if (orientations[0] !== orientations[1] && orientations[2] !== orientations[3]) {
538
+ return true;
539
+ }
540
+ for (let i = 0; i < 4; i++) {
541
+ if (orientations[i] === 0 && pointInRect(...triplets[i])) {
542
+ return true;
543
+ }
544
+ }
545
+ return false;
546
+ }
547
+
548
+ // Whether any two sides of a polygon intersect each other
549
+ function polygonSidesIntersect(vertices) {
550
+ for (let i = 0; i < vertices.length; i++) {
551
+ for (let k = i + 1; k < vertices.length; k++) {
552
+ // If any two vertices are the same point, sides overlap
553
+ if (point$1.equal(vertices[i], vertices[k])) {
554
+ return true;
555
+ }
556
+
557
+ // Find the other end of the sides starting at vertices i and k
558
+ const iNext = (i + 1) % vertices.length;
559
+ const kNext = (k + 1) % vertices.length;
560
+
561
+ // Adjacent sides always intersect (at the vertex); skip those
562
+ if (iNext === k || kNext === i) {
563
+ continue;
564
+ }
565
+ const side1 = [vertices[i], vertices[iNext]];
566
+ const side2 = [vertices[k], vertices[kNext]];
567
+ if (intersects(side1, side2)) {
568
+ return true;
569
+ }
570
+ }
571
+ }
572
+ return false;
573
+ }
574
+ function vector(a, b) {
575
+ return _.map(_.zip(a, b), function (pair) {
576
+ return pair[0] - pair[1];
577
+ });
578
+ }
579
+ function reverseVector(vector) {
580
+ return [-vector[0], -vector[1]];
581
+ }
582
+
583
+ // Returns whether connecting the given sequence of `points` forms a clockwise
584
+ // path (assuming a closed loop, where the last point connects back to the
585
+ // first).
586
+ function clockwise(points) {
587
+ const segments = _.zip(points, points.slice(1).concat(points.slice(0, 1)));
588
+ const areas = _.map(segments, function (segment) {
589
+ const p1 = segment[0];
590
+ const p2 = segment[1];
591
+ return (p2[0] - p1[0]) * (p2[1] + p1[1]);
592
+ });
593
+ return sum$1(areas) > 0;
594
+ }
595
+ function magnitude(v) {
596
+ return Math.sqrt(_.reduce(v, function (memo, el) {
597
+ // @ts-expect-error - TS2345 - Argument of type 'Coord' is not assignable to parameter of type 'number'.
598
+ return memo + Math.pow(el, 2);
599
+ }, 0));
600
+ }
601
+ function dotProduct(a, b) {
602
+ return _.reduce(_.zip(a, b), function (memo, pair) {
603
+ return memo + pair[0] * pair[1];
604
+ }, 0);
605
+ }
606
+ function sideLengths(coords) {
607
+ const segments = _.zip(coords, rotate(coords));
608
+ return segments.map(function (segment) {
609
+ // @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.
610
+ return magnitude(vector(...segment));
611
+ });
612
+ }
613
+
614
+ // Based on http://math.stackexchange.com/a/151149
615
+ function angleMeasures(coords) {
616
+ const triplets = _.zip(rotate(coords, -1), coords, rotate(coords, 1));
617
+ const offsets = _.map(triplets, function (triplet) {
618
+ const p = vector(triplet[1], triplet[0]);
619
+ const q = vector(triplet[2], triplet[1]);
620
+ // @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[]'.
621
+ const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
622
+ // @ts-expect-error - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
623
+ return sign(ccw(...triplet)) > 0 ? raw : -raw;
624
+ });
625
+ const sum = _.reduce(offsets, function (memo, arg) {
626
+ return memo + arg;
627
+ }, 0);
628
+ return _.map(offsets, function (offset) {
629
+ return sum > 0 ? Math.PI - offset : Math.PI + offset;
630
+ });
631
+ }
632
+
633
+ // Whether two polygons are similar (or if specified, congruent)
634
+ function similar(coords1, coords2, tolerance) {
635
+ if (coords1.length !== coords2.length) {
636
+ return false;
637
+ }
638
+ const n = coords1.length;
639
+ const angles1 = angleMeasures(coords1);
640
+ const angles2 = angleMeasures(coords2);
641
+ const sides1 = sideLengths(coords1);
642
+ const sides2 = sideLengths(coords2);
643
+ for (let i = 0; i < 2 * n; i++) {
644
+ let angles = angles2.slice();
645
+ let sides = sides2.slice();
646
+
647
+ // Reverse angles and sides to allow matching reflected polygons
648
+ if (i >= n) {
649
+ angles.reverse();
650
+ sides.reverse();
651
+ // Since sides are calculated from two coordinates,
652
+ // simply reversing results in an off by one error
653
+ // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
654
+ sides = rotate(sides, 1);
655
+ }
656
+
657
+ // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
658
+ angles = rotate(angles, i);
659
+ // @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
660
+ sides = rotate(sides, i);
661
+ if (approximateDeepEqual(angles1, angles)) {
662
+ const sidePairs = _.zip(sides1, sides);
663
+ const factors = _.map(sidePairs, function (pair) {
664
+ return pair[0] / pair[1];
665
+ });
666
+ const same = _.all(factors, function (factor) {
667
+ return approximateEqual(factors[0], factor);
668
+ });
669
+ const congruentEnough = _.all(sidePairs, function (pair) {
670
+ return number$1.equal(pair[0], pair[1], tolerance);
671
+ });
672
+ if (same && congruentEnough) {
673
+ return true;
674
+ }
675
+ }
676
+ }
677
+ return false;
678
+ }
679
+
680
+ // Given triangle with sides ABC return angle opposite side C in degrees
681
+ function lawOfCosines(a, b, c) {
682
+ return Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180 / Math.PI;
683
+ }
684
+ function canonicalSineCoefficients([amplitude, angularFrequency, phase, verticalOffset]) {
685
+ // For a curve of the form f(x) = a * Sin(b * x - c) + d,
686
+ // this function ensures that a, b > 0, and c is its
687
+ // smallest possible positive value.
688
+
689
+ // Guarantee a > 0
690
+ if (amplitude < 0) {
691
+ amplitude *= -1;
692
+ angularFrequency *= -1;
693
+ phase *= -1;
694
+ }
695
+ const period = 2 * Math.PI;
696
+ // Guarantee b > 0
697
+ if (angularFrequency < 0) {
698
+ angularFrequency *= -1;
699
+ phase *= -1;
700
+ phase += period / 2;
701
+ }
702
+
703
+ // Guarantee c is smallest possible positive value
704
+ while (phase > 0) {
705
+ phase -= period;
706
+ }
707
+ while (phase < 0) {
708
+ phase += period;
709
+ }
710
+ return [amplitude, angularFrequency, phase, verticalOffset];
711
+ }
712
+
713
+ // e.g. rotate([1, 2, 3]) -> [2, 3, 1]
714
+ function rotate(array, n) {
715
+ n = typeof n === "undefined" ? 1 : n % array.length;
716
+ return array.slice(n).concat(array.slice(0, n));
717
+ }
718
+ function getLineEquation(first, second) {
719
+ if (approximateEqual(first[0], second[0])) {
720
+ return "x = " + first[0].toFixed(3);
721
+ }
722
+ const m = (second[1] - first[1]) / (second[0] - first[0]);
723
+ const b = first[1] - m * first[0];
724
+ return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
725
+ }
726
+
727
+ // Stolen from the wikipedia article
728
+ // http://en.wikipedia.org/wiki/Line-line_intersection
729
+ function getLineIntersection(
730
+ // TODO(LP-10725): update these to be 2-tuples
731
+ firstPoints, secondPoints) {
732
+ const x1 = firstPoints[0][0];
733
+ const y1 = firstPoints[0][1];
734
+ const x2 = firstPoints[1][0];
735
+ const y2 = firstPoints[1][1];
736
+ const x3 = secondPoints[0][0];
737
+ const y3 = secondPoints[0][1];
738
+ const x4 = secondPoints[1][0];
739
+ const y4 = secondPoints[1][1];
740
+ const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
741
+ if (Math.abs(determinant) < 1e-9) {
742
+ return "Lines are parallel";
743
+ }
744
+ const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / determinant;
745
+ const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / determinant;
746
+ return "Intersection: (" + x.toFixed(3) + ", " + y.toFixed(3) + ")";
747
+ }
748
+
749
+ var geometry = /*#__PURE__*/Object.freeze({
750
+ __proto__: null,
751
+ sign: sign,
752
+ ccw: ccw,
753
+ collinear: collinear,
754
+ intersects: intersects,
755
+ polygonSidesIntersect: polygonSidesIntersect,
756
+ vector: vector,
757
+ reverseVector: reverseVector,
758
+ clockwise: clockwise,
759
+ magnitude: magnitude,
760
+ angleMeasures: angleMeasures,
761
+ similar: similar,
762
+ lawOfCosines: lawOfCosines,
763
+ canonicalSineCoefficients: canonicalSineCoefficients,
764
+ rotate: rotate,
765
+ getLineEquation: getLineEquation,
766
+ getLineIntersection: getLineIntersection
767
+ });
768
+
769
+ // This file contains helper functions for working with angles.
770
+ function convertDegreesToRadians(degrees) {
771
+ return degrees / 180 * Math.PI;
772
+ }
773
+
774
+ // Returns a value between -180 and 180, inclusive. The angle is measured
775
+ // between the positive x-axis and the given vector.
776
+ function calculateAngleInDegrees([x, y]) {
777
+ return Math.atan2(y, x) * 180 / Math.PI;
778
+ }
779
+
780
+ // Converts polar coordinates to cartesian. The th(eta) parameter is in degrees.
781
+ function polar(r, th) {
782
+ if (typeof r === "number") {
783
+ r = [r, r];
784
+ }
785
+ th = th * Math.PI / 180;
786
+ return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
787
+ }
788
+ // This function calculates the angle between two points and an optional vertex.
789
+ // If the vertex is not provided, the angle is measured between the two points.
790
+ // This does not account for reflex angles or clockwise position.
791
+ const getAngleFromVertex = (point, vertex) => {
792
+ const x = point[0] - vertex[0];
793
+ const y = point[1] - vertex[1];
794
+ if (!x && !y) {
795
+ return 0;
796
+ }
797
+ return (180 + Math.atan2(-y, -x) * 180 / Math.PI + 360) % 360;
798
+ };
799
+
800
+ // This function calculates the clockwise angle between three points,
801
+ // and is used to generate the labels and equation strings of the
802
+ // current angle for the interactive graph.
803
+ const getClockwiseAngle = (coords, allowReflexAngles = false) => {
804
+ const coordsCopy = [...coords];
805
+ // The coords are saved as [point1, vertex, point2] in the interactive graph
806
+ const areClockwise = clockwise([coordsCopy[0], coordsCopy[2], coordsCopy[1]]);
807
+
808
+ // We may need to reverse the coordinates if we allow
809
+ // reflex angles and the points are not in clockwise order.
810
+ const shouldReverseCoords = !areClockwise || allowReflexAngles;
811
+
812
+ // Reverse the coordinates accordingly to ensure the angle is calculated correctly
813
+ const clockwiseCoords = shouldReverseCoords ? coordsCopy.reverse() : coordsCopy;
814
+
815
+ // Calculate the angles between the two points and get the difference
816
+ // between the two angles to get the clockwise angle.
817
+ const startAngle = getAngleFromVertex(clockwiseCoords[0], clockwiseCoords[1]);
818
+ const endAngle = getAngleFromVertex(clockwiseCoords[2], clockwiseCoords[1]);
819
+ const angle = (startAngle + 360 - endAngle) % 360;
820
+ return angle;
821
+ };
822
+
823
+ var angles = /*#__PURE__*/Object.freeze({
824
+ __proto__: null,
825
+ convertDegreesToRadians: convertDegreesToRadians,
826
+ calculateAngleInDegrees: calculateAngleInDegrees,
827
+ polar: polar,
828
+ getAngleFromVertex: getAngleFromVertex,
829
+ getClockwiseAngle: getClockwiseAngle
830
+ });
831
+
832
+ // TODO: there's another, very similar getSinusoidCoefficients function
833
+ // they should probably be merged
834
+ function getSinusoidCoefficients(coords) {
835
+ // It's assumed that p1 is the root and p2 is the first peak
836
+ const p1 = coords[0];
837
+ const p2 = coords[1];
838
+
839
+ // Resulting coefficients are canonical for this sine curve
840
+ const amplitude = p2[1] - p1[1];
841
+ const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
842
+ const phase = p1[0] * angularFrequency;
843
+ const verticalOffset = p1[1];
844
+ return [amplitude, angularFrequency, phase, verticalOffset];
845
+ }
846
+ // TODO: there's another, very similar getQuadraticCoefficients function
847
+ // they should probably be merged
848
+ function getQuadraticCoefficients(coords) {
849
+ const p1 = coords[0];
850
+ const p2 = coords[1];
851
+ const p3 = coords[2];
852
+ const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
853
+ if (denom === 0) {
854
+ // Many of the callers assume that the return value is always defined.
855
+ // @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
856
+ return;
857
+ }
858
+ const a = (p3[0] * (p2[1] - p1[1]) + p2[0] * (p1[1] - p3[1]) + p1[0] * (p3[1] - p2[1])) / denom;
859
+ const b = (p3[0] * p3[0] * (p1[1] - p2[1]) + p2[0] * p2[0] * (p3[1] - p1[1]) + p1[0] * p1[0] * (p2[1] - p3[1])) / denom;
860
+ const c = (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / denom;
861
+ return [a, b, c];
862
+ }
863
+
864
+ var coefficients = /*#__PURE__*/Object.freeze({
865
+ __proto__: null,
866
+ getSinusoidCoefficients: getSinusoidCoefficients,
867
+ getQuadraticCoefficients: getQuadraticCoefficients
868
+ });
869
+
502
870
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
503
871
 
504
872
  var jquery = {exports: {}};
@@ -10534,5 +10902,5 @@ function add(a, b) {
10534
10902
  return a + b;
10535
10903
  }
10536
10904
 
10537
- export { KhanMath, libVersion, line, number, point, ray, sum, vector };
10905
+ export { KhanMath, angles, coefficients, geometry, libVersion, line, number, point, ray, sum, vector$1 as vector };
10538
10906
  //# sourceMappingURL=index.js.map