js-draw 0.19.0 → 0.21.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 (88) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +4 -4
  4. package/dist/bundle.js +2 -2
  5. package/dist/bundledStyles.js +1 -1
  6. package/dist/cjs/src/Color4.js +3 -3
  7. package/dist/cjs/src/Editor.d.ts +4 -1
  8. package/dist/cjs/src/Editor.js +30 -12
  9. package/dist/cjs/src/SVGLoader.js +69 -7
  10. package/dist/cjs/src/Viewport.d.ts +2 -0
  11. package/dist/cjs/src/Viewport.js +6 -2
  12. package/dist/cjs/src/components/AbstractComponent.d.ts +13 -1
  13. package/dist/cjs/src/components/AbstractComponent.js +19 -9
  14. package/dist/cjs/src/components/{ImageBackground.d.ts → BackgroundComponent.d.ts} +23 -3
  15. package/dist/cjs/src/components/BackgroundComponent.js +309 -0
  16. package/dist/cjs/src/components/Stroke.d.ts +1 -0
  17. package/dist/cjs/src/components/Stroke.js +15 -2
  18. package/dist/cjs/src/components/TextComponent.d.ts +1 -13
  19. package/dist/cjs/src/components/TextComponent.js +1 -1
  20. package/dist/cjs/src/components/lib.d.ts +2 -2
  21. package/dist/cjs/src/components/lib.js +2 -2
  22. package/dist/cjs/src/components/util/StrokeSmoother.js +26 -18
  23. package/dist/cjs/src/localizations/de.js +1 -1
  24. package/dist/cjs/src/localizations/es.js +1 -1
  25. package/dist/cjs/src/math/LineSegment2.d.ts +2 -0
  26. package/dist/cjs/src/math/LineSegment2.js +4 -0
  27. package/dist/cjs/src/math/Path.d.ts +24 -3
  28. package/dist/cjs/src/math/Path.js +225 -4
  29. package/dist/cjs/src/math/Rect2.js +4 -3
  30. package/dist/cjs/src/math/polynomial/QuadraticBezier.d.ts +28 -0
  31. package/dist/cjs/src/math/polynomial/QuadraticBezier.js +114 -0
  32. package/dist/cjs/src/math/polynomial/solveQuadratic.d.ts +6 -0
  33. package/dist/cjs/src/math/polynomial/solveQuadratic.js +36 -0
  34. package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +5 -3
  35. package/dist/cjs/src/rendering/renderers/SVGRenderer.js +15 -6
  36. package/dist/cjs/src/toolbar/HTMLToolbar.js +7 -0
  37. package/dist/cjs/src/toolbar/localization.d.ts +2 -1
  38. package/dist/cjs/src/toolbar/localization.js +2 -1
  39. package/dist/cjs/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +5 -0
  40. package/dist/cjs/src/toolbar/widgets/DocumentPropertiesWidget.js +77 -2
  41. package/dist/cjs/src/toolbar/widgets/PenToolWidget.js +1 -1
  42. package/dist/cjs/src/tools/FindTool.js +1 -1
  43. package/dist/cjs/src/tools/SoundUITool.js +1 -1
  44. package/dist/mjs/src/Color4.mjs +3 -3
  45. package/dist/mjs/src/Editor.d.ts +4 -1
  46. package/dist/mjs/src/Editor.mjs +29 -11
  47. package/dist/mjs/src/SVGLoader.mjs +68 -6
  48. package/dist/mjs/src/Viewport.d.ts +2 -0
  49. package/dist/mjs/src/Viewport.mjs +6 -2
  50. package/dist/mjs/src/components/AbstractComponent.d.ts +13 -1
  51. package/dist/mjs/src/components/AbstractComponent.mjs +19 -9
  52. package/dist/mjs/src/components/{ImageBackground.d.ts → BackgroundComponent.d.ts} +23 -3
  53. package/dist/mjs/src/components/BackgroundComponent.mjs +279 -0
  54. package/dist/mjs/src/components/Stroke.d.ts +1 -0
  55. package/dist/mjs/src/components/Stroke.mjs +15 -2
  56. package/dist/mjs/src/components/TextComponent.d.ts +1 -13
  57. package/dist/mjs/src/components/TextComponent.mjs +1 -1
  58. package/dist/mjs/src/components/lib.d.ts +2 -2
  59. package/dist/mjs/src/components/lib.mjs +2 -2
  60. package/dist/mjs/src/components/util/StrokeSmoother.mjs +26 -18
  61. package/dist/mjs/src/localizations/de.mjs +1 -1
  62. package/dist/mjs/src/localizations/es.mjs +1 -1
  63. package/dist/mjs/src/math/LineSegment2.d.ts +2 -0
  64. package/dist/mjs/src/math/LineSegment2.mjs +4 -0
  65. package/dist/mjs/src/math/Path.d.ts +24 -3
  66. package/dist/mjs/src/math/Path.mjs +225 -4
  67. package/dist/mjs/src/math/Rect2.mjs +4 -3
  68. package/dist/mjs/src/math/polynomial/QuadraticBezier.d.ts +28 -0
  69. package/dist/mjs/src/math/polynomial/QuadraticBezier.mjs +108 -0
  70. package/dist/mjs/src/math/polynomial/solveQuadratic.d.ts +6 -0
  71. package/dist/mjs/src/math/polynomial/solveQuadratic.mjs +34 -0
  72. package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +5 -3
  73. package/dist/mjs/src/rendering/renderers/SVGRenderer.mjs +15 -6
  74. package/dist/mjs/src/toolbar/HTMLToolbar.mjs +8 -1
  75. package/dist/mjs/src/toolbar/localization.d.ts +2 -1
  76. package/dist/mjs/src/toolbar/localization.mjs +2 -1
  77. package/dist/mjs/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +5 -0
  78. package/dist/mjs/src/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -2
  79. package/dist/mjs/src/toolbar/widgets/PenToolWidget.mjs +1 -1
  80. package/dist/mjs/src/tools/FindTool.mjs +1 -1
  81. package/dist/mjs/src/tools/SoundUITool.mjs +1 -1
  82. package/jest.config.js +1 -1
  83. package/package.json +14 -14
  84. package/src/Coloris.css +52 -0
  85. package/src/Editor.css +12 -0
  86. package/src/toolbar/toolbar.css +9 -0
  87. package/dist/cjs/src/components/ImageBackground.js +0 -146
  88. package/dist/mjs/src/components/ImageBackground.mjs +0 -139
