@khanacademy/perseus 22.1.0 → 22.2.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/index.css CHANGED
@@ -4172,6 +4172,7 @@ html body .mq-editable-field .mq-selection .mq-nthroot-container * {
4172
4172
 
4173
4173
  --mafs-origin-color: var(--mafs-fg);
4174
4174
  --mafs-line-color: #555;
4175
+ --mafs-line-stroke-dash-style: 4, 3;
4175
4176
  --grid-line-subdivision-color: #222;
4176
4177
 
4177
4178
  --mafs-red: #f11d0e;
package/dist/index.js CHANGED
@@ -28965,6 +28965,162 @@ const lockedFigureColors = {
28965
28965
  };
28966
28966
  const plotterPlotTypes = ["bar", "line", "pic", "histogram", "dotplot"];
28967
28967
 
28968
+ function pathBuilder() {
28969
+ return new PathBuilder();
28970
+ }
28971
+ class PathBuilder {
28972
+ path = [];
28973
+ scaleFactor = 1;
28974
+ build() {
28975
+ return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
28976
+ }
28977
+
28978
+ // add a move (M) command to the path
28979
+ move(x, y) {
28980
+ this.path.push({
28981
+ action: "M",
28982
+ coords: [x, y]
28983
+ });
28984
+ return this;
28985
+ }
28986
+
28987
+ // add a curve (C) command to the path
28988
+ curve(control1X, control1Y, control2X, control2Y, endX, endY) {
28989
+ this.path.push({
28990
+ action: "C",
28991
+ coords: [control1X, control1Y, control2X, control2Y, endX, endY]
28992
+ });
28993
+ return this;
28994
+ }
28995
+
28996
+ // Scale all coordinates of the path by a factor
28997
+ scale(factor) {
28998
+ this.scaleFactor *= factor;
28999
+ return this;
29000
+ }
29001
+ }
29002
+ function commandToString(command) {
29003
+ return `${command.action}${command.coords.join(" ")}`;
29004
+ }
29005
+ function scaleCommandBy(scaleFactor) {
29006
+ return command => ({
29007
+ ...command,
29008
+ coords: command.coords.map(c => c * scaleFactor)
29009
+ });
29010
+ }
29011
+
29012
+ // We use the pathBuilder here to scale up the SVG path coordinates used
29013
+ // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
29014
+ // transform because a transform would also scale up the stroke weight, and we
29015
+ // don't want that.
29016
+ const arrowPath = pathBuilder().move(-3, 4).curve(-2.75, 2.5, 0, 0.25, 0.75, 0).curve(0, -0.25, -2.75, -2.5, -3, -4).scale(1.4).build();
29017
+ function Arrowhead(props) {
29018
+ const [point] = useTransformVectorsToPixels(props.tip);
29019
+ return /*#__PURE__*/React__namespace.createElement("g", {
29020
+ transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
29021
+ }, /*#__PURE__*/React__namespace.createElement("g", {
29022
+ transform: "translate(-1.5)"
29023
+ }, /*#__PURE__*/React__namespace.createElement("path", {
29024
+ d: arrowPath,
29025
+ fill: "none",
29026
+ style: {
29027
+ stroke: props.color ?? "inherit"
29028
+ },
29029
+ strokeLinejoin: "round",
29030
+ strokeLinecap: "round"
29031
+ })));
29032
+ }
29033
+
29034
+ function SVGLine(props) {
29035
+ const {
29036
+ start,
29037
+ end,
29038
+ style,
29039
+ className,
29040
+ testId
29041
+ } = props;
29042
+ return /*#__PURE__*/React__namespace.createElement("line", {
29043
+ x1: start[0],
29044
+ y1: start[1],
29045
+ x2: end[0],
29046
+ y2: end[1],
29047
+ style: style,
29048
+ className: className,
29049
+ "data-testid": testId
29050
+ });
29051
+ }
29052
+
29053
+ function Vector(props) {
29054
+ const {
29055
+ tail,
29056
+ tip,
29057
+ color,
29058
+ style
29059
+ } = props;
29060
+ const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
29061
+ const direction = mafs.vec.sub(tip, tail);
29062
+ return /*#__PURE__*/React__namespace.createElement("g", {
29063
+ style: {
29064
+ stroke: color,
29065
+ strokeWidth: 2
29066
+ }
29067
+ }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
29068
+ start: tailPx,
29069
+ end: tipPx,
29070
+ style: style
29071
+ }), /*#__PURE__*/React__namespace.createElement(Arrowhead, {
29072
+ tip: tip,
29073
+ angle: angleDegrees(direction)
29074
+ }));
29075
+ }
29076
+ function angleDegrees(_ref) {
29077
+ let [x, y] = _ref;
29078
+ return Math.atan2(y, x) * 180 / Math.PI;
29079
+ }
29080
+
29081
+ /**
29082
+ * Given a ray and a rectangular box, find the point where the ray intersects
29083
+ * the edge of the box. Assumes the `initialPoint` is inside the box.
29084
+ * @param initialPoint - The starting point of the ray.
29085
+ * @param throughPoint - A point that the ray passes through. Must be different from initialPoint.
29086
+ * @param box - The box with which to intersect the ray, in the form [[xMin, xMax], [yMin, yMax]]
29087
+ */
29088
+ const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
29089
+ const [[xMin, xMax], [yMin, yMax]] = box;
29090
+ const [aX, aY] = throughPoint;
29091
+ const [bX, bY] = initialPoint;
29092
+ const yDiff = bY - aY;
29093
+ const xDiff = bX - aX;
29094
+ const slope = yDiff / xDiff;
29095
+ const yAtXMin = slope * (xMin - aX) + aY;
29096
+ const yAtXMax = slope * (xMax - aX) + aY;
29097
+ const xAtYMin = (yMin - aY) / slope + aX;
29098
+ const xAtYMax = (yMax - aY) / slope + aX;
29099
+
29100
+ // clock analogy to describe quadrants
29101
+ switch (true) {
29102
+ // 12 o'clock to 2:59
29103
+ case yDiff > 0 && xDiff >= 0:
29104
+ return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
29105
+ // 3 o'clock to 5:59
29106
+ case yDiff <= 0 && xDiff > 0:
29107
+ // xAtYMin evaluates to -Infinity here, so we use absolute value
29108
+ return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
29109
+ // 9 o'clock to 11:59
29110
+ case yDiff >= 0 && xDiff < 0:
29111
+ return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
29112
+ // 6 o'clock to 8:59
29113
+ case yDiff < 0 && xDiff <= 0:
29114
+ return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
29115
+ default:
29116
+ return [xMax, yAtXMax];
29117
+ }
29118
+ };
29119
+ const getLines = points => points.map((point, i) => {
29120
+ const next = points[(i + 1) % points.length];
29121
+ return [point, next];
29122
+ });
29123
+
28968
29124
  const LockedLine = props => {
28969
29125
  const {
28970
29126
  color,
@@ -28972,11 +29128,27 @@ const LockedLine = props => {
28972
29128
  kind,
28973
29129
  points,
28974
29130
  showStartPoint,
28975
- showEndPoint
29131
+ showEndPoint,
29132
+ range
28976
29133
  } = props;
28977
29134
  const [point1, point2] = points;
28978
29135
  let line;
28979
- if (kind === "line" || kind === "segment") {
29136
+ if (kind === "ray") {
29137
+ // Rays extend to the end of the graph in one direction.
29138
+ const endExtend = getIntersectionOfRayWithBox(point2.coord, point1.coord, range);
29139
+ line = /*#__PURE__*/React__namespace.createElement(Vector, {
29140
+ tail: point1.coord,
29141
+ tip: endExtend,
29142
+ color: lockedFigureColors[color],
29143
+ style: {
29144
+ strokeDasharray: lineStyle === "dashed" ?
29145
+ // TODO(lems-1930): Uncomment this line when the
29146
+ // dashed style is updated in Mafs.
29147
+ // ? "var(--mafs-line-stroke-dash-style)"
29148
+ "4, 3" : undefined
29149
+ }
29150
+ });
29151
+ } else {
28980
29152
  const LineType = kind === "segment" ? mafs.Line.Segment : mafs.Line.ThroughPoints;
28981
29153
  line = /*#__PURE__*/React__namespace.createElement(LineType, {
28982
29154
  point1: point1.coord,
@@ -28984,12 +29156,9 @@ const LockedLine = props => {
28984
29156
  color: lockedFigureColors[color],
28985
29157
  style: lineStyle
28986
29158
  });
28987
- } else {
28988
- // TODO(LEMS-1928): Implement rays
28989
- return null;
28990
29159
  }
28991
29160
  return /*#__PURE__*/React__namespace.createElement("g", {
28992
- className: "locked-line"
29161
+ className: kind === "ray" ? "locked-ray" : "locked-line"
28993
29162
  }, line, showStartPoint && /*#__PURE__*/React__namespace.createElement(mafs.Point, {
28994
29163
  x: point1.coord[0],
28995
29164
  y: point1.coord[1],
@@ -29038,14 +29207,16 @@ const GraphLockedLayer = props => {
29038
29207
  lockedFigures
29039
29208
  } = props;
29040
29209
  return /*#__PURE__*/React__namespace.createElement(React__namespace.Fragment, null, lockedFigures.map((figure, index) => {
29041
- let Figure;
29042
29210
  switch (figure.type) {
29043
29211
  case "point":
29044
- Figure = LockedPoint;
29045
- break;
29212
+ return /*#__PURE__*/React__namespace.createElement(LockedPoint, _extends({
29213
+ key: `point-${index}`
29214
+ }, figure));
29046
29215
  case "line":
29047
- Figure = LockedLine;
29048
- break;
29216
+ return /*#__PURE__*/React__namespace.createElement(LockedLine, _extends({
29217
+ key: `line-${index}`,
29218
+ range: props.range
29219
+ }, figure));
29049
29220
  default:
29050
29221
  /**
29051
29222
  * Devlopment-time future-proofing: This should
@@ -29055,9 +29226,6 @@ const GraphLockedLayer = props => {
29055
29226
  */
29056
29227
  throw new wonderStuffCore.UnreachableCaseError(figure);
29057
29228
  }
29058
- return /*#__PURE__*/React__namespace.createElement(Figure, _extends({
29059
- key: `${figure.type}-${index}`
29060
- }, figure));
29061
29229
  }));
29062
29230
  };
29063
29231
 
@@ -29139,160 +29307,6 @@ function snap(snapStep, point) {
29139
29307
  return [Math.round(requestedX / snapX) * snapX, Math.round(requestedY / snapY) * snapY];
29140
29308
  }
29141
29309
 
29142
- /**
29143
- * Given a ray and a rectangular box, find the point where the ray intersects
29144
- * the edge of the box. Assumes the `initialPoint` is inside the box.
29145
- * @param initialPoint - The starting point of the ray.
29146
- * @param throughPoint - A point that the ray passes through. Must be different from initialPoint.
29147
- * @param box - The box with which to intersect the ray, in the form [[xMin, xMax], [yMin, yMax]]
29148
- */
29149
- const getIntersectionOfRayWithBox = (initialPoint, throughPoint, box) => {
29150
- const [[xMin, xMax], [yMin, yMax]] = box;
29151
- const [aX, aY] = throughPoint;
29152
- const [bX, bY] = initialPoint;
29153
- const yDiff = bY - aY;
29154
- const xDiff = bX - aX;
29155
- const slope = yDiff / xDiff;
29156
- const yAtXMin = slope * (xMin - aX) + aY;
29157
- const yAtXMax = slope * (xMax - aX) + aY;
29158
- const xAtYMin = (yMin - aY) / slope + aX;
29159
- const xAtYMax = (yMax - aY) / slope + aX;
29160
-
29161
- // clock analogy to describe quadrants
29162
- switch (true) {
29163
- // 12 o'clock to 2:59
29164
- case yDiff > 0 && xDiff >= 0:
29165
- return xAtYMax > xMax ? [xMax, yAtXMax] : [xAtYMax, yMax];
29166
- // 3 o'clock to 5:59
29167
- case yDiff <= 0 && xDiff > 0:
29168
- // xAtYMin evaluates to -Infinity here, so we use absolute value
29169
- return Math.abs(xAtYMin) > xMax ? [xMax, yAtXMax] : [xAtYMin, yMin];
29170
- // 9 o'clock to 11:59
29171
- case yDiff >= 0 && xDiff < 0:
29172
- return xAtYMax < xMin ? [xMin, yAtXMin] : [xAtYMax, yMax];
29173
- // 6 o'clock to 8:59
29174
- case yDiff < 0 && xDiff <= 0:
29175
- return xAtYMin < xMin ? [xMin, yAtXMin] : [xAtYMin, yMin];
29176
- default:
29177
- return [xMax, yAtXMax];
29178
- }
29179
- };
29180
- const getLines = points => points.map((point, i) => {
29181
- const next = points[(i + 1) % points.length];
29182
- return [point, next];
29183
- });
29184
-
29185
- function SVGLine(props) {
29186
- const {
29187
- start,
29188
- end,
29189
- style,
29190
- className,
29191
- testId
29192
- } = props;
29193
- return /*#__PURE__*/React__namespace.createElement("line", {
29194
- x1: start[0],
29195
- y1: start[1],
29196
- x2: end[0],
29197
- y2: end[1],
29198
- style: style,
29199
- className: `${className}`,
29200
- "data-testid": testId
29201
- });
29202
- }
29203
-
29204
- function pathBuilder() {
29205
- return new PathBuilder();
29206
- }
29207
- class PathBuilder {
29208
- path = [];
29209
- scaleFactor = 1;
29210
- build() {
29211
- return this.path.map(scaleCommandBy(this.scaleFactor)).map(commandToString).join("");
29212
- }
29213
-
29214
- // add a move (M) command to the path
29215
- move(x, y) {
29216
- this.path.push({
29217
- action: "M",
29218
- coords: [x, y]
29219
- });
29220
- return this;
29221
- }
29222
-
29223
- // add a curve (C) command to the path
29224
- curve(control1X, control1Y, control2X, control2Y, endX, endY) {
29225
- this.path.push({
29226
- action: "C",
29227
- coords: [control1X, control1Y, control2X, control2Y, endX, endY]
29228
- });
29229
- return this;
29230
- }
29231
-
29232
- // Scale all coordinates of the path by a factor
29233
- scale(factor) {
29234
- this.scaleFactor *= factor;
29235
- return this;
29236
- }
29237
- }
29238
- function commandToString(command) {
29239
- return `${command.action}${command.coords.join(" ")}`;
29240
- }
29241
- function scaleCommandBy(scaleFactor) {
29242
- return command => ({
29243
- ...command,
29244
- coords: command.coords.map(c => c * scaleFactor)
29245
- });
29246
- }
29247
-
29248
- // We use the pathBuilder here to scale up the SVG path coordinates used
29249
- // elsewhere for arrowheads. We scale in the path itself instead of using a CSS
29250
- // transform because a transform would also scale up the stroke weight, and we
29251
- // don't want that.
29252
- const arrowPath = pathBuilder().move(-3, 4).curve(-2.75, 2.5, 0, 0.25, 0.75, 0).curve(0, -0.25, -2.75, -2.5, -3, -4).scale(1.4).build();
29253
- function Arrowhead(props) {
29254
- const [point] = useTransformVectorsToPixels(props.tip);
29255
- return /*#__PURE__*/React__namespace.createElement("g", {
29256
- transform: `translate(${point[0]} ${point[1]}) rotate(${-props.angle})`
29257
- }, /*#__PURE__*/React__namespace.createElement("g", {
29258
- transform: "translate(-1.5)"
29259
- }, /*#__PURE__*/React__namespace.createElement("path", {
29260
- d: arrowPath,
29261
- fill: "none",
29262
- style: {
29263
- stroke: props.color ?? "inherit"
29264
- },
29265
- strokeLinejoin: "round",
29266
- strokeLinecap: "round"
29267
- })));
29268
- }
29269
-
29270
- function Vector(props) {
29271
- const {
29272
- tail,
29273
- tip,
29274
- color
29275
- } = props;
29276
- const [tailPx, tipPx] = useTransformVectorsToPixels(tail, tip);
29277
- const direction = mafs.vec.sub(tip, tail);
29278
- return /*#__PURE__*/React__namespace.createElement("g", {
29279
- style: {
29280
- stroke: color,
29281
- strokeWidth: 2
29282
- }
29283
- }, /*#__PURE__*/React__namespace.createElement(SVGLine, {
29284
- start: tailPx,
29285
- end: tipPx
29286
- }), /*#__PURE__*/React__namespace.createElement(Arrowhead, {
29287
- tip: tip,
29288
- angle: angleDegrees(direction)
29289
- }));
29290
- }
29291
- function angleDegrees(_ref) {
29292
- let [x, y] = _ref;
29293
- return Math.atan2(y, x) * 180 / Math.PI;
29294
- }
29295
-
29296
29310
  const defaultStroke = "var(--movable-line-stroke-color)";
29297
29311
  const MovableLine = props => {
29298
29312
  const {
@@ -31278,7 +31292,8 @@ const MafsGraph = props => {
31278
31292
  containerSizeClass: props.containerSizeClass,
31279
31293
  markings: props.markings
31280
31294
  }), props.lockedFigures && /*#__PURE__*/React__namespace.createElement(GraphLockedLayer, {
31281
- lockedFigures: props.lockedFigures
31295
+ lockedFigures: props.lockedFigures,
31296
+ range: props.range
31282
31297
  }), renderGraph({
31283
31298
  state,
31284
31299
  dispatch
@@ -43374,7 +43389,7 @@ const init = function (options) {
43374
43389
 
43375
43390
  // This file is processed by a Rollup plugin (replace) to inject the production
43376
43391
  const libName = "@khanacademy/perseus";
43377
- const libVersion = "22.1.0";
43392
+ const libVersion = "22.2.0";
43378
43393
  perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
43379
43394
 
43380
43395
  const apiVersion = {