js-draw 0.19.0 → 0.20.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.
Files changed (66) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +3 -0
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/src/Color4.js +3 -3
  6. package/dist/cjs/src/Editor.js +5 -5
  7. package/dist/cjs/src/SVGLoader.js +69 -7
  8. package/dist/cjs/src/Viewport.d.ts +2 -0
  9. package/dist/cjs/src/Viewport.js +6 -2
  10. package/dist/cjs/src/components/{ImageBackground.d.ts → BackgroundComponent.d.ts} +23 -3
  11. package/dist/cjs/src/components/BackgroundComponent.js +309 -0
  12. package/dist/cjs/src/components/Stroke.js +1 -1
  13. package/dist/cjs/src/components/TextComponent.d.ts +1 -13
  14. package/dist/cjs/src/components/TextComponent.js +1 -1
  15. package/dist/cjs/src/components/lib.d.ts +2 -2
  16. package/dist/cjs/src/components/lib.js +2 -2
  17. package/dist/cjs/src/components/util/StrokeSmoother.js +25 -15
  18. package/dist/cjs/src/localizations/de.js +1 -1
  19. package/dist/cjs/src/localizations/es.js +1 -1
  20. package/dist/cjs/src/math/Path.js +1 -1
  21. package/dist/cjs/src/math/polynomial/QuadraticBezier.d.ts +28 -0
  22. package/dist/cjs/src/math/polynomial/QuadraticBezier.js +115 -0
  23. package/dist/cjs/src/math/polynomial/solveQuadratic.d.ts +6 -0
  24. package/dist/cjs/src/math/polynomial/solveQuadratic.js +36 -0
  25. package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +5 -3
  26. package/dist/cjs/src/rendering/renderers/SVGRenderer.js +15 -6
  27. package/dist/cjs/src/toolbar/localization.d.ts +2 -1
  28. package/dist/cjs/src/toolbar/localization.js +2 -1
  29. package/dist/cjs/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +5 -0
  30. package/dist/cjs/src/toolbar/widgets/DocumentPropertiesWidget.js +77 -2
  31. package/dist/cjs/src/toolbar/widgets/PenToolWidget.js +1 -1
  32. package/dist/cjs/src/tools/FindTool.js +1 -1
  33. package/dist/cjs/src/tools/SoundUITool.js +1 -1
  34. package/dist/mjs/src/Color4.mjs +3 -3
  35. package/dist/mjs/src/Editor.mjs +4 -4
  36. package/dist/mjs/src/SVGLoader.mjs +68 -6
  37. package/dist/mjs/src/Viewport.d.ts +2 -0
  38. package/dist/mjs/src/Viewport.mjs +6 -2
  39. package/dist/mjs/src/components/{ImageBackground.d.ts → BackgroundComponent.d.ts} +23 -3
  40. package/dist/mjs/src/components/BackgroundComponent.mjs +279 -0
  41. package/dist/mjs/src/components/Stroke.mjs +1 -1
  42. package/dist/mjs/src/components/TextComponent.d.ts +1 -13
  43. package/dist/mjs/src/components/TextComponent.mjs +1 -1
  44. package/dist/mjs/src/components/lib.d.ts +2 -2
  45. package/dist/mjs/src/components/lib.mjs +2 -2
  46. package/dist/mjs/src/components/util/StrokeSmoother.mjs +25 -15
  47. package/dist/mjs/src/localizations/de.mjs +1 -1
  48. package/dist/mjs/src/localizations/es.mjs +1 -1
  49. package/dist/mjs/src/math/Path.mjs +1 -1
  50. package/dist/mjs/src/math/polynomial/QuadraticBezier.d.ts +28 -0
  51. package/dist/mjs/src/math/polynomial/QuadraticBezier.mjs +109 -0
  52. package/dist/mjs/src/math/polynomial/solveQuadratic.d.ts +6 -0
  53. package/dist/mjs/src/math/polynomial/solveQuadratic.mjs +34 -0
  54. package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +5 -3
  55. package/dist/mjs/src/rendering/renderers/SVGRenderer.mjs +15 -6
  56. package/dist/mjs/src/toolbar/localization.d.ts +2 -1
  57. package/dist/mjs/src/toolbar/localization.mjs +2 -1
  58. package/dist/mjs/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +5 -0
  59. package/dist/mjs/src/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -2
  60. package/dist/mjs/src/toolbar/widgets/PenToolWidget.mjs +1 -1
  61. package/dist/mjs/src/tools/FindTool.mjs +1 -1
  62. package/dist/mjs/src/tools/SoundUITool.mjs +1 -1
  63. package/jest.config.js +1 -1
  64. package/package.json +14 -14
  65. package/dist/cjs/src/components/ImageBackground.js +0 -146
  66. package/dist/mjs/src/components/ImageBackground.mjs +0 -139