@@ -10,6 +10,7 @@ const LineSegment2_1 = __importDefault(require("./LineSegment2"));
10
10
  const Mat33_1 = __importDefault(require("./Mat33"));
11
11
  const Rect2_1 = __importDefault(require("./Rect2"));
12
12
  const Vec2_1 = require("./Vec2");
13
+ const Vec3_1 = __importDefault(require("./Vec3"));
13
14
  var PathCommandType;
14
15
  (function (PathCommandType) {
15
16
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -17,6 +18,23 @@ var PathCommandType;
17
18
  PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
18
19
  PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
19
20
  })(PathCommandType = exports.PathCommandType || (exports.PathCommandType = {}));
21
+ // Returns the bounding box of one path segment.
22
+ const getPartBBox = (part) => {
23
+ let partBBox;
24
+ if (part instanceof LineSegment2_1.default) {
25
+ partBBox = part.bbox;
26
+ }
27
+ else if (part instanceof bezier_js_1.Bezier) {
28
+ const bbox = part.bbox();
29
+ const width = bbox.x.max - bbox.x.min;
30
+ const height = bbox.y.max - bbox.y.min;
31
+ partBBox = new Rect2_1.default(bbox.x.min, bbox.y.min, width, height);
32
+ }
33
+ else {
34
+ partBBox = new Rect2_1.default(part.x, part.y, 0, 0);
35
+ }
36
+ return partBBox;
37
+ };
20
38
  class Path {
21
39
  constructor(startPoint, parts) {
22
40
  this.startPoint = startPoint;
@@ -32,6 +50,13 @@ class Path {
32
50
  this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
33
51
  }
34
52
  }
53
+ getExactBBox() {
54
+ const bboxes = [];
55
+ for (const part of this.geometry) {
56
+ bboxes.push(getPartBBox(part));
57
+ }
58
+ return Rect2_1.default.union(...bboxes);
59
+ }
35
60
  // Lazy-loads and returns this path's geometry
36
61
  get geometry() {
37
62
  if (this.cachedGeometry) {
@@ -54,6 +79,7 @@ class Path {
54
79
  startPoint = part.point;
55
80
  break;
56
81
  case PathCommandType.MoveTo:
82
+ geometry.push(part.point);
57
83
  startPoint = part.point;
58
84
  break;
59
85
  }
@@ -109,11 +135,197 @@ class Path {
109
135
  }
110
136
  return Rect2_1.default.bboxOf(points);
111
137
  }
112
- intersection(line) {
113
- if (!line.bbox.intersects(this.bbox)) {
138
+ /**
139
+ * Let `S` be a closed path a distance `strokeRadius` from this path.
140
+ *
141
+ * @returns Approximate intersections of `line` with `S` using ray marching, starting from
142
+ * both end points of `line` and each point in `additionalRaymarchStartPoints`.
143
+ */
144
+ raymarchIntersectionWith(line, strokeRadius, additionalRaymarchStartPoints = []) {
145
+ var _a, _b;
146
+ // No intersection between bounding boxes: No possible intersection
147
+ // of the interior.
148
+ if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius))) {
149
+ return [];
150
+ }
151
+ const lineLength = line.length;
152
+ const partDistFunctionRecords = [];
153
+ // Determine distance functions for all parts that the given line could possibly intersect with
154
+ for (const part of this.geometry) {
155
+ const bbox = getPartBBox(part).grownBy(strokeRadius);
156
+ if (!bbox.intersects(line.bbox)) {
157
+ continue;
158
+ }
159
+ // Signed distance function
160
+ let partDist;
161
+ if (part instanceof LineSegment2_1.default) {
162
+ partDist = (point) => part.distance(point);
163
+ }
164
+ else if (part instanceof Vec3_1.default) {
165
+ partDist = (point) => part.minus(point).magnitude();
166
+ }
167
+ else {
168
+ partDist = (point) => {
169
+ return part.project(point).d;
170
+ };
171
+ }
172
+ // Part signed distance function (negative result implies `point` is
173
+ // inside the shape).
174
+ const partSdf = (point) => partDist(point) - strokeRadius;
175
+ // If the line can't possibly intersect the part,
176
+ if (partSdf(line.p1) > lineLength && partSdf(line.p2) > lineLength) {
177
+ continue;
178
+ }
179
+ partDistFunctionRecords.push({
180
+ part,
181
+ distFn: partDist,
182
+ bbox,
183
+ });
184
+ }
185
+ // If no distance functions, there are no intersections.
186
+ if (partDistFunctionRecords.length === 0) {
114
187
  return [];
115
188
  }
189
+ // Returns the minimum distance to a part in this stroke, where only parts that the given
190
+ // line could intersect are considered.
191
+ const sdf = (point) => {
192
+ let minDist = Infinity;
193
+ let minDistPart = null;
194
+ const uncheckedDistFunctions = [];
195
+ // First pass: only curves for which the current point is inside
196
+ // the bounding box.
197
+ for (const distFnRecord of partDistFunctionRecords) {
198
+ const { part, distFn, bbox } = distFnRecord;
199
+ // Check later if the current point isn't in the bounding box.
200
+ if (!bbox.containsPoint(point)) {
201
+ uncheckedDistFunctions.push(distFnRecord);
202
+ continue;
203
+ }
204
+ const currentDist = distFn(point);
205
+ if (currentDist <= minDist) {
206
+ minDist = currentDist;
207
+ minDistPart = part;
208
+ }
209
+ }
210
+ // Second pass: Everything else
211
+ for (const { part, distFn, bbox } of uncheckedDistFunctions) {
212
+ // Skip if impossible for the distance to the target to be lesser than
213
+ // the current minimum.
214
+ if (!bbox.grownBy(minDist).containsPoint(point)) {
215
+ continue;
216
+ }
217
+ const currentDist = distFn(point);
218
+ if (currentDist <= minDist) {
219
+ minDist = currentDist;
220
+ minDistPart = part;
221
+ }
222
+ }
223
+ return [minDistPart, minDist - strokeRadius];
224
+ };
225
+ // Raymarch:
226
+ const maxRaymarchSteps = 7;
227
+ // Start raymarching from each of these points. This allows detection of multiple
228
+ // intersections.
229
+ const startPoints = [
230
+ line.p1, ...additionalRaymarchStartPoints, line.p2
231
+ ];
232
+ // Converts a point ON THE LINE to a parameter
233
+ const pointToParameter = (point) => {
234
+ // Because line.direction is a unit vector, this computes the length
235
+ // of the projection of the vector(line.p1->point) onto line.direction.
236
+ //
237
+ // Note that this can be negative if the given point is outside of the given
238
+ // line segment.
239
+ return point.minus(line.p1).dot(line.direction);
240
+ };
241
+ // Sort start points by parameter on the line.
242
+ // This allows us to determine whether the current value of a parameter
243
+ // drops down to a value already tested.
244
+ startPoints.sort((a, b) => {
245
+ const t_a = pointToParameter(a);
246
+ const t_b = pointToParameter(b);
247
+ // Sort in increasing order
248
+ return t_a - t_b;
249
+ });
116
250
  const result = [];
251
+ const stoppingThreshold = strokeRadius / 1000;
252
+ // Returns the maximum x value explored
253
+ const raymarchFrom = (startPoint,
254
+ // Direction to march in (multiplies line.direction)
255
+ directionMultiplier,
256
+ // Terminate if the current point corresponds to a parameter
257
+ // below this.
258
+ minimumLineParameter) => {
259
+ let currentPoint = startPoint;
260
+ let [lastPart, lastDist] = sdf(currentPoint);
261
+ let lastParameter = pointToParameter(currentPoint);
262
+ if (lastDist > lineLength) {
263
+ return lastParameter;
264
+ }
265
+ const direction = line.direction.times(directionMultiplier);
266
+ for (let i = 0; i < maxRaymarchSteps; i++) {
267
+ // Step in the direction of the edge of the shape.
268
+ const step = lastDist;
269
+ currentPoint = currentPoint.plus(direction.times(step));
270
+ lastParameter = pointToParameter(currentPoint);
271
+ // If we're below the minimum parameter, stop. We've already tried
272
+ // this.
273
+ if (lastParameter <= minimumLineParameter) {
274
+ return lastParameter;
275
+ }
276
+ const [currentPart, signedDist] = sdf(currentPoint);
277
+ // Ensure we're stepping in the correct direction.
278
+ // Note that because we could start with a negative distance and work towards a
279
+ // positive distance, we need absolute values here.
280
+ if (Math.abs(signedDist) > Math.abs(lastDist)) {
281
+ // If not, stop.
282
+ return null;
283
+ }
284
+ lastDist = signedDist;
285
+ lastPart = currentPart;
286
+ // Is the distance close enough that we can stop early?
287
+ if (Math.abs(lastDist) < stoppingThreshold) {
288
+ break;
289
+ }
290
+ }
291
+ // Ensure that the point we ended with is on the line.
292
+ const isOnLineSegment = lastParameter >= 0 && lastParameter <= lineLength;
293
+ if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
294
+ result.push({
295
+ point: currentPoint,
296
+ parameterValue: NaN,
297
+ curve: lastPart,
298
+ });
299
+ }
300
+ return lastParameter;
301
+ };
302
+ // The maximum value of the line's parameter explored so far (0 corresponds to
303
+ // line.p1)
304
+ let maxLineT = 0;
305
+ // Raymarch for each start point.
306
+ //
307
+ // Use a for (i from 0 to length) loop because startPoints may be added
308
+ // during iteration.
309
+ for (let i = 0; i < startPoints.length; i++) {
310
+ const startPoint = startPoints[i];
311
+ // Try raymarching in both directions.
312
+ maxLineT = Math.max(maxLineT, (_a = raymarchFrom(startPoint, 1, maxLineT)) !== null && _a !== void 0 ? _a : maxLineT);
313
+ maxLineT = Math.max(maxLineT, (_b = raymarchFrom(startPoint, -1, maxLineT)) !== null && _b !== void 0 ? _b : maxLineT);
314
+ }
315
+ return result;
316
+ }
317
+ /**
318
+ * Returns a list of intersections with this path. If `strokeRadius` is given,
319
+ * intersections are approximated with the surface `strokeRadius` away from this.
320
+ *
321
+ * If `strokeRadius > 0`, the resultant `parameterValue` has no defined value.
322
+ */
323
+ intersection(line, strokeRadius) {
324
+ let result = [];
325
+ // Is any intersection between shapes within the bounding boxes impossible?
326
+ if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius !== null && strokeRadius !== void 0 ? strokeRadius : 0))) {
327
+ return [];
328
+ }
117
329
  for (const part of this.geometry) {
118
330
  if (part instanceof LineSegment2_1.default) {
119
331
  const intersection = part.intersection(line);
@@ -125,7 +337,7 @@ class Path {
125
337
  });
126
338
  }
