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
@@ -4,6 +4,7 @@ import LineSegment2 from './LineSegment2.mjs';
4
4
  import Mat33 from './Mat33.mjs';
5
5
  import Rect2 from './Rect2.mjs';
6
6
  import { Vec2 } from './Vec2.mjs';
7
+ import Vec3 from './Vec3.mjs';
7
8
  export var PathCommandType;
8
9
  (function (PathCommandType) {
9
10
  PathCommandType[PathCommandType["LineTo"] = 0] = "LineTo";
@@ -11,6 +12,23 @@ export var PathCommandType;
11
12
  PathCommandType[PathCommandType["CubicBezierTo"] = 2] = "CubicBezierTo";
12
13
  PathCommandType[PathCommandType["QuadraticBezierTo"] = 3] = "QuadraticBezierTo";
13
14
  })(PathCommandType || (PathCommandType = {}));
15
+ // Returns the bounding box of one path segment.
16
+ const getPartBBox = (part) => {
17
+ let partBBox;
18
+ if (part instanceof LineSegment2) {
19
+ partBBox = part.bbox;
20
+ }
21
+ else if (part instanceof Bezier) {
22
+ const bbox = part.bbox();
23
+ const width = bbox.x.max - bbox.x.min;
24
+ const height = bbox.y.max - bbox.y.min;
25
+ partBBox = new Rect2(bbox.x.min, bbox.y.min, width, height);
26
+ }
27
+ else {
28
+ partBBox = new Rect2(part.x, part.y, 0, 0);
29
+ }
30
+ return partBBox;
31
+ };
14
32
  export default class Path {
15
33
  constructor(startPoint, parts) {
16
34
  this.startPoint = startPoint;
@@ -26,6 +44,13 @@ export default class Path {
26
44
  this.bbox = this.bbox.union(Path.computeBBoxForSegment(startPoint, part));
27
45
  }
28
46
  }
47
+ getExactBBox() {
48
+ const bboxes = [];
49
+ for (const part of this.geometry) {
50
+ bboxes.push(getPartBBox(part));
51
+ }
52
+ return Rect2.union(...bboxes);
53
+ }
29
54
  // Lazy-loads and returns this path's geometry
30
55
  get geometry() {
31
56
  if (this.cachedGeometry) {
@@ -48,6 +73,7 @@ export default class Path {
48
73
  startPoint = part.point;
49
74
  break;
50
75
  case PathCommandType.MoveTo:
76
+ geometry.push(part.point);
51
77
  startPoint = part.point;
52
78
  break;
53
79
  }
@@ -103,11 +129,197 @@ export default class Path {
103
129
  }
104
130
  return Rect2.bboxOf(points);
105
131
  }
106
- intersection(line) {
107
- if (!line.bbox.intersects(this.bbox)) {
132
+ /**
133
+ * Let `S` be a closed path a distance `strokeRadius` from this path.
134
+ *
135
+ * @returns Approximate intersections of `line` with `S` using ray marching, starting from
136
+ * both end points of `line` and each point in `additionalRaymarchStartPoints`.
137
+ */
138
+ raymarchIntersectionWith(line, strokeRadius, additionalRaymarchStartPoints = []) {
139
+ var _a, _b;
140
+ // No intersection between bounding boxes: No possible intersection
141
+ // of the interior.
142
+ if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius))) {
143
+ return [];
144
+ }
145
+ const lineLength = line.length;
146
+ const partDistFunctionRecords = [];
147
+ // Determine distance functions for all parts that the given line could possibly intersect with
148
+ for (const part of this.geometry) {
149
+ const bbox = getPartBBox(part).grownBy(strokeRadius);
150
+ if (!bbox.intersects(line.bbox)) {
151
+ continue;
152
+ }
153
+ // Signed distance function
154
+ let partDist;
155
+ if (part instanceof LineSegment2) {
156
+ partDist = (point) => part.distance(point);
157
+ }
158
+ else if (part instanceof Vec3) {
159
+ partDist = (point) => part.minus(point).magnitude();
160
+ }
161
+ else {
162
+ partDist = (point) => {
163
+ return part.project(point).d;
164
+ };
165
+ }
166
+ // Part signed distance function (negative result implies `point` is
167
+ // inside the shape).
168
+ const partSdf = (point) => partDist(point) - strokeRadius;
169
+ // If the line can't possibly intersect the part,
170
+ if (partSdf(line.p1) > lineLength && partSdf(line.p2) > lineLength) {
171
+ continue;
172
+ }
173
+ partDistFunctionRecords.push({
174
+ part,
175
+ distFn: partDist,
176
+ bbox,
177
+ });
178
+ }
179
+ // If no distance functions, there are no intersections.
180
+ if (partDistFunctionRecords.length === 0) {
108
181
  return [];
109
182
  }
183
+ // Returns the minimum distance to a part in this stroke, where only parts that the given
184
+ // line could intersect are considered.
185
+ const sdf = (point) => {
186
+ let minDist = Infinity;
187
+ let minDistPart = null;
188
+ const uncheckedDistFunctions = [];
189
+ // First pass: only curves for which the current point is inside
190
+ // the bounding box.
191
+ for (const distFnRecord of partDistFunctionRecords) {
192
+ const { part, distFn, bbox } = distFnRecord;
193
+ // Check later if the current point isn't in the bounding box.
194
+ if (!bbox.containsPoint(point)) {
195
+ uncheckedDistFunctions.push(distFnRecord);
196
+ continue;
197
+ }
198
+ const currentDist = distFn(point);
199
+ if (currentDist <= minDist) {
200
+ minDist = currentDist;
201
+ minDistPart = part;
202
+ }
203
+ }
204
+ // Second pass: Everything else
205
+ for (const { part, distFn, bbox } of uncheckedDistFunctions) {
206
+ // Skip if impossible for the distance to the target to be lesser than
207
+ // the current minimum.
208
+ if (!bbox.grownBy(minDist).containsPoint(point)) {
209
+ continue;
210
+ }
211
+ const currentDist = distFn(point);
212
+ if (currentDist <= minDist) {
213
+ minDist = currentDist;
214
+ minDistPart = part;
215
+ }
216
+ }
217
+ return [minDistPart, minDist - strokeRadius];
218
+ };
219
+ // Raymarch:
220
+ const maxRaymarchSteps = 7;
221
+ // Start raymarching from each of these points. This allows detection of multiple
222
+ // intersections.
223
+ const startPoints = [
224
+ line.p1, ...additionalRaymarchStartPoints, line.p2
225
+ ];
226
+ // Converts a point ON THE LINE to a parameter
227
+ const pointToParameter = (point) => {
228
+ // Because line.direction is a unit vector, this computes the length
229
+ // of the projection of the vector(line.p1->point) onto line.direction.
230
+ //
231
+ // Note that this can be negative if the given point is outside of the given
232
+ // line segment.
233
+ return point.minus(line.p1).dot(line.direction);
234
+ };
235
+ // Sort start points by parameter on the line.
236
+ // This allows us to determine whether the current value of a parameter
237
+ // drops down to a value already tested.
238
+ startPoints.sort((a, b) => {
239
+ const t_a = pointToParameter(a);
240
+ const t_b = pointToParameter(b);
241
+ // Sort in increasing order
242
+ return t_a - t_b;
243
+ });
110
244
  const result = [];
245
+ const stoppingThreshold = strokeRadius / 1000;
246
+ // Returns the maximum x value explored
247
+ const raymarchFrom = (startPoint,
248
+ // Direction to march in (multiplies line.direction)
249
+ directionMultiplier,
250
+ // Terminate if the current point corresponds to a parameter
251
+ // below this.
252
+ minimumLineParameter) => {
253
+ let currentPoint = startPoint;
254
+ let [lastPart, lastDist] = sdf(currentPoint);
255
+ let lastParameter = pointToParameter(currentPoint);
256
+ if (lastDist > lineLength) {
257
+ return lastParameter;
258
+ }
259
+ const direction = line.direction.times(directionMultiplier);
260
+ for (let i = 0; i < maxRaymarchSteps; i++) {
261
+ // Step in the direction of the edge of the shape.
262
+ const step = lastDist;
263
+ currentPoint = currentPoint.plus(direction.times(step));
264
+ lastParameter = pointToParameter(currentPoint);
265
+ // If we're below the minimum parameter, stop. We've already tried
266
+ // this.
267
+ if (lastParameter <= minimumLineParameter) {
268
+ return lastParameter;
269
+ }
270
+ const [currentPart, signedDist] = sdf(currentPoint);
271
+ // Ensure we're stepping in the correct direction.
272
+ // Note that because we could start with a negative distance and work towards a
273
+ // positive distance, we need absolute values here.
274
+ if (Math.abs(signedDist) > Math.abs(lastDist)) {
275
+ // If not, stop.
276
+ return null;
277
+ }
278
+ lastDist = signedDist;
279
+ lastPart = currentPart;
280
+ // Is the distance close enough that we can stop early?
281
+ if (Math.abs(lastDist) < stoppingThreshold) {
282
+ break;
283
+ }
284
+ }
285
+ // Ensure that the point we ended with is on the line.
286
+ const isOnLineSegment = lastParameter >= 0 && lastParameter <= lineLength;
287
+ if (lastPart && isOnLineSegment && Math.abs(lastDist) < stoppingThreshold) {
288
+ result.push({
289
+ point: currentPoint,
290
+ parameterValue: NaN,
291
+ curve: lastPart,
292
+ });
293
+ }
294
+ return lastParameter;
295
+ };
296
+ // The maximum value of the line's parameter explored so far (0 corresponds to
297
+ // line.p1)
298
+ let maxLineT = 0;
299
+ // Raymarch for each start point.
300
+ //
301
+ // Use a for (i from 0 to length) loop because startPoints may be added
302
+ // during iteration.
303
+ for (let i = 0; i < startPoints.length; i++) {
304
+ const startPoint = startPoints[i];
305
+ // Try raymarching in both directions.
306
+ maxLineT = Math.max(maxLineT, (_a = raymarchFrom(startPoint, 1, maxLineT)) !== null && _a !== void 0 ? _a : maxLineT);
307
+ maxLineT = Math.max(maxLineT, (_b = raymarchFrom(startPoint, -1, maxLineT)) !== null && _b !== void 0 ? _b : maxLineT);
308
+ }
309
+ return result;
310
+ }
311
+ /**
312
+ * Returns a list of intersections with this path. If `strokeRadius` is given,
313
+ * intersections are approximated with the surface `strokeRadius` away from this.
314
+ *
315
+ * If `strokeRadius > 0`, the resultant `parameterValue` has no defined value.
316
+ */
317
+ intersection(line, strokeRadius) {
318
+ let result = [];
319
+ // Is any intersection between shapes within the bounding boxes impossible?
320
+ if (!line.bbox.intersects(this.bbox.grownBy(strokeRadius !== null && strokeRadius !== void 0 ? strokeRadius : 0))) {
321
+ return [];
322
+ }
111
323
  for (const part of this.geometry) {
112
324
  if (part instanceof LineSegment2) {
113
325
  const intersection = part.intersection(line);
@@ -119,7 +331,7 @@ export default class Path {
119
331
  });
120
332
  }
121
333
  }
122
- else {
334
+ else if (part instanceof Bezier) {
123
335
  const intersectionPoints = part.intersects(line).map(t => {
124
336
  // We're using the .intersects(line) function, which is documented
125
337
  // to always return numbers. However, to satisfy the type checker (and
@@ -142,6 +354,15 @@ export default class Path {
142
354
  result.push(...intersectionPoints);
143
355
  }
144
356
  }
357
+ // If given a non-zero strokeWidth, attempt to raymarch.
358
+ // Even if raymarching, we need to collect starting points.
359
+ // We use the above-calculated intersections for this.
360
+ const doRaymarching = strokeRadius && strokeRadius > 1e-8;
361
+ if (doRaymarching) {
362
+ // Starting points for raymarching (in addition to the end points of the line).
363
+ const startPoints = result.map(intersection => intersection.point);
364
+ result = this.raymarchIntersectionWith(line, strokeRadius, startPoints);
365
+ }
145
366
  return result;
146
367
  }
147
368
  static mapPathCommand(part, mapping) {
@@ -341,7 +562,7 @@ export default class Path {
341
562
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
342
563
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
343
564
  const expandedRect = visibleRect.grownBy(strokeWidth)
344
- .transformedBoundingBox(Mat33.scaling2D(2, visibleRect.center));
565
+ .transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
345
566
  // TODO: Handle simplifying very small paths.
346
567
  if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
347
568
  return renderablePath;
@@ -68,9 +68,7 @@ export default class Rect2 {
68
68
  }
69
69
  // Returns a new rectangle containing both [this] and [other].
70
70
  union(other) {
71
- const topLeft = this.topLeft.zip(other.topLeft, Math.min);
72
- const bottomRight = this.bottomRight.zip(other.bottomRight, Math.max);
73
- return Rect2.fromCorners(topLeft, bottomRight);
71
+ return Rect2.union(this, other);
74
72
  }
75
73
  // Returns a the subdivision of this into [columns] columns
76
74
  // and [rows] rows. For example,
@@ -108,6 +106,9 @@ export default class Rect2 {
108
106
  }
109
107
  // Returns this grown by [margin] in both the x and y directions.
110
108
  grownBy(margin) {
109
+ if (margin === 0) {
110
+ return this;
111
+ }
111
112
  return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2);
112
113
  }
113
114
  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,108 @@
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 (more) 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
+ // .d: Distance
102
+ return this.bezierJs.project(point.xy).d;
103
+ }
104
+ normal(t) {
105
+ const tangent = this.derivativeAt(t);
106
+ return tangent.orthog().normalized();
107
+ }
108
+ }
@@ -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;
@@ -186,14 +186,16 @@ export default class CanvasRenderer extends AbstractRenderer {
186
186
  }
187
187
  }
188
188
  endObject() {
189
+ // Cache this.objectLevel — it may be decremented by super.endObject.
190
+ const objectLevel = this.objectLevel;
191
+ this.currentObjectBBox = null;
192
+ super.endObject();
189
193
  if (!this.ignoringObject && this.clipLevels.length > 0) {
190
- if (this.clipLevels[this.clipLevels.length - 1] === this.objectLevel) {
194
+ if (this.clipLevels[this.clipLevels.length - 1] === objectLevel) {
191
195
  this.ctx.restore();
192
196
  this.clipLevels.pop();
193
197
  }
194
198
  }
195
- this.currentObjectBBox = null;
196
- super.endObject();
197
199
  // If exiting an object with a too-small-to-draw bounding box,
198
200
  if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
199
201
  this.ignoreObjectsAboveLevel = null;
@@ -3,6 +3,7 @@ import Path from '../../math/Path.mjs';
3
3
  import { toRoundedString } from '../../math/rounding.mjs';
4
4
  import { Vec2 } from '../../math/Vec2.mjs';
5
5
  import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader.mjs';
6
+ import { stylesEqual } from '../RenderingStyle.mjs';
6
7
  import AbstractRenderer from './AbstractRenderer.mjs';
7
8
  export const renderedStylesheetId = 'js-draw-style-sheet';
8
9
  const svgNameSpace = 'http://www.w3.org/2000/svg';
@@ -108,11 +109,10 @@ export default class SVGRenderer extends AbstractRenderer {
108
109
  (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
109
110
  }
110
111
  drawPath(pathSpec) {
111
- var _a;
112
112
  const style = pathSpec.style;
113
113
  const path = Path.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
114
114
  // Try to extend the previous path, if possible
115
- if (!style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) || this.lastPathString.length === 0) {
115
+ if (this.lastPathString.length === 0 || !this.lastPathStyle || !stylesEqual(this.lastPathStyle, style)) {
116
116
  this.addPathToSVG();
117
117
  this.lastPathStyle = style;
118
118
  this.lastPathString = [];
@@ -226,7 +226,7 @@ export default class SVGRenderer extends AbstractRenderer {
226
226
  this.objectElems = [];
227
227
  }
228
228
  endObject(loaderData, elemClassNames) {
229
- var _a, _b;
229
+ var _a;
230
230
  super.endObject(loaderData);
231
231
  // Don't extend paths across objects
232
232
  this.addPathToSVG();
@@ -248,9 +248,18 @@ export default class SVGRenderer extends AbstractRenderer {
248
248
  }
249
249
  }
250
250
  // Add class names to the object, if given.
251
- if (elemClassNames) {
252
- for (const elem of (_b = this.objectElems) !== null && _b !== void 0 ? _b : []) {
253
- elem.classList.add(...elemClassNames);
251
+ if (elemClassNames && this.objectElems) {
252
+ if (this.objectElems.length === 1) {
253
+ this.objectElems[0].classList.add(...elemClassNames);
254
+ }
255
+ else {
256
+ const wrapper = document.createElementNS(svgNameSpace, 'g');
257
+ wrapper.classList.add(...elemClassNames);
258
+ for (const elem of this.objectElems) {
259
+ elem.remove();
260
+ wrapper.appendChild(elem);
261
+ }
262
+ this.elem.appendChild(wrapper);
254
263
  }
255
264
  }
256
265
  }
@@ -1,5 +1,5 @@
1
1
  import { EditorEventType } from '../types.mjs';
2
- import { coloris, init as colorisInit } from '@melloware/coloris';
2
+ import { coloris, close as closeColoris, init as colorisInit } from '@melloware/coloris';
3
3
  import Color4 from '../Color4.mjs';
4
4
  import { defaultToolbarLocalization } from './localization.mjs';
5
5
  import SelectionTool from '../tools/SelectionTool/SelectionTool.mjs';
@@ -60,6 +60,13 @@ export default class HTMLToolbar {
60
60
  const closePickerOverlay = document.createElement('div');
61
61
  closePickerOverlay.className = `${toolbarCSSPrefix}closeColorPickerOverlay`;
62
62
  this.editor.createHTMLOverlay(closePickerOverlay);
63
+ // Hide the color picker when attempting to draw on the overlay.
64
+ this.listeners.push(this.editor.handlePointerEventsFrom(closePickerOverlay, (eventName) => {
65
+ if (eventName === 'pointerdown') {
66
+ closeColoris();
67
+ }
68
+ return true;
69
+ }));
63
70
  const maxSwatchLen = 12;
64
71
  const swatches = [
65
72
  Color4.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;
@@ -20,7 +20,7 @@ export const defaultToolbarLocalization = {
20
20
  duplicateSelection: 'Duplicate selection',
21
21
  undo: 'Undo',
22
22
  redo: 'Redo',
23
- selectObjectType: 'Object type: ',
23
+ selectPenType: 'Pen type: ',
24
24
  pickColorFromScreen: 'Pick color from screen',
25
25
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
26
26
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
@@ -28,6 +28,7 @@ export const defaultToolbarLocalization = {
28
28
  backgroundColor: 'Background Color: ',
29
29
  imageWidthOption: 'Width: ',
30
30
  imageHeightOption: 'Height: ',
31
+ useGridOption: 'Grid: ',
31
32
  toggleOverflow: 'More',
32
33
  touchPanning: 'Touchscreen panning',
33
34
  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;