@@ -0,0 +1,279 @@
1
+ import Color4 from '../Color4.mjs';
2
+ import { EditorImageEventType } from '../EditorImage.mjs';
3
+ import Rect2 from '../math/Rect2.mjs';
4
+ import AbstractComponent from './AbstractComponent.mjs';
5
+ import { createRestyleComponentCommand } from './RestylableComponent.mjs';
6
+ import Path, { PathCommandType } from '../math/Path.mjs';
7
+ import { Vec2 } from '../math/Vec2.mjs';
8
+ import Viewport from '../Viewport.mjs';
9
+ import { toRoundedString } from '../math/rounding.mjs';
10
+ export var BackgroundType;
11
+ (function (BackgroundType) {
12
+ BackgroundType[BackgroundType["SolidColor"] = 0] = "SolidColor";
13
+ BackgroundType[BackgroundType["Grid"] = 1] = "Grid";
14
+ BackgroundType[BackgroundType["None"] = 2] = "None";
15
+ })(BackgroundType || (BackgroundType = {}));
16
+ export const imageBackgroundCSSClassName = 'js-draw-image-background';
17
+ // Class name prefix indicating the size of the background's grid cells (if present).
18
+ export const imageBackgroundGridSizeCSSPrefix = 'js-draw-image-background-grid-';
19
+ // Flag included in rendered SVGs (etc) that indicates that the secondary color of the
20
+ // background has been manually set.
21
+ export const imageBackgroundNonAutomaticSecondaryColorCSSClassName = 'js-draw-image-background-non-automatic-secondary-color';
22
+ export const backgroundTypeToClassNameMap = {
23
+ [BackgroundType.Grid]: 'js-draw-image-background-grid',
24
+ [BackgroundType.SolidColor]: imageBackgroundCSSClassName,
25
+ [BackgroundType.None]: '',
26
+ };
27
+ // Represents the background of the editor's canvas.
28
+ export default class BackgroundComponent extends AbstractComponent {
29
+ constructor(backgroundType, mainColor) {
30
+ super('image-background', 0);
31
+ this.backgroundType = backgroundType;
32
+ this.mainColor = mainColor;
33
+ this.viewportSizeChangeListener = null;
34
+ this.gridSize = Viewport.getGridSize(2);
35
+ this.gridStrokeWidth = 0.7;
36
+ this.secondaryColor = null;
37
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
38
+ this.isRestylableComponent = true;
39
+ this.contentBBox = Rect2.empty;
40
+ }
41
+ static ofGrid(backgroundColor, gridSize, gridColor, gridStrokeWidth) {
42
+ const background = new BackgroundComponent(BackgroundType.Grid, backgroundColor);
43
+ if (gridSize !== undefined) {
44
+ background.gridSize = gridSize;
45
+ }
46
+ if (gridColor !== undefined) {
47
+ background.secondaryColor = gridColor;
48
+ }
49
+ if (gridStrokeWidth !== undefined) {
50
+ background.gridStrokeWidth = gridStrokeWidth;
51
+ }
52
+ return background;
53
+ }
54
+ getBackgroundType() {
55
+ return this.backgroundType;
56
+ }
57
+ // @internal
58
+ getMainColor() {
59
+ return this.mainColor;
60
+ }
61
+ // @internal
62
+ getSecondaryColor() {
63
+ return this.secondaryColor;
64
+ }
65
+ // @internal
66
+ getGridSize() {
67
+ return this.gridSize;
68
+ }
69
+ getStyle() {
70
+ let color = this.mainColor;
71
+ if (this.backgroundType === BackgroundType.None) {
72
+ color = undefined;
73
+ }
74
+ return {
75
+ color,
76
+ };
77
+ }
78
+ updateStyle(style) {
79
+ return createRestyleComponentCommand(this.getStyle(), style, this);
80
+ }
81
+ // @internal
82
+ forceStyle(style, editor) {
83
+ const fill = style.color;
84
+ if (!fill) {
85
+ return;
86
+ }
87
+ this.mainColor = fill;
88
+ // A solid background and transparent fill is equivalent to no background.
89
+ if (fill.eq(Color4.transparent) && this.backgroundType === BackgroundType.SolidColor) {
90
+ this.backgroundType = BackgroundType.None;
91
+ }
92
+ else if (this.backgroundType === BackgroundType.None) {
93
+ this.backgroundType = BackgroundType.SolidColor;
94
+ }
95
+ if (editor) {
96
+ editor.image.queueRerenderOf(this);
97
+ editor.queueRerender();
98
+ }
99
+ }
100
+ onAddToImage(image) {
101
+ if (this.viewportSizeChangeListener) {
102
+ console.warn('onAddToImage called when background is already in an image');
103
+ this.onRemoveFromImage();
104
+ }
105
+ this.viewportSizeChangeListener = image.notifier.on(EditorImageEventType.ExportViewportChanged, () => {
106
+ this.recomputeBBox(image);
107
+ });
108
+ this.recomputeBBox(image);
109
+ }
110
+ onRemoveFromImage() {
111
+ var _a;
112
+ (_a = this.viewportSizeChangeListener) === null || _a === void 0 ? void 0 : _a.remove();
113
+ this.viewportSizeChangeListener = null;
114
+ }
115
+ recomputeBBox(image) {
116
+ const importExportRect = image.getImportExportViewport().visibleRect;
117
+ if (!this.contentBBox.eq(importExportRect)) {
118
+ this.contentBBox = importExportRect;
119
+ // Re-render this if already added to the EditorImage.
120
+ image.queueRerenderOf(this);
121
+ }
122
+ }
123
+ generateGridPath(visibleRect) {
124
+ var _a, _b;
125
+ const targetRect = (_b = (_a = visibleRect === null || visibleRect === void 0 ? void 0 : visibleRect.grownBy(this.gridStrokeWidth)) === null || _a === void 0 ? void 0 : _a.intersection(this.contentBBox)) !== null && _b !== void 0 ? _b : this.contentBBox;
126
+ const roundDownToGrid = (coord) => Math.floor(coord / this.gridSize) * this.gridSize;
127
+ const roundUpToGrid = (coord) => Math.ceil(coord / this.gridSize) * this.gridSize;
128
+ const startY = roundUpToGrid(targetRect.y);
129
+ const endY = roundDownToGrid(targetRect.y + targetRect.h);
130
+ const startX = roundUpToGrid(targetRect.x);
131
+ const endX = roundDownToGrid(targetRect.x + targetRect.w);
132
+ const result = [];
133
+ // Don't generate grids with a huge number of rows/columns -- such grids
134
+ // take a long time to render and are likely invisible due to the number of
135
+ // cells.
136
+ const rowCount = (endY - startY) / this.gridSize;
137
+ const colCount = (endX - startX) / this.gridSize;
138
+ const maxGridCols = 1000;
139
+ const maxGridRows = 1000;
140
+ if (rowCount > maxGridRows || colCount > maxGridCols) {
141
+ return Path.empty;
142
+ }
143
+ const startPoint = Vec2.of(targetRect.x, startY);
144
+ for (let y = startY; y <= endY; y += this.gridSize) {
145
+ result.push({
146
+ kind: PathCommandType.MoveTo,
147
+ point: Vec2.of(targetRect.x, y),
148
+ });
149
+ result.push({
150
+ kind: PathCommandType.LineTo,
151
+ point: Vec2.of(targetRect.x + targetRect.w, y),
152
+ });
153
+ }
154
+ for (let x = startX; x <= endX; x += this.gridSize) {
155
+ result.push({
156
+ kind: PathCommandType.MoveTo,
157
+ point: Vec2.of(x, targetRect.y),
158
+ });
159
+ result.push({
160
+ kind: PathCommandType.LineTo,
161
+ point: Vec2.of(x, targetRect.y + targetRect.h)
162
+ });
163
+ }
164
+ return new Path(startPoint, result);
165
+ }
166
+ render(canvas, visibleRect) {
167
+ if (this.backgroundType === BackgroundType.None) {
168
+ return;
169
+ }
170
+ const clip = true;
171
+ canvas.startObject(this.contentBBox, clip);
172
+ if (this.backgroundType === BackgroundType.SolidColor || this.backgroundType === BackgroundType.Grid) {
173
+ // If the rectangle for this region contains the visible rect,
174
+ // we can fill the entire visible rectangle (which may be more efficient than
175
+ // filling the entire region for this.)
176
+ if (visibleRect) {
177
+ const intersection = visibleRect.intersection(this.contentBBox);
178
+ if (intersection) {
179
+ canvas.fillRect(intersection, this.mainColor);
180
+ }
181
+ }
182
+ else {
183
+ canvas.fillRect(this.contentBBox, this.mainColor);
184
+ }
185
+ }
186
+ if (this.backgroundType === BackgroundType.Grid) {
187
+ let gridColor = this.secondaryColor;
188
+ gridColor !== null && gridColor !== void 0 ? gridColor : (gridColor = Color4.ofRGBA(1 - this.mainColor.r, 1 - this.mainColor.g, 1 - this.mainColor.b, 0.2));
189
+ // If the background fill is completely transparent, ensure visibility on otherwise light
190
+ // and dark backgrounds.
191
+ if (this.mainColor.a === 0) {
192
+ gridColor = Color4.ofRGBA(0.5, 0.5, 0.5, 0.2);
193
+ }
194
+ const style = {
195
+ fill: Color4.transparent,
196
+ stroke: { width: this.gridStrokeWidth, color: gridColor }
197
+ };
198
+ canvas.drawPath(this.generateGridPath(visibleRect).toRenderable(style));
199
+ }
200
+ const backgroundTypeCSSClass = backgroundTypeToClassNameMap[this.backgroundType];
201
+ const classNames = [imageBackgroundCSSClassName];
202
+ if (backgroundTypeCSSClass !== imageBackgroundCSSClassName) {
203
+ classNames.push(backgroundTypeCSSClass);
204
+ const gridSizeStr = toRoundedString(this.gridSize).replace(/[.]/g, 'p');
205
+ classNames.push(imageBackgroundGridSizeCSSPrefix + gridSizeStr);
206
+ }
207
+ if (this.secondaryColor !== null) {
208
+ classNames.push(imageBackgroundNonAutomaticSecondaryColorCSSClassName);
209
+ }
210
+ canvas.endObject(this.getLoadSaveData(), classNames);
211
+ }
212
+ intersects(lineSegment) {
213
+ return this.contentBBox.getEdges().some(edge => edge.intersects(lineSegment));
214
+ }
215
+ isSelectable() {
216
+ return false;
217
+ }
218
+ isBackground() {
219
+ return true;
220
+ }
221
+ serializeToJSON() {
222
+ var _a;
223
+ return {
224
+ mainColor: this.mainColor.toHexString(),
225
+ secondaryColor: (_a = this.secondaryColor) === null || _a === void 0 ? void 0 : _a.toHexString(),
226
+ backgroundType: this.backgroundType,
227
+ gridSize: this.gridSize,
228
+ gridStrokeWidth: this.gridStrokeWidth,
229
+ };
230
+ }
231
+ applyTransformation(_affineTransfm) {
232
+ // Do nothing — it doesn't make sense to transform the background.
233
+ }
234
+ description(localizationTable) {
235
+ if (this.backgroundType === BackgroundType.SolidColor) {
236
+ return localizationTable.filledBackgroundWithColor(this.mainColor.toString());
237
+ }
238
+ else {
239
+ return localizationTable.emptyBackground;
240
+ }
241
+ }
242
+ createClone() {
243
+ return new BackgroundComponent(this.backgroundType, this.mainColor);
244
+ }
245
+ // @internal
246
+ static deserializeFromJSON(json) {
247
+ var _a, _b;
248
+ if (typeof json === 'string') {
249
+ json = JSON.parse(json);
250
+ }
251
+ if (typeof json.mainColor !== 'string') {
252
+ throw new Error('Error deserializing — mainColor must be of type string.');
253
+ }
254
+ let backgroundType;
255
+ const jsonBackgroundType = json.backgroundType;
256
+ if (jsonBackgroundType === BackgroundType.None || jsonBackgroundType === BackgroundType.Grid
257
+ || jsonBackgroundType === BackgroundType.SolidColor) {
258
+ backgroundType = jsonBackgroundType;
259
+ }
260
+ else {
261
+ const exhaustivenessCheck = jsonBackgroundType;
262
+ return exhaustivenessCheck;
263
+ }
264
+ const mainColor = Color4.fromHex(json.mainColor);
265
+ const secondaryColor = json.secondaryColor ? Color4.fromHex(json.secondaryColor) : null;
266
+ const gridSize = (_a = json.gridSize) !== null && _a !== void 0 ? _a : undefined;
267
+ const gridStrokeWidth = (_b = json.gridStrokeWidth) !== null && _b !== void 0 ? _b : undefined;
268
+ const result = new BackgroundComponent(backgroundType, mainColor);
269
+ result.secondaryColor = secondaryColor;
270
+ if (gridSize) {
271
+ result.gridSize = gridSize;
272
+ }
273
+ if (gridStrokeWidth) {
274
+ result.gridStrokeWidth = gridStrokeWidth;
275
+ }
276
+ return result;
277
+ }
278
+ }
279
+ AbstractComponent.registerComponent('image-background', BackgroundComponent.deserializeFromJSON);
@@ -125,7 +125,7 @@ export default class Stroke extends AbstractComponent {
125
125
  if (!bbox.intersects(visibleRect)) {
126
126
  continue;
127
127
  }
128
- const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
128
+ const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 3 || bbox.size.y > visibleRect.size.y * 3;
129
129
  if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, (_b = (_a = part.style.stroke) === null || _a === void 0 ? void 0 : _a.width) !== null && _b !== void 0 ? _b : 0)) {
130
130
  continue;
131
131
  }