127
339
  }
128
- else {
340
+ else if (part instanceof bezier_js_1.Bezier) {
129
341
  const intersectionPoints = part.intersects(line).map(t => {
130
342
  // We're using the .intersects(line) function, which is documented
131
343
  // to always return numbers. However, to satisfy the type checker (and
@@ -148,6 +360,15 @@ class Path {
148
360
  result.push(...intersectionPoints);
149
361
  }
150
362
  }
363
+ // If given a non-zero strokeWidth, attempt to raymarch.
364
+ // Even if raymarching, we need to collect starting points.
365
+ // We use the above-calculated intersections for this.
366
+ const doRaymarching = strokeRadius && strokeRadius > 1e-8;
367
+ if (doRaymarching) {
368
+ // Starting points for raymarching (in addition to the end points of the line).
369
+ const startPoints = result.map(intersection => intersection.point);
370
+ result = this.raymarchIntersectionWith(line, strokeRadius, startPoints);
371
+ }
151
372
  return result;
152
373
  }
153
374
  static mapPathCommand(part, mapping) {
@@ -347,7 +568,7 @@ class Path {
347
568
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
348
569
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
349
570
  const expandedRect = visibleRect.grownBy(strokeWidth)
350
- .transformedBoundingBox(Mat33_1.default.scaling2D(2, visibleRect.center));
571
+ .transformedBoundingBox(Mat33_1.default.scaling2D(4, visibleRect.center));
351
572
  // TODO: Handle simplifying very small paths.
352
573
  if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
353
574
  return renderablePath;
@@ -73,9 +73,7 @@ class Rect2 {
73
73
  }
74
74
  // Returns a new rectangle containing both [this] and [other].
75
75
  union(other) {
76
- const topLeft = this.topLeft.zip(other.topLeft, Math.min);
77
- const bottomRight = this.bottomRight.zip(other.bottomRight, Math.max);
78
- return Rect2.fromCorners(topLeft, bottomRight);
76
+ return Rect2.union(this, other);
79
77
  }
80
78
  // Returns a the subdivision of this into [columns] columns
81
79
  // and [rows] rows. For example,
@@ -113,6 +111,9 @@ class Rect2 {
113
111
  }
114
112
  // Returns this grown by [margin] in both the x and y directions.
115
113
  grownBy(margin) {
114
+ if (margin === 0) {
115
+ return this;
116
+ }
116
117
  return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
117
118
  }
118
119
  getClosestPointOnBoundaryTo(target) {
@@ -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 (more) exact distance from `point` to this.
25
+ */
26
+ distance(point: Point2): number;
27
+ normal(t: number): Vec2;
28
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const bezier_js_1 = require("bezier-js");
7
+ const Vec2_1 = require("../Vec2");
8
+ const solveQuadratic_1 = __importDefault(require("./solveQuadratic"));
9
+ class QuadraticBezier {
10
+ constructor(p0, p1, p2) {
11
+ this.p0 = p0;
12
+ this.p1 = p1;
13
+ this.p2 = p2;
14
+ this.bezierJs = null;
15
+ }
16
+ /**
17
+ * Returns a component of a quadratic Bézier curve at t, where p0,p1,p2 are either all x or
18
+ * all y components of the target curve.
19
+ */
20
+ static componentAt(t, p0, p1, p2) {
21
+ return p0 + t * (-2 * p0 + 2 * p1) + t * t * (p0 - 2 * p1 + p2);
22
+ }
23
+ static derivativeComponentAt(t, p0, p1, p2) {
24
+ return -2 * p0 + 2 * p1 + 2 * t * (p0 - 2 * p1 + p2);
25
+ }
26
+ /**
27
+ * @returns the curve evaluated at `t`.
28
+ */
29
+ at(t) {
30
+ const p0 = this.p0;
31
+ const p1 = this.p1;
32
+ const p2 = this.p2;
33
+ return Vec2_1.Vec2.of(QuadraticBezier.componentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.componentAt(t, p0.y, p1.y, p2.y));
34
+ }
35
+ derivativeAt(t) {
36
+ const p0 = this.p0;
37
+ const p1 = this.p1;
38
+ const p2 = this.p2;
39
+ return Vec2_1.Vec2.of(QuadraticBezier.derivativeComponentAt(t, p0.x, p1.x, p2.x), QuadraticBezier.derivativeComponentAt(t, p0.y, p1.y, p2.y));
40
+ }
41
+ /**
42
+ * @returns the approximate distance from `point` to this curve.
43
+ */
44
+ approximateDistance(point) {
45
+ // We want to minimize f(t) = |B(t) - p|².
46
+ // Expanding,
47
+ // f(t) = (Bₓ(t) - pₓ)² + (Bᵧ(t) - pᵧ)²
48
+ // ⇒ f'(t) = Dₜ(Bₓ(t) - pₓ)² + Dₜ(Bᵧ(t) - pᵧ)²
49
+ //
50
+ // Considering just one component,
51
+ // Dₜ(Bₓ(t) - pₓ)² = 2(Bₓ(t) - pₓ)(DₜBₓ(t))
52
+ // = 2(Bₓ(t)DₜBₓ(t) - pₓBₓ(t))
53
+ // = 2(p0ₓ + (t)(-2p0ₓ + 2p1ₓ) + (t²)(p0ₓ - 2p1ₓ + p2ₓ) - pₓ)((-2p0ₓ + 2p1ₓ) + 2(t)(p0ₓ - 2p1ₓ + p2ₓ))
54
+ // - (pₓ)((-2p0ₓ + 2p1ₓ) + (t)(p0ₓ - 2p1ₓ + p2ₓ))
55
+ const A = this.p0.x - point.x;
56
+ const B = -2 * this.p0.x + 2 * this.p1.x;
57
+ const C = this.p0.x - 2 * this.p1.x + this.p2.x;
58
+ // Let A = p0ₓ - pₓ, B = -2p0ₓ + 2p1ₓ, C = p0ₓ - 2p1ₓ + p2ₓ. We then have,
59
+ // Dₜ(Bₓ(t) - pₓ)²
60
+ // = 2(A + tB + t²C)(B + 2tC) - (pₓ)(B + 2tC)
61
+ // = 2(AB + tB² + t²BC + 2tCA + 2tCtB + 2tCt²C) - pₓB - pₓ2tC
62
+ // = 2(AB + tB² + 2tCA + t²BC + 2t²CB + 2C²t³) - pₓB - pₓ2tC
63
+ // = 2AB + 2t(B² + 2CA) + 2t²(BC + 2CB) + 4C²t³ - pₓB - pₓ2tC
64
+ // = 2AB + 2t(B² + 2CA - pₓC) + 2t²(BC + 2CB) + 4C²t³ - pₓB
65
+ //
66
+ const D = this.p0.y - point.y;
67
+ const E = -2 * this.p0.y + 2 * this.p1.y;
68
+ const F = this.p0.y - 2 * this.p1.y + this.p2.y;
69
+ // Using D = p0ᵧ - pᵧ, E = -2p0ᵧ + 2p1ᵧ, F = p0ᵧ - 2p1ᵧ + p2ᵧ, we thus have,
70
+ // f'(t) = 2AB + 2t(B² + 2CA - pₓC) + 2t²(BC + 2CB) + 4C²t³ - pₓB
71
+ // + 2DE + 2t(E² + 2FD - pᵧF) + 2t²(EF + 2FE) + 4F²t³ - pᵧE
72
+ const a = 2 * A * B + 2 * D * E - point.x * B - point.y * E;
73
+ const b = 2 * B * B + 2 * E * E + 2 * C * A + 2 * F * D - point.x * C - point.y * F;
74
+ const c = 2 * E * F + 2 * B * C + 2 * C * B + 2 * F * E;
75
+ //const d = 4 * C * C + 4 * F * F;
76
+ // Thus,
77
+ // f'(t) = a + bt + ct² + dt³
78
+ const fDerivAtZero = a;
79
+ const f2ndDerivAtZero = b;
80
+ const f3rdDerivAtZero = 2 * c;
81
+ // Using the first few terms of a Maclaurin series to approximate f'(t),
82
+ // f'(t) ≈ f'(0) + t f''(0) + t² f'''(0) / 2
83
+ let [min1, min2] = (0, solveQuadratic_1.default)(f3rdDerivAtZero / 2, f2ndDerivAtZero, fDerivAtZero);
84
+ // If the quadratic has no solutions, approximate.
85
+ if (isNaN(min1)) {
86
+ min1 = 0.25;
87
+ }
88
+ if (isNaN(min2)) {
89
+ min2 = 0.75;
90
+ }
91
+ const at1 = this.at(min1);
92
+ const at2 = this.at(min2);
93
+ const sqrDist1 = at1.minus(point).magnitudeSquared();
94
+ const sqrDist2 = at2.minus(point).magnitudeSquared();
95
+ const sqrDist3 = this.at(0).minus(point).magnitudeSquared();
96
+ const sqrDist4 = this.at(1).minus(point).magnitudeSquared();
97
+ return Math.sqrt(Math.min(sqrDist1, sqrDist2, sqrDist3, sqrDist4));
98
+ }
99
+ /**
100
+ * @returns the (more) exact distance from `point` to this.
101
+ */
102
+ distance(point) {
103
+ if (!this.bezierJs) {
104
+ this.bezierJs = new bezier_js_1.Bezier([this.p0.xy, this.p1.xy, this.p2.xy]);
105
+ }
106
+ // .d: Distance
107
+ return this.bezierJs.project(point.xy).d;
108
+ }
109
+ normal(t) {
110
+ const tangent = this.derivativeAt(t);
111
+ return tangent.orthog().normalized();
112
+ }
113
+ }
114
+ exports.default = QuadraticBezier;
@@ -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,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Solves an equation of the form ax² + bx + c = 0.
5
+ * The larger solution is returned first.
6
+ */
7
+ const solveQuadratic = (a, b, c) => {
8
+ // See also https://en.wikipedia.org/wiki/Quadratic_formula
9
+ if (a === 0) {
10
+ let solution;
11
+ if (b === 0) {
12
+ solution = c === 0 ? 0 : NaN;
13
+ }
14
+ else {
15
+ // Then we have bx + c = 0
16
+ // which implies bx = -c.
17
+ // Thus, x = -c/b
18
+ solution = -c / b;
19
+ }
20
+ return [solution, solution];
21
+ }
22
+ const discriminant = b * b - 4 * a * c;
23
+ if (discriminant < 0) {
24
+ return [NaN, NaN];
25
+ }
26
+ const rootDiscriminant = Math.sqrt(discriminant);
27
+ const solution1 = (-b + rootDiscriminant) / (2 * a);
28
+ const solution2 = (-b - rootDiscriminant) / (2 * a);
29
+ if (solution1 > solution2) {
30
+ return [solution1, solution2];
31
+ }
32
+ else {
33
+ return [solution2, solution1];
34
+ }
35
+ };
36
+ exports.default = solveQuadratic;
@@ -191,14 +191,16 @@ class CanvasRenderer extends AbstractRenderer_1.default {
191
191
  }
192
192
  }
193
193
  endObject() {
194
+ // Cache this.objectLevel — it may be decremented by super.endObject.
195
+ const objectLevel = this.objectLevel;
196
+ this.currentObjectBBox = null;
197
+ super.endObject();
194
198
  if (!this.ignoringObject && this.clipLevels.length > 0) {
195
- if (this.clipLevels[this.clipLevels.length - 1] === this.objectLevel) {
199
+ if (this.clipLevels[this.clipLevels.length - 1] === objectLevel) {
196
200
  this.ctx.restore();
197
201
  this.clipLevels.pop();
198
202
  }
199
203
  }
200
- this.currentObjectBBox = null;
201
- super.endObject();
202
204
  // If exiting an object with a too-small-to-draw bounding box,
203
205
  if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
204
206
  this.ignoreObjectsAboveLevel = null;
@@ -9,6 +9,7 @@ const Path_1 = __importDefault(require("../../math/Path"));
9
9
  const rounding_1 = require("../../math/rounding");
10
10
  const Vec2_1 = require("../../math/Vec2");
11
11
  const SVGLoader_1 = require("../../SVGLoader");
12
+ const RenderingStyle_1 = require("../RenderingStyle");
12
13
  const AbstractRenderer_1 = __importDefault(require("./AbstractRenderer"));
13
14
  exports.renderedStylesheetId = 'js-draw-style-sheet';
14
15
  const svgNameSpace = 'http://www.w3.org/2000/svg';
@@ -114,11 +115,10 @@ class SVGRenderer extends AbstractRenderer_1.default {
114
115
  (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
115
116
  }
116
117
  drawPath(pathSpec) {
117
- var _a;
118
118
  const style = pathSpec.style;
119
119
  const path = Path_1.default.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
120
120
  // Try to extend the previous path, if possible
121
- if (!style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) || this.lastPathString.length === 0) {
121
+ if (this.lastPathString.length === 0 || !this.lastPathStyle || !(0, RenderingStyle_1.stylesEqual)(this.lastPathStyle, style)) {
122
122
  this.addPathToSVG();
123
123
  this.lastPathStyle = style;
124
124
  this.lastPathString = [];
@@ -232,7 +232,7 @@ class SVGRenderer extends AbstractRenderer_1.default {
232
232
  this.objectElems = [];
233
233
  }
234
234
  endObject(loaderData, elemClassNames) {
235
- var _a, _b;
235
+ var _a;
236
236
  super.endObject(loaderData);
237
237
  // Don't extend paths across objects
238
238
  this.addPathToSVG();
@@ -254,9 +254,18 @@ class SVGRenderer extends AbstractRenderer_1.default {
254
254
  }
255
255
  }
256
256
  // Add class names to the object, if given.
257
- if (elemClassNames) {
258
- for (const elem of (_b = this.objectElems) !== null && _b !== void 0 ? _b : []) {
259
- elem.classList.add(...elemClassNames);
257
+ if (elemClassNames && this.objectElems) {
258
+ if (this.objectElems.length === 1) {
259
+ this.objectElems[0].classList.add(...elemClassNames);
260
+ }
261
+ else {
262
+ const wrapper = document.createElementNS(svgNameSpace, 'g');
263
+ wrapper.classList.add(...elemClassNames);
264
+ for (const elem of this.objectElems) {
265
+ elem.remove();
266
+ wrapper.appendChild(elem);
267
+ }
268
+ this.elem.appendChild(wrapper);
260
269
  }
261
270
  }
262
271
  }
@@ -66,6 +66,13 @@ class HTMLToolbar {
66
66
  const closePickerOverlay = document.createElement('div');
67
67
  closePickerOverlay.className = `${exports.toolbarCSSPrefix}closeColorPickerOverlay`;
68
68
  this.editor.createHTMLOverlay(closePickerOverlay);
69
+ // Hide the color picker when attempting to draw on the overlay.
70
+ this.listeners.push(this.editor.handlePointerEventsFrom(closePickerOverlay, (eventName) => {
71
+ if (eventName === 'pointerdown') {
72
+ (0, coloris_1.close)();
73
+ }
74
+ return true;
75
+ }));
69
76
  const maxSwatchLen = 12;
70
77
  const swatches = [
71
78
  Color4_1.default.red.toHexString(),
@@ -14,7 +14,7 @@ export interface ToolbarLocalization {
14
14
  submit: string;
15
15
  freehandPen: string;
16
16
  pressureSensitiveFreehandPen: string;
17
- selectObjectType: string;
17
+ selectPenType: string;
18
18
  colorLabel: string;
19
19
  pen: string;
20
20
  eraser: string;
@@ -37,6 +37,7 @@ export interface ToolbarLocalization {
37
37
  backgroundColor: string;
38
38
  imageWidthOption: string;
39
39
  imageHeightOption: string;
40
+ useGridOption: string;
40
41
  toggleOverflow: string;
41
42
  errorImageHasZeroSize: string;
42
43
  dropdownShown: (toolName: string) => string;
@@ -23,7 +23,7 @@ exports.defaultToolbarLocalization = {
23
23
  duplicateSelection: 'Duplicate selection',
24
24
  undo: 'Undo',
25
25
  redo: 'Redo',
26
- selectObjectType: 'Object type: ',
26
+ selectPenType: 'Pen type: ',
27
27
  pickColorFromScreen: 'Pick color from screen',
28
28
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
29
29
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
@@ -31,6 +31,7 @@ exports.defaultToolbarLocalization = {
31
31
  backgroundColor: 'Background Color: ',
32
32
  imageWidthOption: 'Width: ',
33
33
  imageHeightOption: 'Height: ',
34
+ useGridOption: 'Grid: ',
34
35
  toggleOverflow: 'More',
35
36
  touchPanning: 'Touchscreen panning',
36
37
  freehandPen: 'Freehand',
@@ -12,6 +12,11 @@ export default class DocumentPropertiesWidget extends BaseWidget {
12
12
  private updateDropdown;
13
13
  private setBackgroundColor;
14
14
  private getBackgroundColor;
15
+ private removeBackgroundComponents;
16
+ /** Replace existing background components with a background of the given type. */
17
+ private setBackgroundType;
18
+ /** Returns the type of the topmost background component */
19
+ private getBackgroundType;
15
20
  private updateImportExportRectSize;
16
21
  private static idCounter;
17
22
  protected fillDropdown(dropdown: HTMLElement): boolean;