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.
- package/.eslintrc.js +1 -0
- package/CHANGELOG.md +3 -0
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/src/Color4.js +3 -3
- package/dist/cjs/src/Editor.js +5 -5
- package/dist/cjs/src/SVGLoader.js +69 -7
- package/dist/cjs/src/Viewport.d.ts +2 -0
- package/dist/cjs/src/Viewport.js +6 -2
- package/dist/cjs/src/components/{ImageBackground.d.ts → BackgroundComponent.d.ts} +23 -3
- package/dist/cjs/src/components/BackgroundComponent.js +309 -0
- package/dist/cjs/src/components/Stroke.js +1 -1
- package/dist/cjs/src/components/TextComponent.d.ts +1 -13
- package/dist/cjs/src/components/TextComponent.js +1 -1
- package/dist/cjs/src/components/lib.d.ts +2 -2
- package/dist/cjs/src/components/lib.js +2 -2
- package/dist/cjs/src/components/util/StrokeSmoother.js +25 -15
- package/dist/cjs/src/localizations/de.js +1 -1
- package/dist/cjs/src/localizations/es.js +1 -1
- package/dist/cjs/src/math/Path.js +1 -1
- package/dist/cjs/src/math/polynomial/QuadraticBezier.d.ts +28 -0
- package/dist/cjs/src/math/polynomial/QuadraticBezier.js +115 -0
- package/dist/cjs/src/math/polynomial/solveQuadratic.d.ts +6 -0
- package/dist/cjs/src/math/polynomial/solveQuadratic.js +36 -0
- package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +5 -3
- package/dist/cjs/src/rendering/renderers/SVGRenderer.js +15 -6
- package/dist/cjs/src/toolbar/localization.d.ts +2 -1
- package/dist/cjs/src/toolbar/localization.js +2 -1
- package/dist/cjs/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +5 -0
- package/dist/cjs/src/toolbar/widgets/DocumentPropertiesWidget.js +77 -2
- package/dist/cjs/src/toolbar/widgets/PenToolWidget.js +1 -1
- package/dist/cjs/src/tools/FindTool.js +1 -1
- package/dist/cjs/src/tools/SoundUITool.js +1 -1
- package/dist/mjs/src/Color4.mjs +3 -3
- package/dist/mjs/src/Editor.mjs +4 -4
- package/dist/mjs/src/SVGLoader.mjs +68 -6
- package/dist/mjs/src/Viewport.d.ts +2 -0
- package/dist/mjs/src/Viewport.mjs +6 -2
- package/dist/mjs/src/components/{ImageBackground.d.ts → BackgroundComponent.d.ts} +23 -3
- package/dist/mjs/src/components/BackgroundComponent.mjs +279 -0
- package/dist/mjs/src/components/Stroke.mjs +1 -1
- package/dist/mjs/src/components/TextComponent.d.ts +1 -13
- package/dist/mjs/src/components/TextComponent.mjs +1 -1
- package/dist/mjs/src/components/lib.d.ts +2 -2
- package/dist/mjs/src/components/lib.mjs +2 -2
- package/dist/mjs/src/components/util/StrokeSmoother.mjs +25 -15
- package/dist/mjs/src/localizations/de.mjs +1 -1
- package/dist/mjs/src/localizations/es.mjs +1 -1
- package/dist/mjs/src/math/Path.mjs +1 -1
- package/dist/mjs/src/math/polynomial/QuadraticBezier.d.ts +28 -0
- package/dist/mjs/src/math/polynomial/QuadraticBezier.mjs +109 -0
- package/dist/mjs/src/math/polynomial/solveQuadratic.d.ts +6 -0
- package/dist/mjs/src/math/polynomial/solveQuadratic.mjs +34 -0
- package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +5 -3
- package/dist/mjs/src/rendering/renderers/SVGRenderer.mjs +15 -6
- package/dist/mjs/src/toolbar/localization.d.ts +2 -1
- package/dist/mjs/src/toolbar/localization.mjs +2 -1
- package/dist/mjs/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +5 -0
- package/dist/mjs/src/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -2
- package/dist/mjs/src/toolbar/widgets/PenToolWidget.mjs +1 -1
- package/dist/mjs/src/tools/FindTool.mjs +1 -1
- package/dist/mjs/src/tools/SoundUITool.mjs +1 -1
- package/jest.config.js +1 -1
- package/package.json +14 -14
- package/dist/cjs/src/components/ImageBackground.js +0 -146
- 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 *
|
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;
|
@@ -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
|
13
|
-
export { Stroke, TextComponent as Text, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent, Stroke as StrokeComponent,
|
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
|
12
|
-
export { Stroke, TextComponent as Text, createRestyleComponentCommand, isRestylableComponent, TextComponent, Stroke as StrokeComponent,
|
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 =
|
42
|
-
const controlPt =
|
43
|
-
const endPt =
|
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 =
|
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 =
|
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 =
|
75
|
-
const endPt =
|
76
|
-
const controlPoint =
|
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
|
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
|
180
|
-
if (isNaN(
|
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
|
-
|
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',
|
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',
|
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(
|
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,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;
|