@@ -36,19 +36,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
36
36
  getStyle(): ComponentStyle;
37
37
  updateStyle(style: ComponentStyle): SerializableCommand;
38
38
  forceStyle(style: ComponentStyle, editor: Editor | null): void;
39
- getTextStyle(): {
40
- renderingStyle: {
41
- fill: import("../Color4").default;
42
- stroke: {
43
- color: import("../Color4").default;
44
- width: number;
45
- } | undefined;
46
- };
47
- size: number;
48
- fontFamily: string;
49
- fontWeight?: string | undefined;
50
- fontVariant?: string | undefined;
51
- };
39
+ getTextStyle(): TextRenderingStyle;
52
40
  getBaselinePos(): import("../lib").Vec3;
53
41
  getTransform(): Mat33;
54
42
  protected applyTransformation(affineTransfm: Mat33): void;
@@ -158,7 +158,7 @@ export default class TextComponent extends AbstractComponent {
158
158
  editor.queueRerender();
159
159
  }
160
160
  }
161
- // See this.getStyle
161
+ // See {@link getStyle}
162
162
  getTextStyle() {
163
163
  return cloneTextStyle(this.style);
164
164
  }
@@ -9,5 +9,5 @@ import TextComponent from './TextComponent';
9
9
  import ImageComponent from './ImageComponent';
