js-draw 0.5.0 → 0.7.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 (92) hide show
  1. package/.firebase/hosting.ZG9jcw.cache +338 -0
  2. package/.github/ISSUE_TEMPLATE/translation.md +1 -1
  3. package/CHANGELOG.md +19 -0
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Editor.d.ts +8 -6
  6. package/dist/src/Editor.js +8 -4
  7. package/dist/src/EditorImage.d.ts +3 -0
  8. package/dist/src/EditorImage.js +7 -0
  9. package/dist/src/SVGLoader.js +7 -8
  10. package/dist/src/components/AbstractComponent.d.ts +1 -0
  11. package/dist/src/components/AbstractComponent.js +4 -0
  12. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -0
  13. package/dist/src/components/SVGGlobalAttributesObject.js +3 -0
  14. package/dist/src/components/Stroke.js +1 -0
  15. package/dist/src/components/Text.d.ts +11 -8
  16. package/dist/src/components/Text.js +63 -20
  17. package/dist/src/components/UnknownSVGObject.d.ts +1 -0
  18. package/dist/src/components/UnknownSVGObject.js +3 -0
  19. package/dist/src/components/builders/FreehandLineBuilder.d.ts +9 -2
  20. package/dist/src/components/builders/FreehandLineBuilder.js +129 -30
  21. package/dist/src/components/lib.d.ts +2 -2
  22. package/dist/src/components/lib.js +2 -2
  23. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
  24. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
  25. package/dist/src/rendering/renderers/SVGRenderer.js +49 -22
  26. package/dist/src/testing/beforeEachFile.js +4 -0
  27. package/dist/src/toolbar/HTMLToolbar.js +2 -3
  28. package/dist/src/toolbar/IconProvider.d.ts +30 -0
  29. package/dist/src/toolbar/IconProvider.js +417 -0
  30. package/dist/src/toolbar/lib.d.ts +1 -1
  31. package/dist/src/toolbar/lib.js +1 -2
  32. package/dist/src/toolbar/localization.d.ts +0 -1
  33. package/dist/src/toolbar/localization.js +0 -1
  34. package/dist/src/toolbar/makeColorInput.js +1 -2
  35. package/dist/src/toolbar/widgets/BaseWidget.js +1 -2
  36. package/dist/src/toolbar/widgets/EraserToolWidget.js +1 -2
  37. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +5 -3
  38. package/dist/src/toolbar/widgets/HandToolWidget.js +35 -12
  39. package/dist/src/toolbar/widgets/PenToolWidget.js +10 -8
  40. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +3 -0
  41. package/dist/src/toolbar/widgets/SelectionToolWidget.js +20 -7
  42. package/dist/src/toolbar/widgets/TextToolWidget.js +1 -2
  43. package/dist/src/tools/PanZoom.d.ts +1 -1
  44. package/dist/src/tools/PanZoom.js +4 -1
  45. package/dist/src/tools/PasteHandler.js +2 -22
  46. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -0
  47. package/dist/src/tools/SelectionTool/SelectionTool.js +66 -3
  48. package/dist/src/tools/TextTool.d.ts +4 -0
  49. package/dist/src/tools/TextTool.js +73 -15
  50. package/dist/src/tools/ToolController.js +1 -0
  51. package/dist/src/tools/localization.d.ts +1 -0
  52. package/dist/src/tools/localization.js +1 -0
  53. package/package.json +1 -1
  54. package/src/Editor.toSVG.test.ts +27 -0
  55. package/src/Editor.ts +15 -9
  56. package/src/EditorImage.ts +9 -0
  57. package/src/SVGLoader.test.ts +57 -0
  58. package/src/SVGLoader.ts +9 -10
  59. package/src/components/AbstractComponent.ts +5 -0
  60. package/src/components/SVGGlobalAttributesObject.ts +4 -0
  61. package/src/components/Stroke.ts +1 -0
  62. package/src/components/Text.test.ts +3 -18
  63. package/src/components/Text.ts +78 -25
  64. package/src/components/UnknownSVGObject.ts +4 -0
  65. package/src/components/builders/FreehandLineBuilder.ts +162 -34
  66. package/src/components/lib.ts +3 -3
  67. package/src/rendering/renderers/CanvasRenderer.ts +2 -2
  68. package/src/rendering/renderers/SVGRenderer.ts +50 -24
  69. package/src/testing/beforeEachFile.ts +6 -1
  70. package/src/toolbar/HTMLToolbar.ts +2 -3
  71. package/src/toolbar/IconProvider.ts +480 -0
  72. package/src/toolbar/lib.ts +1 -1
  73. package/src/toolbar/localization.ts +0 -2
  74. package/src/toolbar/makeColorInput.ts +1 -2
  75. package/src/toolbar/widgets/BaseWidget.ts +1 -2
  76. package/src/toolbar/widgets/EraserToolWidget.ts +1 -2
  77. package/src/toolbar/widgets/HandToolWidget.ts +42 -20
  78. package/src/toolbar/widgets/PenToolWidget.ts +11 -8
  79. package/src/toolbar/widgets/SelectionToolWidget.ts +24 -8
  80. package/src/toolbar/widgets/TextToolWidget.ts +1 -2
  81. package/src/tools/PanZoom.ts +4 -1
  82. package/src/tools/PasteHandler.ts +2 -24
  83. package/src/tools/SelectionTool/SelectionTool.css +1 -0
  84. package/src/tools/SelectionTool/SelectionTool.test.ts +40 -0
  85. package/src/tools/SelectionTool/SelectionTool.ts +73 -4
  86. package/src/tools/TextTool.ts +82 -17
  87. package/src/tools/ToolController.ts +1 -0
  88. package/src/tools/localization.ts +4 -0
  89. package/typedoc.json +5 -1
  90. package/dist/src/toolbar/icons.d.ts +0 -20
  91. package/dist/src/toolbar/icons.js +0 -385
  92. package/src/toolbar/icons.ts +0 -443