10
10
  import RestyleableComponent from './RestylableComponent';
11
11
  import { createRestyleComponentCommand, isRestylableComponent, ComponentStyle as RestyleableComponentStyle } from './RestylableComponent';
12
- import ImageBackground from './ImageBackground';
13
- export { Stroke, TextComponent as Text, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent, Stroke as StrokeComponent, ImageBackground as BackgroundComponent, ImageComponent, };
12
+ import BackgroundComponent from './BackgroundComponent';
13
+ export { Stroke, TextComponent as Text, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
@@ -8,5 +8,5 @@ import Stroke from './Stroke.mjs';
8
8
  import TextComponent from './TextComponent.mjs';
9
9
  import ImageComponent from './ImageComponent.mjs';
10
10
  import { createRestyleComponentCommand, isRestylableComponent } from './RestylableComponent.mjs';
11
- import ImageBackground from './ImageBackground.mjs';
12
- export { Stroke, TextComponent as Text, createRestyleComponentCommand, isRestylableComponent, TextComponent, Stroke as StrokeComponent, ImageBackground as BackgroundComponent, ImageComponent, };
11
+ import BackgroundComponent from './BackgroundComponent.mjs';
12
+ export { Stroke, TextComponent as Text, createRestyleComponentCommand, isRestylableComponent, TextComponent, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
@@ -1,7 +1,7 @@
1
- import { Bezier } from 'bezier-js';
2
1
  import { Vec2 } from '../../math/Vec2.mjs';
3
2
  import Rect2 from '../../math/Rect2.mjs';
4
3
  import LineSegment2 from '../../math/LineSegment2.mjs';
4
+ import QuadraticBezier from '../../math/polynomial/QuadraticBezier.mjs';
5
5
  // Handles stroke smoothing
6
6
  export class StrokeSmoother {
7
7
  constructor(startPoint,
@@ -38,9 +38,9 @@ export class StrokeSmoother {
38
38
  if (!this.currentCurve) {
39
39
  return 0;
40
40
  }
41
- const startPt = Vec2.ofXY(this.currentCurve.points[0]);
42
- const controlPt = Vec2.ofXY(this.currentCurve.points[1]);
43
- const endPt = Vec2.ofXY(this.currentCurve.points[2]);
41
+ const startPt = this.currentCurve.p0;
42
+ const controlPt = this.currentCurve.p1;
43
+ const endPt = this.currentCurve.p2;
44
44
  const toControlDist = startPt.minus(controlPt).length();
45
45
  const toEndDist = endPt.minus(controlPt).length();
46
46
  return toControlDist + toEndDist;
@@ -52,7 +52,7 @@ export class StrokeSmoother {
52
52
  }
53
53
  this.onCurveAdded(this.currentSegmentToPath());
54
54
  const lastPoint = this.buffer[this.buffer.length - 1];
55
- this.lastExitingVec = Vec2.ofXY(this.currentCurve.points[2]).minus(Vec2.ofXY(this.currentCurve.points[1]));
55
+ this.lastExitingVec = this.currentCurve.p2.minus(this.currentCurve.p1);
56
56
  console.assert(this.lastExitingVec.magnitude() !== 0, 'lastExitingVec has zero length!');
57
57
  // Use the last two points to start a new curve (the last point isn't used
58
58
  // in the current curve and we want connected curves to share end points)
@@ -67,13 +67,13 @@ export class StrokeSmoother {
67
67
  if (this.currentCurve == null) {
68
68
  throw new Error('Invalid State: currentCurve is null!');
69
69
  }
70
- const startVec = Vec2.ofXY(this.currentCurve.normal(0)).normalized();
70
+ const startVec = this.currentCurve.normal(0).normalized();
71
71
  if (!isFinite(startVec.magnitude())) {
72
72
  throw new Error(`startVec(${startVec}) is NaN or ∞`);
73
73
  }
74
- const startPt = Vec2.ofXY(this.currentCurve.get(0));
75
- const endPt = Vec2.ofXY(this.currentCurve.get(1));
76
- const controlPoint = Vec2.ofXY(this.currentCurve.points[1]);
74
+ const startPt = this.currentCurve.at(0);
75
+ const endPt = this.currentCurve.at(1);
76
+ const controlPoint = this.currentCurve.p1;
77
77
  return {
78
78
  startPoint: startPt,
79
79
  controlPoint,
@@ -125,7 +125,7 @@ export class StrokeSmoother {
125
125
  const p2 = lastPoint.pos.plus((_b = this.lastExitingVec) !== null && _b !== void 0 ? _b : Vec2.unitX);
126
126
  const p3 = newPoint.pos;
127
127
  // Quadratic Bézier curve
128
- this.currentCurve = new Bezier(p1.xy, p2.xy, p3.xy);
128
+ this.currentCurve = new QuadraticBezier(p1, p2, p3);
129
129
  console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
130
130
  if (this.isFirstSegment) {
131
131
  // The start of a curve often lacks accurate pressure information. Update it.
@@ -176,19 +176,29 @@ export class StrokeSmoother {
176
176
  console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
177
177
  console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
178
178
  const prevCurve = this.currentCurve;
179
- this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
180
- if (isNaN(Vec2.ofXY(this.currentCurve.normal(0)).magnitude())) {
179
+ this.currentCurve = new QuadraticBezier(segmentStart, controlPoint, segmentEnd);
180
+ if (isNaN(this.currentCurve.normal(0).magnitude())) {
181
181
  console.error('NaN normal at 0. Curve:', this.currentCurve);
182
182
  this.currentCurve = prevCurve;
183
183
  }
184
184
  // Should we start making a new curve? Check whether all buffer points are within
185
185
  // ±strokeWidth of the curve.
186
186
  const curveMatchesPoints = (curve) => {
187
+ let nonMatching = 0;
188
+ const maxNonMatching = 2;
189
+ const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 3, this.minFitAllowed);
187
190
  for (const point of this.buffer) {
188
- const proj = Vec2.ofXY(curve.project(point.xy));
189
- const dist = proj.minus(point).magnitude();
190
- const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 3, this.minFitAllowed);
191
+ let dist = curve.approximateDistance(point);
191
192
  if (dist > minFit || dist > this.maxFitAllowed) {
193
+ // Avoid using the slower .distance
194
+ if (nonMatching >= maxNonMatching - 1) {
195
+ dist = curve.distance(point);
196
+ }
197
+ if (dist > minFit || dist > this.maxFitAllowed) {
198
+ nonMatching++;
199
+ }
200
+ }
201
+ if (nonMatching >= maxNonMatching) {
192
202
  return false;
193
203
  }
194
204
  }
@@ -1,4 +1,4 @@
1
1
  import { defaultEditorLocalization } from '../localization.mjs';
2
2
  // German localization
3
- const localization = Object.assign(Object.assign({}, defaultEditorLocalization), { pen: 'Stift', eraser: 'Radierer', select: 'Auswahl', handTool: 'Verschieben', zoom: 'Vergrößerung', resetView: 'Ansicht zurücksetzen', thicknessLabel: 'Dicke: ', colorLabel: 'Farbe: ', fontLabel: 'Schriftart: ', resizeImageToSelection: 'Bildgröße an Auswahl anpassen', deleteSelection: 'Auswahl löschen', duplicateSelection: 'Auswahl duplizieren', undo: 'Rückgängig', redo: 'Wiederholen', pickColorFromScreen: 'Farbe von Bildschirm auswählen', clickToPickColorAnnouncement: 'Klicke auf den Bildschirm, um eine Farbe auszuwählen', selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.', touchPanning: 'Ansicht mit Touchscreen verschieben', anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben', selectObjectType: 'Objekt-Typ: ', freehandPen: 'Freihand', arrowPen: 'Pfeil', linePen: 'Linie', outlinedRectanglePen: 'Umrissenes Rechteck', filledRectanglePen: 'Ausgefülltes Rechteck', dropdownShown: t => `Dropdown-Menü für ${t} angezeigt`, dropdownHidden: t => `Dropdown-Menü für ${t} versteckt`, zoomLevel: t => `Vergößerung: ${t}%`, colorChangedAnnouncement: t => `Farbe zu ${t} geändert`, penTool: t => `Stift ${t}`, selectionTool: 'Auswahl', eraserTool: 'Radiergummi', touchPanTool: 'Ansicht mit Touchscreen verschieben', twoFingerPanZoomTool: 'Ansicht verschieben und vergrößern', undoRedoTool: 'Rückgängig/Wiederholen', rightClickDragPanTool: 'Rechtsklick-Ziehen', pipetteTool: 'Farbe von Bildschirm auswählen', keyboardPanZoom: 'Tastaturkürzel zum Verschieben/Vergrößern der Ansicht', textTool: 'Text', enterTextToInsert: 'Einzufügender Text', toolEnabledAnnouncement: t => `${t} aktiviert`, toolDisabledAnnouncement: t => `${t} deaktiviert`, updatedViewport: 'Transformierte Ansicht', transformedElements: t => `${t} Element${1 === t ? '' : 'e'} transformiert`, resizeOutputCommand: t => `Bildgröße auf ${t.w}x${t.h} geändert`, addElementAction: t => `${t} hinzugefügt`, eraseAction: (t, e) => `${e} ${t} gelöscht`, duplicateAction: (t, e) => `${e} ${t} dupliziert`, inverseOf: t => `Umkehrung von ${t}`, elements: 'Elemente', erasedNoElements: 'Nichts entfernt', duplicatedNoElements: 'Nichts dupliziert', rotatedBy: t => `${Math.abs(t)} Grad ${t < 0 ? 'im Uhrzeigersinn' : 'gegen den Uhrzeigersinn'} gedreht`, movedLeft: 'Nacht links bewegt', movedUp: 'Nacht oben bewegt', movedDown: 'Nacht unten bewegt', movedRight: 'Nacht rechts bewegt', zoomedOut: 'Ansicht verkleinert', zoomedIn: 'Ansicht vergrößert', selectedElements: t => `${t} Element${1 === t ? '' : 'e'} ausgewählt`, stroke: 'Strich', svgObject: 'SVG-Objekt', text: t => `Text-Objekt: ${t}`, pathNodeCount: t => `Es gibt ${t} sichtbare Pfad-Objekte.`, textNodeCount: t => `Es gibt ${t} sichtbare Text-Knotenpunkte.`, textNode: t => `Text: ${t}`, rerenderAsText: 'Als Text darstellen', accessibilityInputInstructions: 'Drücke ‚t‘, um den Inhalt des Ansichtsfensters als Text zu lesen. Verwende die Pfeiltasten, um die Ansicht zu verschieben, und klicke und ziehe, um Striche zu zeichnen. Drücke ‚w‘ zum Vergrößern und ‚s‘ zum Verkleinern der Ansicht.', loading: t => `Laden ${t}%...`, doneLoading: 'Laden fertig', imageEditor: 'Bild-Editor', undoAnnouncement: t => `Rückgangig gemacht ${t}`, redoAnnouncement: t => `Wiederholt ${t}` });
3
+ const localization = Object.assign(Object.assign({}, defaultEditorLocalization), { pen: 'Stift', eraser: 'Radierer', select: 'Auswahl', handTool: 'Verschieben', zoom: 'Vergrößerung', resetView: 'Ansicht zurücksetzen', thicknessLabel: 'Dicke: ', colorLabel: 'Farbe: ', fontLabel: 'Schriftart: ', resizeImageToSelection: 'Bildgröße an Auswahl anpassen', deleteSelection: 'Auswahl löschen', duplicateSelection: 'Auswahl duplizieren', undo: 'Rückgängig', redo: 'Wiederholen', pickColorFromScreen: 'Farbe von Bildschirm auswählen', clickToPickColorAnnouncement: 'Klicke auf den Bildschirm, um eine Farbe auszuwählen', selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.', touchPanning: 'Ansicht mit Touchscreen verschieben', anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben', selectPenType: 'Objekt-Typ: ', freehandPen: 'Freihand', arrowPen: 'Pfeil', linePen: 'Linie', outlinedRectanglePen: 'Umrissenes Rechteck', filledRectanglePen: 'Ausgefülltes Rechteck', dropdownShown: t => `Dropdown-Menü für ${t} angezeigt`, dropdownHidden: t => `Dropdown-Menü für ${t} versteckt`, zoomLevel: t => `Vergößerung: ${t}%`, colorChangedAnnouncement: t => `Farbe zu ${t} geändert`, penTool: t => `Stift ${t}`, selectionTool: 'Auswahl', eraserTool: 'Radiergummi', touchPanTool: 'Ansicht mit Touchscreen verschieben', twoFingerPanZoomTool: 'Ansicht verschieben und vergrößern', undoRedoTool: 'Rückgängig/Wiederholen', rightClickDragPanTool: 'Rechtsklick-Ziehen', pipetteTool: 'Farbe von Bildschirm auswählen', keyboardPanZoom: 'Tastaturkürzel zum Verschieben/Vergrößern der Ansicht', textTool: 'Text', enterTextToInsert: 'Einzufügender Text', toolEnabledAnnouncement: t => `${t} aktiviert`, toolDisabledAnnouncement: t => `${t} deaktiviert`, updatedViewport: 'Transformierte Ansicht', transformedElements: t => `${t} Element${1 === t ? '' : 'e'} transformiert`, resizeOutputCommand: t => `Bildgröße auf ${t.w}x${t.h} geändert`, addElementAction: t => `${t} hinzugefügt`, eraseAction: (t, e) => `${e} ${t} gelöscht`, duplicateAction: (t, e) => `${e} ${t} dupliziert`, inverseOf: t => `Umkehrung von ${t}`, elements: 'Elemente', erasedNoElements: 'Nichts entfernt', duplicatedNoElements: 'Nichts dupliziert', rotatedBy: t => `${Math.abs(t)} Grad ${t < 0 ? 'im Uhrzeigersinn' : 'gegen den Uhrzeigersinn'} gedreht`, movedLeft: 'Nacht links bewegt', movedUp: 'Nacht oben bewegt', movedDown: 'Nacht unten bewegt', movedRight: 'Nacht rechts bewegt', zoomedOut: 'Ansicht verkleinert', zoomedIn: 'Ansicht vergrößert', selectedElements: t => `${t} Element${1 === t ? '' : 'e'} ausgewählt`, stroke: 'Strich', svgObject: 'SVG-Objekt', text: t => `Text-Objekt: ${t}`, pathNodeCount: t => `Es gibt ${t} sichtbare Pfad-Objekte.`, textNodeCount: t => `Es gibt ${t} sichtbare Text-Knotenpunkte.`, textNode: t => `Text: ${t}`, rerenderAsText: 'Als Text darstellen', accessibilityInputInstructions: 'Drücke ‚t‘, um den Inhalt des Ansichtsfensters als Text zu lesen. Verwende die Pfeiltasten, um die Ansicht zu verschieben, und klicke und ziehe, um Striche zu zeichnen. Drücke ‚w‘ zum Vergrößern und ‚s‘ zum Verkleinern der Ansicht.', loading: t => `Laden ${t}%...`, doneLoading: 'Laden fertig', imageEditor: 'Bild-Editor', undoAnnouncement: t => `Rückgangig gemacht ${t}`, redoAnnouncement: t => `Wiederholt ${t}` });
4
4
  export default localization;
@@ -6,7 +6,7 @@ const localization = Object.assign(Object.assign({}, defaultEditorLocalization),
6
6
  loading: (percentage) => `Cargando: ${percentage}%...`, imageEditor: 'Editor de dibujos', undoAnnouncement: (commandDescription) => `${commandDescription} fue deshecho`, redoAnnouncement: (commandDescription) => `${commandDescription} fue rehecho`, undo: 'Deshace', redo: 'Rehace',
7
7
  // Strings for the toolbar
8
8
  // (see src/toolbar/localization.ts)
9
- pen: 'Lapiz', eraser: 'Borrador', select: 'Selecciona', thicknessLabel: 'Tamaño: ', colorLabel: 'Color: ', doneLoading: 'El cargado terminó', fontLabel: 'Fuente: ', anyDevicePanning: 'Mover la pantalla con todo dispotivo', touchPanning: 'Mover la pantalla con un dedo', touchPanTool: 'Instrumento de mover la pantalla con un dedo', outlinedRectanglePen: 'Rectángulo con nada más que un borde', filledRectanglePen: 'Rectángulo sin borde', linePen: 'Línea', arrowPen: 'Flecha', freehandPen: 'Dibuja sin restricción de forma', selectObjectType: 'Forma de dibuja:', handTool: 'Mover', zoom: 'Zoom', resetView: 'Reiniciar vista', resizeImageToSelection: 'Redimensionar la imagen a lo que está seleccionado', deleteSelection: 'Borra la selección', duplicateSelection: 'Duplica la selección', pickColorFromScreen: 'Selecciona un color de la pantalla', clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color', dropdownShown(toolName) {
9
+ pen: 'Lapiz', eraser: 'Borrador', select: 'Selecciona', thicknessLabel: 'Tamaño: ', colorLabel: 'Color: ', doneLoading: 'El cargado terminó', fontLabel: 'Fuente: ', anyDevicePanning: 'Mover la pantalla con todo dispotivo', touchPanning: 'Mover la pantalla con un dedo', touchPanTool: 'Instrumento de mover la pantalla con un dedo', outlinedRectanglePen: 'Rectángulo con nada más que un borde', filledRectanglePen: 'Rectángulo sin borde', linePen: 'Línea', arrowPen: 'Flecha', freehandPen: 'Dibuja sin restricción de forma', selectPenType: 'Forma de dibuja:', handTool: 'Mover', zoom: 'Zoom', resetView: 'Reiniciar vista', resizeImageToSelection: 'Redimensionar la imagen a lo que está seleccionado', deleteSelection: 'Borra la selección', duplicateSelection: 'Duplica la selección', pickColorFromScreen: 'Selecciona un color de la pantalla', clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color', dropdownShown(toolName) {
10
10
  return `Menú por ${toolName} es visible`;
11
11
  }, dropdownHidden: function (toolName) {
12
12
  return `Menú por ${toolName} fue ocultado`;
@@ -341,7 +341,7 @@ export default class Path {
341
341
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
342
342
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
343
343
  const expandedRect = visibleRect.grownBy(strokeWidth)
344
- .transformedBoundingBox(Mat33.scaling2D(2, visibleRect.center));
344
+ .transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
345
345
  // TODO: Handle simplifying very small paths.
346
346
  if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
347
347
  return renderablePath;
@@ -0,0 +1,28 @@
1
+ import { Point2, Vec2 } from '../Vec2';
2
+ export default class QuadraticBezier {
3
+ readonly p0: Point2;
4
+ readonly p1: Point2;
5
+ readonly p2: Point2;
6
+ private bezierJs;
7
+ constructor(p0: Point2, p1: Point2, p2: Point2);
8
+ /**
9
+ * Returns a component of a quadratic Bézier curve at t, where p0,p1,p2 are either all x or
10
+ * all y components of the target curve.
11
+ */
12
+ private static componentAt;
13
+ private static derivativeComponentAt;
14
+ /**
15
+ * @returns the curve evaluated at `t`.
16
+ */
17
+ at(t: number): Point2;
18
+ derivativeAt(t: number): Point2;
19
+ /**
20
+ * @returns the approximate distance from `point` to this curve.
21
+ */
22
+ approximateDistance(point: Point2): number;
23
+ /**
24
+ * @returns the exact distance from `point` to this.
25
+ */
26
+ distance(point: Point2): number;
27
+ normal(t: number): Vec2;
28
+ }
@@ -0,0 +1,109 @@
1
+ import { Bezier } from 'bezier-js';
2
+ import { Vec2 } from '../Vec2.mjs';
3
+ import solveQuadratic from './solveQuadratic.mjs';
4
+ export default class QuadraticBezier {
5
+ constructor(p0, p1, p2) {
6
+ this.p0 = p0;
7
+ this.p1 = p1;
8
+ this.p2 = p2;
9
+ this.bezierJs = null;
10
+ }
11
+ /**
12
+ * Returns a component of a quadratic Bézier curve at t, where p0,p1,p2 are either all x or
13
+ * all y components of the target curve.
14
+ */
15
+ static componentAt(t, p0, p1, p2) {
16
+ return p0 + t * (-2 * p0 + 2 * p1) + t * t * (p0 - 2 * p1 + p2);
17
+ }
18
+ static derivativeComponentAt(t, p0, p1, p2) {
19
+ return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
20
+ }
21
+ /**
22
+ * @returns the curve evaluated at `t`.
23
+ */
24
+ at(t) {
25
+ const p0 = this.p0;
26
+ const p1 = this.p1;
27
+ const p2 = this.p2;
28
+ return Vec2.of(QuadraticBezier.componentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.componentAt(t, p0.y, p1.y, p2.y));
29
+ }
30
+ derivativeAt(t) {
31
+ const p0 = this.p0;
32
+ const p1 = this.p1;
33
+ const p2 = this.p2;
34
+ return Vec2.of(QuadraticBezier.derivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.derivativeComponentAt(t, p0.y, p1.y, p2.y));
35
+ }
36
+ /**
37
+ * @returns the approximate distance from `point` to this curve.
38
+ */
39
+ approximateDistance(point) {
40
+ // We want to minimize f(t) = |B(t) - p|².
41
+ // Expanding,
42
+ // f(t) = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)²
43
+ // ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)²
44
+ //
45
+ // Considering just one component,
46
+ // Dₜ(Bₓ(t) - pₓ)² = 2(Bₓ(t) - pₓ)(DₜBₓ(t))
47
+ // = 2(Bₓ(t)DₜBₓ(t) - pₓBₓ(t))
48
+ // = 2(p0ₓ + (t)(-2p0ₓ + 2p1ₓ) + (t²)(p0ₓ - 2p1ₓ + p2ₓ) - pₓ)((-2p0ₓ + 2p1ₓ) + 2(t)(p0ₓ - 2p1ₓ + p2ₓ))
49
+ // - (pₓ)((-2p0ₓ + 2p1ₓ) + (t)(p0ₓ - 2p1ₓ + p2ₓ))
50
+ const A = this.p0.x - point.x;
51
+ const B = -2 * this.p0.x + 2 * this.p1.x;
52
+ const C = this.p0.x - 2 * this.p1.x + this.p2.x;
53
+ // Let A = p0ₓ - pₓ, B = -2p0ₓ + 2p1ₓ, C = p0ₓ - 2p1ₓ + p2ₓ. We then have,
54
+ // Dₜ(Bₓ(t) - pₓ)²
55
+ // = 2(A + tB + t²C)(B + 2tC) - (pₓ)(B + 2tC)
56
+ // = 2(AB + tB² + t²BC + 2tCA + 2tCtB + 2tCt²C) - pₓB - pₓ2tC
57
+ // = 2(AB + tB² + 2tCA + t²BC + 2t²CB + 2C²t³) - pₓB - pₓ2tC
58
+ // = 2AB + 2t(B² + 2CA) + 2t²(BC + 2CB) + 4C²t³ - pₓB - pₓ2tC
59
+ // = 2AB + 2t(B² + 2CA - pₓC) + 2t²(BC + 2CB) + 4C²t³ - pₓB
60
+ //
61
+ const D = this.p0.y - point.y;
62
+ const E = -2 * this.p0.y + 2 * this.p1.y;
63
+ const F = this.p0.y - 2 * this.p1.y + this.p2.y;
64
+ // Using D = p0ᵧ - pᵧ, E = -2p0ᵧ + 2p1ᵧ, F = p0ᵧ - 2p1ᵧ + p2ᵧ, we thus have,
65
+ // f'(t) = 2AB + 2t(B² + 2CA - pₓC) + 2t²(BC + 2CB) + 4C²t³ - pₓB
66
+ // + 2DE + 2t(E² + 2FD - pᵧF) + 2t²(EF + 2FE) + 4F²t³ - pᵧE
67
+ const a = 2 * A * B + 2 * D * E - point.x * B - point.y * E;
68
+ const b = 2 * B * B + 2 * E * E + 2 * C * A + 2 * F * D - point.x * C - point.y * F;
69
+ const c = 2 * E * F + 2 * B * C + 2 * C * B + 2 * F * E;
70
+ //const d = 4 * C * C + 4 * F * F;
71
+ // Thus,
72
+ // f'(t) = a + bt + ct² + dt³
73
+ const fDerivAtZero = a;
74
+ const f2ndDerivAtZero = b;
75
+ const f3rdDerivAtZero = 2 * c;
76
+ // Using the first few terms of a Maclaurin series to approximate f'(t),
77
+ // f'(t) ≈ f'(0) + t f''(0) + t² f'''(0) / 2
78
+ let [min1, min2] = solveQuadratic(f3rdDerivAtZero / 2, f2ndDerivAtZero, fDerivAtZero);
79
+ // If the quadratic has no solutions, approximate.
80
+ if (isNaN(min1)) {
81
+ min1 = 0.25;
82
+ }
83
+ if (isNaN(min2)) {
84
+ min2 = 0.75;
85
+ }
86
+ const at1 = this.at(min1);
87
+ const at2 = this.at(min2);
88
+ const sqrDist1 = at1.minus(point).magnitudeSquared();
89
+ const sqrDist2 = at2.minus(point).magnitudeSquared();
90
+ const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
91
+ const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
92
+ return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
93
+ }
94
+ /**
95
+ * @returns the exact distance from `point` to this.
96
+ */
97
+ distance(point) {
98
+ if (!this.bezierJs) {
99
+ this.bezierJs = new Bezier([this.p0.xy, this.p1.xy, this.p2.xy]);
100
+ }
101
+ const proj = Vec2.ofXY(this.bezierJs.project(point.xy));
102
+ const dist = proj.minus(point).magnitude();
103
+ return dist;
104
+ }
105
+ normal(t) {
106
+ const tangent = this.derivativeAt(t);
107
+ return tangent.orthog().normalized();
108
+ }
109
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Solves an equation of the form ax² + bx + c = 0.
3
+ * The larger solution is returned first.
4
+ */
5
+ declare const solveQuadratic: (a: number, b: number, c: number) => [number, number];
6
+ export default solveQuadratic;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Solves an equation of the form ax² + bx + c = 0.
3
+ * The larger solution is returned first.
4
+ */
5
+ const solveQuadratic = (a, b, c) => {
6
+ // See also https://en.wikipedia.org/wiki/Quadratic_formula
7
+ if (a === 0) {
8
+ let solution;
9
+ if (b === 0) {
10
+ solution = c === 0 ? 0 : NaN;
11
+ }
12
+ else {
13
+ // Then we have bx + c = 0
14
+ // which implies bx = -c.
15
+ // Thus, x = -c/b
16
+ solution = -c / b;
17
+ }
18
+ return [solution, solution];
19
+ }
20
+ const discriminant = b * b - 4 * a * c;
21
+ if (discriminant < 0) {
22
+ return [NaN, NaN];
23
+ }
24
+ const rootDiscriminant = Math.sqrt(discriminant);
25
+ const solution1 = (-b + rootDiscriminant) / (2 * a);
26
+ const solution2 = (-b - rootDiscriminant) / (2 * a);
27
+ if (solution1 > solution2) {
28
+ return [solution1, solution2];
29
+ }
30
+ else {
31
+ return [solution2, solution1];
32
+ }
33
+ };
34
+ export default solveQuadratic;