@@ -6,11 +6,11 @@ import LineSegment2 from '../../math/LineSegment2';
6
6
  import Stroke from '../Stroke';
7
7
  import Viewport from '../../Viewport';
8
8
  export const makeFreehandLineBuilder = (initialPoint, viewport) => {
9
- // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
9
+ // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
10
10
  // less than ±1 px from the curve.
11
- const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
11
+ const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
12
12
  const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
13
- return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
13
+ return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
14
14
  };
15
15
  // Handles stroke smoothing and creates Strokes from user/stylus input.
16
16
  export default class FreehandLineBuilder {
@@ -19,13 +19,17 @@ export default class FreehandLineBuilder {
19
19
  // for which a point is considered 'part of the curve'.
20
20
  // Note that the maximum will be smaller if the stroke width is less than
21
21
  // [maxFitAllowed].
22
- minFitAllowed, maxFitAllowed) {
22
+ minFitAllowed, maxFitAllowed, viewport) {
23
23
  this.startPoint = startPoint;
24
24
  this.minFitAllowed = minFitAllowed;
25
25
  this.maxFitAllowed = maxFitAllowed;
26
+ this.viewport = viewport;
26
27
  this.isFirstSegment = true;
27
28
  this.pathStartConnector = null;
28
29
  this.mostRecentConnector = null;
30
+ this.lastUpperBezier = null;
31
+ this.lastLowerBezier = null;
32
+ this.parts = [];
29
33
  this.lastExitingVec = null;
30
34
  this.currentCurve = null;
31
35
  this.lastPoint = this.startPoint;
@@ -46,16 +50,16 @@ export default class FreehandLineBuilder {
46
50
  fill: (_a = this.lastPoint.color) !== null && _a !== void 0 ? _a : null,
47
51
  };
48
52
  }
49
- previewPath() {
53
+ previewCurrentPath() {
50
54
  var _a;
51
- let upperPath;
52
- let lowerPath;
55
+ const upperPath = this.upperSegments.slice();
56
+ const lowerPath = this.lowerSegments.slice();
53
57
  let lowerToUpperCap;
54
58
  let pathStartConnector;
55
59
  if (this.currentCurve) {
56
- const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
57
- upperPath = this.upperSegments.concat(upperCurve);
58
- lowerPath = this.lowerSegments.concat(lowerCurve);
60
+ const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.currentSegmentToPath();
61
+ upperPath.push(upperCurveCommand);
62
+ lowerPath.push(lowerCurveCommand);
59
63
  lowerToUpperCap = lowerToUpperConnector;
60
64
  pathStartConnector = (_a = this.pathStartConnector) !== null && _a !== void 0 ? _a : upperToLowerConnector;
61
65
  }
@@ -63,12 +67,17 @@ export default class FreehandLineBuilder {
63
67
  if (this.mostRecentConnector === null || this.pathStartConnector === null) {
64
68
  return null;
65
69
  }
66
- upperPath = this.upperSegments.slice();
67
- lowerPath = this.lowerSegments.slice();
68
70
  lowerToUpperCap = this.mostRecentConnector;
69
71
  pathStartConnector = this.pathStartConnector;
70
72
  }
71
- const startPoint = lowerPath[lowerPath.length - 1].endPoint;
73
+ let startPoint;
74
+ const lastLowerSegment = lowerPath[lowerPath.length - 1];
75
+ if (lastLowerSegment.kind === PathCommandType.LineTo || lastLowerSegment.kind === PathCommandType.MoveTo) {
76
+ startPoint = lastLowerSegment.point;
77
+ }
78
+ else {
79
+ startPoint = lastLowerSegment.endPoint;
80
+ }
72
81
  return {
73
82
  // Start at the end of the lower curve:
74
83
  // Start point
@@ -103,21 +112,33 @@ export default class FreehandLineBuilder {
103
112
  style: this.getRenderingStyle(),
104
113
  };
105
114
  }
115
+ previewFullPath() {
116
+ const preview = this.previewCurrentPath();
117
+ if (preview) {
118
+ return [...this.parts, preview];
119
+ }
120
+ return null;
121
+ }
106
122
  previewStroke() {
107
- const pathPreview = this.previewPath();
123
+ const pathPreview = this.previewFullPath();
108
124
  if (pathPreview) {
109
- return new Stroke([pathPreview]);
125
+ return new Stroke(pathPreview);
110
126
  }
111
127
  return null;
112
128
  }
113
129
  preview(renderer) {
114
- const path = this.previewPath();
115
- if (path) {
116
- renderer.drawPath(path);
130
+ const paths = this.previewFullPath();
131
+ if (paths) {
132
+ const approxBBox = this.viewport.visibleRect;
133
+ renderer.startObject(approxBBox);
134
+ for (const path of paths) {
135
+ renderer.drawPath(path);
136
+ }
137
+ renderer.endObject();
117
138
  }
118
139
  }
119
140
  build() {
120
- if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
141
+ if (this.lastPoint) {
121
142
  this.finalizeCurrentCurve();
122
143
  }
123
144
  return this.previewStroke();
@@ -129,6 +150,61 @@ export default class FreehandLineBuilder {
129
150
  }
130
151
  return Viewport.roundPoint(point, minFit);
131
152
  }
153
+ // Returns true if, due to overlap with previous segments, a new RenderablePathSpec should be created.
154
+ shouldStartNewSegment(lowerCurve, upperCurve) {
155
+ if (!this.lastLowerBezier || !this.lastUpperBezier) {
156
+ return false;
157
+ }
158
+ const getIntersection = (curve1, curve2) => {
159
+ const intersection = curve1.intersects(curve2);
160
+ if (!intersection || intersection.length === 0) {
161
+ return null;
162
+ }
163
+ // From http://pomax.github.io/bezierjs/#intersect-curve,
164
+ // .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
165
+ const firstTPair = intersection[0];
166
+ const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
167
+ if (!match) {
168
+ throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
169
+ }
170
+ const t = parseFloat(match[1]);
171
+ return Vec2.ofXY(curve1.get(t));
172
+ };
173
+ const getExitDirection = (curve) => {
174
+ return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
175
+ };
176
+ const getEnterDirection = (curve) => {
177
+ return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
178
+ };
179
+ // Prevent
180
+ // /
181
+ // / /
182
+ // / / /|
183
+ // / / |
184
+ // / |
185
+ // where the next stroke and the previous stroke are in different directions.
186
+ //
187
+ // Are the exit/enter directions of the previous and current curves in different enough directions?
188
+ if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
189
+ || getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
190
+ // Also handle if the curves exit/enter directions differ
191
+ || getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
192
+ || getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
193
+ return true;
194
+ }
195
+ // Check whether the lower curve intersects the other wall:
196
+ // / / ← lower
197
+ // / / /
198
+ // / / /
199
+ // //
200
+ // / /
201
+ const lowerIntersection = getIntersection(lowerCurve, this.lastUpperBezier);
202
+ const upperIntersection = getIntersection(upperCurve, this.lastLowerBezier);
203
+ if (lowerIntersection || upperIntersection) {
204
+ return true;
205
+ }
206
+ return false;
207
+ }
132
208
  // Returns the distance between the start, control, and end points of the curve.
133
209
  approxCurrentCurveLength() {
134
210
  if (!this.currentCurve) {
@@ -185,8 +261,17 @@ export default class FreehandLineBuilder {
185
261
  this.mostRecentConnector = this.pathStartConnector;
186
262
  return;
187
263
  }
188
- const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
189
- if (this.isFirstSegment) {
264
+ const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.currentSegmentToPath();
265
+ const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
266
+ if (shouldStartNew) {
267
+ const part = this.previewCurrentPath();
268
+ if (part) {
269
+ this.parts.push(part);
270
+ this.upperSegments = [];
271
+ this.lowerSegments = [];
272
+ }
273
+ }
274
+ if (this.isFirstSegment || shouldStartNew) {
190
275
  // We draw the upper path (reversed), then the lower path, so we need the
191
276
  // upperToLowerConnector to join the two paths.
192
277
  this.pathStartConnector = upperToLowerConnector;
@@ -195,8 +280,10 @@ export default class FreehandLineBuilder {
195
280
  // With the most recent connector, we're joining the end of the lowerPath to the most recent
196
281
  // upperPath:
197
282
  this.mostRecentConnector = lowerToUpperConnector;
198
- this.upperSegments.push(upperCurve);
199
- this.lowerSegments.push(lowerCurve);
283
+ this.lowerSegments.push(lowerCurveCommand);
284
+ this.upperSegments.push(upperCurveCommand);
285
+ this.lastLowerBezier = lowerCurve;
286
+ this.lastUpperBezier = upperCurve;
200
287
  const lastPoint = this.buffer[this.buffer.length - 1];
201
288
  this.lastExitingVec = Vec2.ofXY(this.currentCurve.points[2]).minus(Vec2.ofXY(this.currentCurve.points[1]));
202
289
  console.assert(this.lastExitingVec.magnitude() !== 0, 'lastExitingVec has zero length!');
@@ -238,11 +325,13 @@ export default class FreehandLineBuilder {
238
325
  .normalized().times(this.curveStartWidth / 2 * halfVecT
239
326
  + this.curveEndWidth / 2 * (1 - halfVecT));
240
327
  // Each starts at startPt ± startVec
328
+ const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
241
329
  const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
242
330
  const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
243
331
  const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
244
332
  const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
245
- const lowerCurve = {
333
+ const upperCurveEndPoint = this.roundPoint(startPt.minus(startVec));
334
+ const lowerCurveCommand = {
246
335
  kind: PathCommandType.QuadraticBezierTo,
247
336
  controlPoint: lowerCurveControlPoint,
248
337
  endPoint: lowerCurveEndPoint,
@@ -250,19 +339,24 @@ export default class FreehandLineBuilder {
250
339
  // From the end of the upperCurve to the start of the lowerCurve:
251
340
  const upperToLowerConnector = {
252
341
  kind: PathCommandType.LineTo,
253
- point: this.roundPoint(startPt.plus(startVec)),
342
+ point: lowerCurveStartPoint,
254
343
  };
255
344
  // From the end of lowerCurve to the start of upperCurve:
256
345
  const lowerToUpperConnector = {
257
346
  kind: PathCommandType.LineTo,
258
347
  point: upperCurveStartPoint,
259
348
  };
260
- const upperCurve = {
349
+ const upperCurveCommand = {
261
350
  kind: PathCommandType.QuadraticBezierTo,
262
351
  controlPoint: upperCurveControlPoint,
263
- endPoint: this.roundPoint(startPt.minus(startVec)),
352
+ endPoint: upperCurveEndPoint,
353
+ };
354
+ const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
355
+ const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
356
+ return {
357
+ upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
358
+ upperCurve, lowerCurve,
264
359
  };
265
- return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
266
360
  }
267
361
  // Compute the direction of the velocity at the end of this.buffer
268
362
  computeExitingVec() {
@@ -299,6 +393,10 @@ export default class FreehandLineBuilder {
299
393
  const pointRadius = newPoint.width / 2;
300
394
  const prevEndWidth = this.curveEndWidth;
301
395
  this.curveEndWidth = pointRadius;
396
+ if (this.isFirstSegment) {
397
+ // The start of a curve often lacks accurate pressure information. Update it.
398
+ this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
399
+ }
302
400
  // recompute bbox
303
401
  this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
304
402
  if (this.currentCurve === null) {
@@ -310,10 +408,11 @@ export default class FreehandLineBuilder {
310
408
  this.curveStartWidth = lastPoint.width / 2;
311
409
  console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
312
410
  }
411
+ // If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
313
412
  let enteringVec = this.lastExitingVec;
314
413
  if (!enteringVec) {
315
- let sampleIdx = Math.ceil(this.buffer.length / 3);
316
- if (sampleIdx === 0) {
414
+ let sampleIdx = Math.ceil(this.buffer.length / 2);
415
+ if (sampleIdx === 0 || sampleIdx >= this.buffer.length) {
317
416
  sampleIdx = this.buffer.length - 1;
318
417
  }
319
418
  enteringVec = this.buffer[sampleIdx].minus(this.buffer[0]);
@@ -3,6 +3,6 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
3
  export * from './AbstractComponent';
4
4
  export { default as AbstractComponent } from './AbstractComponent';
5
5
  import Stroke from './Stroke';
6
- import Text from './Text';
6
+ import TextComponent from './Text';
7
7
  import ImageComponent from './ImageComponent';
8
- export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
8
+ export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -3,6 +3,6 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
3
  export * from './AbstractComponent';
4
4
  export { default as AbstractComponent } from './AbstractComponent';
5
5
  import Stroke from './Stroke';
6
- import Text from './Text';
6
+ import TextComponent from './Text';
7
7
  import ImageComponent from './ImageComponent';
8
- export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
8
+ export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -1,5 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
- import Text from '../../components/Text';
2
+ import TextComponent from '../../components/Text';
3
3
  import { Vec2 } from '../../math/Vec2';
4
4
  import AbstractRenderer from './AbstractRenderer';
5
5
  export default class CanvasRenderer extends AbstractRenderer {
@@ -113,7 +113,7 @@ export default class CanvasRenderer extends AbstractRenderer {
113
113
  this.ctx.save();
114
114
  transform = this.getCanvasToScreenTransform().rightMul(transform);
115
115
  this.transformBy(transform);
116
- Text.applyTextStyles(this.ctx, style);
116
+ TextComponent.applyTextStyles(this.ctx, style);
117
117
  if (style.renderingStyle.fill.a !== 0) {
118
118
  this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
119
119
  this.ctx.fillText(text, 0, 0);
@@ -20,6 +20,8 @@ export default class SVGRenderer extends AbstractRenderer {
20
20
  private addPathToSVG;
21
21
  drawPath(pathSpec: RenderablePathSpec): void;
22
22
  private transformFrom;
23
+ private textContainer;
24
+ private textContainerTransform;
23
25
  drawText(text: string, transform: Mat33, style: TextStyle): void;
24
26
  drawImage(image: RenderableImage): void;
25
27
  startObject(boundingBox: Rect2): void;
@@ -15,6 +15,8 @@ export default class SVGRenderer extends AbstractRenderer {
15
15
  this.lastPathString = [];
16
16
  this.objectElems = null;
17
17
  this.overwrittenAttrs = {};
18
+ this.textContainer = null;
19
+ this.textContainerTransform = null;
18
20
  this.clear();
19
21
  }
20
22
  // Sets an attribute on the root SVG element.
@@ -82,35 +84,59 @@ export default class SVGRenderer extends AbstractRenderer {
82
84
  this.lastPathString.push(path.toString());
83
85
  }
84
86
  // Apply [elemTransform] to [elem].
85
- transformFrom(elemTransform, elem) {
86
- let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
87
+ transformFrom(elemTransform, elem, inCanvasSpace = false) {
88
+ let transform = !inCanvasSpace ? this.getCanvasToScreenTransform().rightMul(elemTransform) : elemTransform;
87
89
  const translation = transform.transformVec2(Vec2.zero);
88
90
  transform = transform.rightMul(Mat33.translation(translation.times(-1)));
89
- elem.style.transform = `matrix(
90
- ${transform.a1}, ${transform.b1},
91
- ${transform.a2}, ${transform.b2},
92
- ${transform.a3}, ${transform.b3}
93
- )`;
91
+ if (!transform.eq(Mat33.identity)) {
92
+ elem.style.transform = `matrix(
93
+ ${transform.a1}, ${transform.b1},
94
+ ${transform.a2}, ${transform.b2},
95
+ ${transform.a3}, ${transform.b3}
96
+ )`;
97
+ }
98
+ else {
99
+ elem.style.transform = '';
100
+ }
94
101
  elem.setAttribute('x', `${toRoundedString(translation.x)}`);
95
102
  elem.setAttribute('y', `${toRoundedString(translation.y)}`);
96
103
  }
97
104
  drawText(text, transform, style) {
98
- var _a, _b, _c;
99
- const textElem = document.createElementNS(svgNameSpace, 'text');
100
- textElem.appendChild(document.createTextNode(text));
101
- this.transformFrom(transform, textElem);
102
- textElem.style.fontFamily = style.fontFamily;
103
- textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
104
- textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
105
- textElem.style.fontSize = style.size + 'px';
106
- textElem.style.fill = style.renderingStyle.fill.toHexString();
107
- if (style.renderingStyle.stroke) {
108
- const strokeStyle = style.renderingStyle.stroke;
109
- textElem.style.stroke = strokeStyle.color.toHexString();
110
- textElem.style.strokeWidth = strokeStyle.width + 'px';
105
+ var _a;
106
+ const applyTextStyles = (elem, style) => {
107
+ var _a, _b;
108
+ elem.style.fontFamily = style.fontFamily;
109
+ elem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
110
+ elem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
111
+ elem.style.fontSize = style.size + 'px';
112
+ elem.style.fill = style.renderingStyle.fill.toHexString();
113
+ if (style.renderingStyle.stroke) {
114
+ const strokeStyle = style.renderingStyle.stroke;
115
+ elem.style.stroke = strokeStyle.color.toHexString();
116
+ elem.style.strokeWidth = strokeStyle.width + 'px';
117
+ }
118
+ };
119
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
120
+ if (!this.textContainer) {
121
+ const container = document.createElementNS(svgNameSpace, 'text');
122
+ container.appendChild(document.createTextNode(text));
123
+ this.transformFrom(transform, container, true);
124
+ applyTextStyles(container, style);
125
+ this.elem.appendChild(container);
126
+ (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(container);
127
+ if (this.objectLevel > 0) {
128
+ this.textContainer = container;
129
+ this.textContainerTransform = transform;
130
+ }
131
+ }
132
+ else {
133
+ const elem = document.createElementNS(svgNameSpace, 'tspan');
134
+ elem.appendChild(document.createTextNode(text));
135
+ this.textContainer.appendChild(elem);
136
+ transform = this.textContainerTransform.inverse().rightMul(transform);
137
+ this.transformFrom(transform, elem, true);
138
+ applyTextStyles(elem, style);
111
139
  }
112
- this.elem.appendChild(textElem);
113
- (_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
114
140
  }
115
141
  drawImage(image) {
116
142
  var _a, _b, _c, _d, _e;
@@ -128,6 +154,7 @@ export default class SVGRenderer extends AbstractRenderer {
128
154
  // Only accumulate a path within an object
129
155
  this.lastPathString = [];
130
156
  this.lastPathStyle = null;
157
+ this.textContainer = null;
131
158
  this.objectElems = [];
132
159
  }
133
160
  endObject(loaderData) {
@@ -1,3 +1,7 @@
1
1
  import loadExpectExtensions from './loadExpectExtensions';
2
2
  loadExpectExtensions();
3
3
  jest.useFakeTimers();
4
+ // jsdom doesn't support HTMLCanvasElement#getContext — it logs an error
5
+ // to the console. Make it return null so we can handle a non-existent Canvas
6
+ // at runtime (e.g. use something else, if available).
7
+ HTMLCanvasElement.prototype.getContext = () => null;
@@ -2,7 +2,6 @@ import { EditorEventType } from '../types';
2
2
  import { coloris, init as colorisInit } from '@melloware/coloris';
3
3
  import Color4 from '../Color4';
4
4
  import { defaultToolbarLocalization } from './localization';
5
- import { makeRedoIcon, makeUndoIcon } from './icons';
6
5
  import SelectionTool from '../tools/SelectionTool/SelectionTool';
7
6
  import PanZoomTool from '../tools/PanZoom';
8
7
  import TextTool from '../tools/TextTool';
@@ -123,13 +122,13 @@ export default class HTMLToolbar {
123
122
  undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
124
123
  const undoButton = this.addActionButton({
125
124
  label: this.localizationTable.undo,
126
- icon: makeUndoIcon()
125
+ icon: this.editor.icons.makeUndoIcon()
127
126
  }, () => {
128
127
  this.editor.history.undo();
129
128
  }, undoRedoGroup);
130
129
  const redoButton = this.addActionButton({
131
130
  label: this.localizationTable.redo,
132
- icon: makeRedoIcon(),
131
+ icon: this.editor.icons.makeRedoIcon(),
133
132
  }, () => {
134
133
  this.editor.history.redo();
135
134
  }, undoRedoGroup);
@@ -0,0 +1,30 @@
1
+ import Color4 from '../Color4';
2
+ import { ComponentBuilderFactory } from '../components/builders/types';
3
+ import { TextStyle } from '../components/Text';
4
+ import Pen from '../tools/Pen';
5
+ declare type IconType = SVGSVGElement | HTMLImageElement;
6
+ export default class IconProvider {
7
+ makeUndoIcon(): IconType;
8
+ makeRedoIcon(mirror?: boolean): IconType;
9
+ makeDropdownIcon(): IconType;
10
+ makeEraserIcon(): IconType;
11
+ makeSelectionIcon(): IconType;
12
+ /**
13
+ * @param pathData - SVG path data (e.g. `m10,10l30,30z`)
14
+ * @param fill - A valid CSS color (e.g. `var(--icon-color)` or `#f0f`). This can be `none`.
15
+ */
16
+ protected makeIconFromPath(pathData: string, fill?: string, strokeColor?: string, strokeWidth?: string): IconType;
17
+ makeHandToolIcon(): IconType;
18
+ makeTouchPanningIcon(): IconType;
19
+ makeAllDevicePanningIcon(): IconType;
20
+ makeZoomIcon(): IconType;
21
+ makeTextIcon(textStyle: TextStyle): IconType;
22
+ makePenIcon(tipThickness: number, color: string | Color4): IconType;
23
+ makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
24
+ makePipetteIcon(color?: Color4): IconType;
25
+ makeResizeViewportIcon(): IconType;
26
+ makeDuplicateSelectionIcon(): IconType;
27
+ makeDeleteSelectionIcon(): IconType;
28
+ makeSaveIcon(): IconType;
29
+ }
30
+ export {};