js-draw 0.15.0 → 0.15.2

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 (85) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +16 -0
  2. package/CHANGELOG.md +13 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +1 -1
  5. package/dist/src/Color4.js +5 -1
  6. package/dist/src/Editor.d.ts +0 -2
  7. package/dist/src/Editor.js +15 -30
  8. package/dist/src/EditorImage.d.ts +25 -0
  9. package/dist/src/EditorImage.js +57 -2
  10. package/dist/src/EventDispatcher.d.ts +4 -3
  11. package/dist/src/SVGLoader.d.ts +1 -0
  12. package/dist/src/SVGLoader.js +15 -1
  13. package/dist/src/Viewport.d.ts +3 -3
  14. package/dist/src/Viewport.js +4 -8
  15. package/dist/src/components/AbstractComponent.d.ts +5 -1
  16. package/dist/src/components/AbstractComponent.js +22 -8
  17. package/dist/src/components/ImageBackground.d.ts +41 -0
  18. package/dist/src/components/ImageBackground.js +132 -0
  19. package/dist/src/components/ImageComponent.js +2 -0
  20. package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
  21. package/dist/src/components/builders/ArrowBuilder.js +43 -40
  22. package/dist/src/components/builders/LineBuilder.d.ts +3 -1
  23. package/dist/src/components/builders/LineBuilder.js +25 -28
  24. package/dist/src/components/builders/RectangleBuilder.js +1 -1
  25. package/dist/src/components/lib.d.ts +2 -1
  26. package/dist/src/components/lib.js +2 -1
  27. package/dist/src/components/localization.d.ts +2 -0
  28. package/dist/src/components/localization.js +2 -0
  29. package/dist/src/math/Mat33.js +43 -5
  30. package/dist/src/math/Path.d.ts +5 -0
  31. package/dist/src/math/Path.js +80 -28
  32. package/dist/src/math/Vec3.js +1 -1
  33. package/dist/src/rendering/Display.js +1 -1
  34. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
  35. package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
  36. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  37. package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
  38. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  39. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  40. package/dist/src/testing/sendTouchEvent.d.ts +6 -0
  41. package/dist/src/testing/sendTouchEvent.js +26 -0
  42. package/dist/src/toolbar/IconProvider.js +1 -2
  43. package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
  44. package/dist/src/tools/Eraser.js +5 -2
  45. package/dist/src/tools/PanZoom.js +12 -0
  46. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  47. package/dist/src/tools/SelectionTool/SelectionTool.js +8 -2
  48. package/package.json +1 -1
  49. package/src/Color4.test.ts +6 -0
  50. package/src/Color4.ts +6 -1
  51. package/src/Editor.ts +15 -36
  52. package/src/EditorImage.ts +74 -2
  53. package/src/EventDispatcher.ts +4 -1
  54. package/src/SVGLoader.ts +12 -1
  55. package/src/Viewport.ts +4 -7
  56. package/src/components/AbstractComponent.transformBy.test.ts +22 -0
  57. package/src/components/AbstractComponent.ts +21 -4
  58. package/src/components/ImageBackground.ts +167 -0
  59. package/src/components/ImageComponent.ts +2 -0
  60. package/src/components/builders/ArrowBuilder.ts +44 -41
  61. package/src/components/builders/LineBuilder.ts +26 -28
  62. package/src/components/builders/RectangleBuilder.ts +1 -1
  63. package/src/components/lib.ts +2 -0
  64. package/src/components/localization.ts +4 -0
  65. package/src/math/Mat33.test.ts +20 -1
  66. package/src/math/Mat33.ts +47 -5
  67. package/src/math/Path.ts +87 -28
  68. package/src/math/Vec3.test.ts +4 -0
  69. package/src/math/Vec3.ts +1 -1
  70. package/src/rendering/Display.ts +1 -1
  71. package/src/rendering/renderers/AbstractRenderer.ts +20 -3
  72. package/src/rendering/renderers/CanvasRenderer.ts +16 -3
  73. package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
  74. package/src/rendering/renderers/SVGRenderer.ts +8 -1
  75. package/src/testing/sendTouchEvent.ts +43 -0
  76. package/src/toolbar/IconProvider.ts +1 -2
  77. package/src/toolbar/toolbar.css +7 -0
  78. package/src/toolbar/widgets/HandToolWidget.ts +1 -1
  79. package/src/tools/Eraser.test.ts +24 -1
  80. package/src/tools/Eraser.ts +6 -2
  81. package/src/tools/PanZoom.test.ts +267 -23
  82. package/src/tools/PanZoom.ts +15 -1
  83. package/src/tools/SelectionTool/Selection.ts +1 -1
  84. package/src/tools/SelectionTool/SelectionTool.ts +8 -1
  85. package/src/types.ts +1 -0
@@ -64,19 +64,34 @@ export default class AbstractRenderer {
64
64
  this.currentPaths.push(path);
65
65
  }
66
66
  }
67
- // Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
67
+ // Strokes a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
68
68
  // This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
69
69
  drawRect(rect, lineWidth, lineFill) {
70
70
  const path = Path.fromRect(rect, lineWidth);
71
71
  this.drawPath(path.toRenderable(lineFill));
72
72
  }
73
- // Note the start/end of an object with the given bounding box.
73
+ // Fills a rectangle.
74
+ fillRect(rect, fill) {
75
+ const path = Path.fromRect(rect);
76
+ this.drawPath(path.toRenderable({ fill }));
77
+ }
78
+ // Note the start of an object with the given bounding box.
74
79
  // Renderers are not required to support [clip]
75
80
  startObject(_boundingBox, _clip) {
76
81
  this.currentPaths = [];
77
82
  this.objectLevel++;
78
83
  }
79
- endObject(_loaderData) {
84
+ /**
85
+ * Notes the end of an object.
86
+ * @param _loaderData - a map from strings to JSON-ifyable objects
87
+ * and contains properties attached to the object by whatever loader loaded the image. This
88
+ * is used to preserve attributes not supported by js-draw when loading/saving an image.
89
+ * Renderers may ignore this.
90
+ *
91
+ * @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object.
92
+ * Renderers may ignore this.
93
+ */
94
+ endObject(_loaderData, _objectTags) {
80
95
  // Render the paths all at once
81
96
  this.flushPath();
82
97
  this.currentPaths = null;
@@ -10,6 +10,7 @@ export default class CanvasRenderer extends AbstractRenderer {
10
10
  private ctx;
11
11
  private ignoreObjectsAboveLevel;
12
12
  private ignoringObject;
13
+ private currentObjectBBox;
13
14
  private minSquareCurveApproxDist;
14
15
  private minRenderSizeAnyDimen;
15
16
  private minRenderSizeBothDimens;
@@ -30,7 +31,7 @@ export default class CanvasRenderer extends AbstractRenderer {
30
31
  drawText(text: string, transform: Mat33, style: TextStyle): void;
31
32
  drawImage(image: RenderableImage): void;
32
33
  private clipLevels;
33
- startObject(boundingBox: Rect2, clip: boolean): void;
34
+ startObject(boundingBox: Rect2, clip?: boolean): void;
34
35
  endObject(): void;
35
36
  drawPoints(...points: Point2[]): void;
36
37
  isTooSmallToRender(rect: Rect2): boolean;
@@ -1,5 +1,6 @@
1
1
  import Color4 from '../../Color4';
2
2
  import TextComponent from '../../components/TextComponent';
3
+ import Path from '../../math/Path';
3
4
  import { Vec2 } from '../../math/Vec2';
4
5
  import AbstractRenderer from './AbstractRenderer';
5
6
  export default class CanvasRenderer extends AbstractRenderer {
@@ -8,6 +9,7 @@ export default class CanvasRenderer extends AbstractRenderer {
8
9
  this.ctx = ctx;
9
10
  this.ignoreObjectsAboveLevel = null;
10
11
  this.ignoringObject = false;
12
+ this.currentObjectBBox = null;
11
13
  this.clipLevels = [];
12
14
  this.setDraftMode(false);
13
15
  }
@@ -43,8 +45,8 @@ export default class CanvasRenderer extends AbstractRenderer {
43
45
  }
44
46
  else {
45
47
  this.minSquareCurveApproxDist = 0.5;
46
- this.minRenderSizeBothDimens = 0.3;
47
- this.minRenderSizeAnyDimen = 1e-5;
48
+ this.minRenderSizeBothDimens = 0.2;
49
+ this.minRenderSizeAnyDimen = 1e-6;
48
50
  }
49
51
  }
50
52
  displaySize() {
@@ -106,9 +108,15 @@ export default class CanvasRenderer extends AbstractRenderer {
106
108
  }
107
109
  }
108
110
  drawPath(path) {
111
+ var _a;
109
112
  if (this.ignoringObject) {
110
113
  return;
111
114
  }
115
+ // If part of a huge object, it might be worth trimming the path
116
+ if ((_a = this.currentObjectBBox) === null || _a === void 0 ? void 0 : _a.containsRect(this.getViewport().visibleRect)) {
117
+ // Try to trim/remove parts of the path outside of the bounding box.
118
+ path = Path.visualEquivalent(path, this.getViewport().visibleRect);
119
+ }
112
120
  super.drawPath(path);
113
121
  }
114
122
  drawText(text, transform, style) {
@@ -140,6 +148,7 @@ export default class CanvasRenderer extends AbstractRenderer {
140
148
  this.ignoringObject = true;
141
149
  }
142
150
  super.startObject(boundingBox);
151
+ this.currentObjectBBox = boundingBox;
143
152
  if (!this.ignoringObject && clip) {
144
153
  this.clipLevels.push(this.objectLevel);
145
154
  this.ctx.save();
@@ -158,6 +167,7 @@ export default class CanvasRenderer extends AbstractRenderer {
158
167
  this.clipLevels.pop();
159
168
  }
160
169
  }
170
+ this.currentObjectBBox = null;
161
171
  super.endObject();
162
172
  // If exiting an object with a too-small-to-draw bounding box,
163
173
  if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
@@ -28,7 +28,7 @@ export default class SVGRenderer extends AbstractRenderer {
28
28
  drawText(text: string, transform: Mat33, style: TextStyle): void;
29
29
  drawImage(image: RenderableImage): void;
30
30
  startObject(boundingBox: Rect2): void;
31
- endObject(loaderData?: LoadSaveDataTable): void;
31
+ endObject(loaderData?: LoadSaveDataTable, elemClassNames?: string[]): void;
32
32
  private unimplementedMessage;
33
33
  protected beginPath(_startPoint: Point2): void;
34
34
  protected endPath(_style: RenderingStyle): void;
@@ -215,8 +215,8 @@ export default class SVGRenderer extends AbstractRenderer {
215
215
  this.textParentStyle = null;
216
216
  this.objectElems = [];
217
217
  }
218
- endObject(loaderData) {
219
- var _a;
218
+ endObject(loaderData, elemClassNames) {
219
+ var _a, _b;
220
220
  super.endObject(loaderData);
221
221
  // Don't extend paths across objects
222
222
  this.addPathToSVG();
@@ -237,6 +237,12 @@ export default class SVGRenderer extends AbstractRenderer {
237
237
  }
238
238
  }
239
239
  }
240
+ // Add class names to the object, if given.
241
+ if (elemClassNames) {
242
+ for (const elem of (_b = this.objectElems) !== null && _b !== void 0 ? _b : []) {
243
+ elem.classList.add(...elemClassNames);
244
+ }
245
+ }
240
246
  }
241
247
  // Not implemented -- use drawPath instead.
242
248
  unimplementedMessage() { throw new Error('Not implemenented!'); }
@@ -0,0 +1,6 @@
1
+ import Editor from '../Editor';
2
+ import { Vec2 } from '../math/Vec2';
3
+ import Pointer from '../Pointer';
4
+ import { InputEvtType } from '../types';
5
+ declare const sendTouchEvent: (editor: Editor, eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, screenPos: Vec2, allOtherPointers?: Pointer[]) => Pointer;
6
+ export default sendTouchEvent;
@@ -0,0 +1,26 @@
1
+ import Pointer, { PointerDevice } from '../Pointer';
2
+ import { InputEvtType } from '../types';
3
+ const sendTouchEvent = (editor, eventType, screenPos, allOtherPointers) => {
4
+ const canvasPos = editor.viewport.screenToCanvas(screenPos);
5
+ let ptrId = 0;
6
+ let maxPtrId = 0;
7
+ // Get a unique ID for the main pointer
8
+ // (try to use id=0, but don't use it if it's already in use).
9
+ for (const pointer of allOtherPointers !== null && allOtherPointers !== void 0 ? allOtherPointers : []) {
10
+ maxPtrId = Math.max(pointer.id, maxPtrId);
11
+ if (pointer.id === ptrId) {
12
+ ptrId = maxPtrId + 1;
13
+ }
14
+ }
15
+ const mainPointer = Pointer.ofCanvasPoint(canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch);
16
+ editor.toolController.dispatchInputEvent({
17
+ kind: eventType,
18
+ allPointers: [
19
+ ...(allOtherPointers !== null && allOtherPointers !== void 0 ? allOtherPointers : []),
20
+ mainPointer,
21
+ ],
22
+ current: mainPointer,
23
+ });
24
+ return mainPointer;
25
+ };
26
+ export default sendTouchEvent;
@@ -1,5 +1,4 @@
1
1
  import Color4 from '../Color4';
2
- import EventDispatcher from '../EventDispatcher';
3
2
  import { Vec2 } from '../math/Vec2';
4
3
  import SVGRenderer from '../rendering/renderers/SVGRenderer';
5
4
  import Viewport from '../Viewport';
@@ -482,7 +481,7 @@ export default class IconProvider {
482
481
  color: pen.getColor(),
483
482
  time: nowTime,
484
483
  };
485
- const viewport = new Viewport(new EventDispatcher());
484
+ const viewport = new Viewport(() => { });
486
485
  const builder = factory(startPoint, viewport);
487
486
  builder.addPoint(endPoint);
488
487
  const icon = document.createElementNS(svgNamespace, 'svg');
@@ -71,7 +71,7 @@ class ZoomWidget extends BaseWidget {
71
71
  this.setDropdownVisible(!this.isDropdownVisible());
72
72
  }
73
73
  fillDropdown(dropdown) {
74
- dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
74
+ dropdown.replaceChildren(makeZoomControl(this.localizationTable, this.editor));
75
75
  return true;
76
76
  }
77
77
  }
@@ -51,10 +51,13 @@ export default class Eraser extends BaseTool {
51
51
  const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
52
52
  return component.intersects(line) || component.intersectsRect(eraserRect);
53
53
  });
54
+ // Only erase components that could be selected (and thus interacted with)
55
+ // by the user.
56
+ const toErase = intersectingElems.filter(elem => elem.isSelectable());
54
57
  // Remove any intersecting elements.
55
- this.toRemove.push(...intersectingElems);
58
+ this.toRemove.push(...toErase);
56
59
  // Create new Erase commands for the now-to-be-erased elements and apply them.
57
- const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
60
+ const newPartialCommands = toErase.map(elem => new Erase([elem]));
58
61
  newPartialCommands.forEach(cmd => cmd.apply(this.editor));
59
62
  this.partialCommands.push(...newPartialCommands);
60
63
  this.drawPreviewAt(currentPoint);
@@ -85,6 +85,12 @@ export default class PanZoom extends BaseTool {
85
85
  }
86
86
  // Returns information about the pointers in a gesture
87
87
  computePinchData(p1, p2) {
88
+ // Swap the pointers to ensure consistent ordering.
89
+ if (p1.id < p2.id) {
90
+ const tmp = p1;
91
+ p1 = p2;
92
+ p2 = tmp;
93
+ }
88
94
  const screenBetween = p2.screenPos.minus(p1.screenPos);
89
95
  const angle = screenBetween.angle();
90
96
  const dist = screenBetween.magnitude();
@@ -169,6 +175,12 @@ export default class PanZoom extends BaseTool {
169
175
  // Snap the rotation
170
176
  if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
171
177
  fullRotation = roundedFullRotation;
178
+ // Work around a rotation/matrix multiply bug.
179
+ // (See commit after 4abe27ff8e7913155828f98dee77b09c57c51d30).
180
+ // TODO: Fix the underlying issue and remove this.
181
+ if (fullRotation !== 0) {
182
+ fullRotation += 0.0001;
183
+ }
172
184
  }
173
185
  return fullRotation - this.editor.viewport.getRotationAngle();
174
186
  }
@@ -157,7 +157,7 @@ export default class Selection {
157
157
  singleItemSelectionMode = true;
158
158
  }
159
159
  this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
160
- return elem.intersectsRect(this.region);
160
+ return elem.intersectsRect(this.region) && elem.isSelectable();
161
161
  });
162
162
  if (singleItemSelectionMode && this.selectedElems.length > 0) {
163
163
  this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
@@ -158,7 +158,7 @@ export default class SelectionTool extends BaseTool {
158
158
  }
159
159
  }
160
160
  onGestureCancel() {
161
- var _a, _b, _c;
161
+ var _a, _b, _c, _d;
162
162
  if (this.selectionBoxHandlingEvt) {
163
163
  (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragCancel();
164
164
  }
@@ -167,6 +167,8 @@ export default class SelectionTool extends BaseTool {
167
167
  (_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.cancelSelection();
168
168
  this.selectionBox = this.prevSelectionBox;
169
169
  (_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.addTo(this.handleOverlay);
170
+ (_d = this.selectionBox) === null || _d === void 0 ? void 0 : _d.recomputeRegion();
171
+ this.prevSelectionBox = null;
170
172
  }
171
173
  this.expandingSelectionBox = false;
172
174
  }
@@ -282,6 +284,10 @@ export default class SelectionTool extends BaseTool {
282
284
  });
283
285
  return true;
284
286
  }
287
+ if (evt.key === 'a') {
288
+ // Selected all in onKeyDown. Don't finalizeTransform.
289
+ return true;
290
+ }
285
291
  }
286
292
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
287
293
  this.selectionBox.finalizeTransform();
@@ -298,7 +304,7 @@ export default class SelectionTool extends BaseTool {
298
304
  if (selectedElems.length === 0) {
299
305
  return false;
300
306
  }
301
- const exportViewport = new Viewport(this.editor.notifier);
307
+ const exportViewport = new Viewport(() => { });
302
308
  exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
303
309
  exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
304
310
  const svgNameSpace = 'http://www.w3.org/2000/svg';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
@@ -31,4 +31,10 @@ describe('Color4', () => {
31
31
  it('should mix red with nothing and get red', () => {
32
32
  expect(Color4.average([ Color4.red ])).objEq(Color4.red);
33
33
  });
34
+
35
+ it('different colors should be different', () => {
36
+ expect(Color4.red.eq(Color4.red)).toBe(true);
37
+ expect(Color4.red.eq(Color4.green)).toBe(false);
38
+ expect(Color4.fromString('#ff000000').eq(Color4.transparent)).toBe(true);
39
+ });
34
40
  });
package/src/Color4.ts CHANGED
@@ -10,7 +10,7 @@ export default class Color4 {
10
10
  /** Blue component. `b` ∈ [0, 1] */
11
11
  public readonly b: number,
12
12
 
13
- /** Alpha/transparent component. `a` ∈ [0, 1] */
13
+ /** Alpha/transparent component. `a` ∈ [0, 1]. 0 = transparent */
14
14
  public readonly a: number
15
15
  ) {
16
16
  }
@@ -126,6 +126,11 @@ export default class Color4 {
126
126
  return false;
127
127
  }
128
128
 
129
+ // If both completely transparent,
130
+ if (this.a === 0 && other.a === 0) {
131
+ return true;
132
+ }
133
+
129
134
  return this.toHexString() === other.toHexString();
130
135
  }
131
136
 
package/src/Editor.ts CHANGED
@@ -109,9 +109,6 @@ export class Editor {
109
109
  */
110
110
  public readonly image: EditorImage;
111
111
 
112
- /** Viewport for the exported/imported image. */
113
- private importExportViewport: Viewport;
114
-
115
112
  /**
116
113
  * Allows transforming the view and querying information about
117
114
  * what is currently visible.
@@ -215,8 +212,13 @@ export class Editor {
215
212
  this.renderingRegion.setAttribute('alt', '');
216
213
 
217
214
  this.notifier = new EventDispatcher();
218
- this.importExportViewport = new Viewport(this.notifier);
219
- this.viewport = new Viewport(this.notifier);
215
+ this.viewport = new Viewport((oldTransform, newTransform) => {
216
+ this.notifier.dispatch(EditorEventType.ViewportChanged, {
217
+ kind: EditorEventType.ViewportChanged,
218
+ newTransform,
219
+ oldTransform,
220
+ });
221
+ });
220
222
  this.display = new Display(this, this.settings.renderingMode, this.renderingRegion);
221
223
  this.image = new EditorImage();
222
224
  this.history = new UndoRedoHistory(this, this.announceRedoCallback, this.announceUndoCallback);
@@ -224,9 +226,6 @@ export class Editor {
224
226
 
225
227
  parent.appendChild(this.container);
226
228
 
227
- // Default to a 500x500 image
228
- this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
229
-
230
229
  this.viewport.updateScreenSize(
231
230
  Vec2.of(this.display.width, this.display.height)
232
231
  );
@@ -773,7 +772,7 @@ export class Editor {
773
772
  const exportRectFill = { fill: Color4.fromHex('#44444455') };
774
773
  const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
775
774
  renderer.drawRect(
776
- this.importExportViewport.visibleRect,
775
+ this.getImportExportRect(),
777
776
  exportRectStrokeWidth,
778
777
  exportRectFill
779
778
  );
@@ -920,13 +919,14 @@ export class Editor {
920
919
  public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
921
920
  const canvas = document.createElement('canvas');
922
921
 
923
- const resolution = this.importExportViewport.getScreenRectSize();
922
+ const importExportViewport = this.image.getImportExportViewport();
923
+ const resolution = importExportViewport.getScreenRectSize();
924
924
 
925
925
  canvas.width = resolution.x;
926
926
  canvas.height = resolution.y;
927
927
 
928
928
  const ctx = canvas.getContext('2d')!;
929
- const renderer = new CanvasRenderer(ctx, this.importExportViewport);
929
+ const renderer = new CanvasRenderer(ctx, importExportViewport);
930
930
 
931
931
  this.image.renderAll(renderer);
932
932
 
@@ -935,12 +935,12 @@ export class Editor {
935
935
  }
936
936
 
937
937
  public toSVG(): SVGElement {
938
- const importExportViewport = this.importExportViewport;
938
+ const importExportViewport = this.image.getImportExportViewport();
939
939
  const svgNameSpace = 'http://www.w3.org/2000/svg';
940
940
  const result = document.createElementNS(svgNameSpace, 'svg');
941
941
  const renderer = new SVGRenderer(result, importExportViewport);
942
942
 
943
- const origTransform = this.importExportViewport.canvasToScreenTransform;
943
+ const origTransform = importExportViewport.canvasToScreenTransform;
944
944
  // Render with (0,0) at (0,0) — we'll handle translation with
945
945
  // the viewBox property.
946
946
  importExportViewport.resetTransform(Mat33.identity);
@@ -992,33 +992,12 @@ export class Editor {
992
992
 
993
993
  // Returns the size of the visible region of the output SVG
994
994
  public getImportExportRect(): Rect2 {
995
- return this.importExportViewport.visibleRect;
995
+ return this.image.getImportExportViewport().visibleRect;
996
996
  }
997
997
 
998
998
  // Resize the output SVG to match `imageRect`.
999
999
  public setImportExportRect(imageRect: Rect2): Command {
1000
- const origSize = this.importExportViewport.visibleRect.size;
1001
- const origTransform = this.importExportViewport.canvasToScreenTransform;
1002
-
1003
- return new class extends Command {
1004
- public apply(editor: Editor) {
1005
- const viewport = editor.importExportViewport;
1006
- viewport.updateScreenSize(imageRect.size);
1007
- viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
1008
- editor.queueRerender();
1009
- }
1010
-
1011
- public unapply(editor: Editor) {
1012
- const viewport = editor.importExportViewport;
1013
- viewport.updateScreenSize(origSize);
1014
- viewport.resetTransform(origTransform);
1015
- editor.queueRerender();
1016
- }
1017
-
1018
- public description(_editor: Editor, localizationTable: EditorLocalization) {
1019
- return localizationTable.resizeOutputCommand(imageRect);
1020
- }
1021
- };
1000
+ return this.image.setImportExportRect(imageRect);
1022
1001
  }
1023
1002
 
1024
1003
  /**
@@ -6,21 +6,80 @@ import Rect2 from './math/Rect2';
6
6
  import { EditorLocalization } from './localization';
7
7
  import RenderingCache from './rendering/caching/RenderingCache';
8
8
  import SerializableCommand from './commands/SerializableCommand';
9
+ import EventDispatcher from './EventDispatcher';
10
+ import { Vec2 } from './math/Vec2';
11
+ import Command from './commands/Command';
12
+ import Mat33 from './math/Mat33';
9
13
 
10
14
  // @internal Sort by z-index, low to high
11
15
  export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
12
16
  leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
13
17
  };
14
18
 
19
+ export enum EditorImageEventType {
20
+ ExportViewportChanged
21
+ }
22
+
23
+ export type EditorImageNotifier = EventDispatcher<EditorImageEventType, { image: EditorImage }>;
24
+
15
25
  // Handles lookup/storage of elements in the image
16
26
  export default class EditorImage {
17
27
  private root: ImageNode;
18
28
  private componentsById: Record<string, AbstractComponent>;
19
29
 
30
+ /** Viewport for the exported/imported image. */
31
+ private importExportViewport: Viewport;
32
+
33
+ // @internal
34
+ public readonly notifier: EditorImageNotifier;
35
+
20
36
  // @internal
21
37
  public constructor() {
22
38
  this.root = new ImageNode();
23
39
  this.componentsById = {};
40
+
41
+ this.notifier = new EventDispatcher();
42
+ this.importExportViewport = new Viewport(() => {
43
+ this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
44
+ image: this,
45
+ });
46
+ });
47
+
48
+ // Default to a 500x500 image
49
+ this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
50
+ }
51
+
52
+ /**
53
+ * @returns a `Viewport` for rendering the image when importing/exporting.
54
+ */
55
+ public getImportExportViewport() {
56
+ return this.importExportViewport;
57
+ }
58
+
59
+ public setImportExportRect(imageRect: Rect2) {
60
+ const importExportViewport = this.getImportExportViewport();
61
+ const origSize = importExportViewport.visibleRect.size;
62
+ const origTransform = importExportViewport.canvasToScreenTransform;
63
+
64
+ return new class extends Command {
65
+ public apply(editor: Editor) {
66
+ const viewport = editor.image.getImportExportViewport();
67
+ viewport.updateScreenSize(imageRect.size);
68
+ viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
69
+ editor.queueRerender();
70
+ }
71
+
72
+ public unapply(editor: Editor) {
73
+ const viewport = editor.image.getImportExportViewport();
74
+ viewport.updateScreenSize(origSize);
75
+ viewport.resetTransform(origTransform);
76
+ editor.queueRerender();
77
+ }
78
+
79
+ public description(_editor: Editor, localizationTable: EditorLocalization) {
80
+ return localizationTable.resizeOutputCommand(imageRect);
81
+ }
82
+ };
24
83
  }
25
84
 
26
85
  // Returns the parent of the given element, if it exists.
@@ -98,10 +157,17 @@ export default class EditorImage {
98
157
  }
99
158
 
100
159
  private addElementDirectly(elem: AbstractComponent): ImageNode {
160
+ elem.onAddToImage(this);
161
+
101
162
  this.componentsById[elem.getId()] = elem;
102
163
  return this.root.addLeaf(elem);
103
164
  }
104
165
 
166
+ private removeElementDirectly(element: AbstractComponent) {
167
+ const container = this.findParent(element);
168
+ container?.remove();
169
+ }
170
+
105
171
  /**
106
172
  * Returns a command that adds the given element to the `EditorImage`.
107
173
  * If `applyByFlattening` is true, the content of the wet ink renderer is
@@ -113,6 +179,11 @@ export default class EditorImage {
113
179
  return new EditorImage.AddElementCommand(elem, applyByFlattening);
114
180
  }
115
181
 
182
+ /** @see EditorImage.addElement */
183
+ public addElement(elem: AbstractComponent, applyByFlattening: boolean = true) {
184
+ return EditorImage.addElement(elem, applyByFlattening);
185
+ }
186
+
116
187
  // A Command that can access private [EditorImage] functionality
117
188
  private static AddElementCommand = class extends SerializableCommand {
118
189
  private serializedElem: any;
@@ -147,8 +218,7 @@ export default class EditorImage {
147
218
  }
148
219
 
149
220
  public unapply(editor: Editor) {
150
- const container = editor.image.findParent(this.element);
151
- container?.remove();
221
+ editor.image.removeElementDirectly(this.element);
152
222
  editor.queueRerender();
153
223
  }
154
224
 
@@ -395,6 +465,8 @@ export class ImageNode {
395
465
  return;
396
466
  }
397
467
 
468
+ this.content?.onRemoveFromImage();
469
+
398
470
  const oldChildCount = this.parent.children.length;
399
471
  this.parent.children = this.parent.children.filter(node => {
400
472
  return node !== this;
@@ -20,6 +20,9 @@
20
20
 
21
21
  type Listener<Value> = (data: Value)=> void;
22
22
  type CallbackHandler<EventType> = (data: EventType)=> void;
23
+ export interface DispatcherEventListener {
24
+ remove: ()=>void;
25
+ }
23
26
 
24
27
  // { @inheritDoc EventDispatcher! }
25
28
  export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
@@ -38,7 +41,7 @@ export default class EventDispatcher<EventKeyType extends string|symbol|number,
38
41
  }
39
42
  }
40
43
 
41
- public on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {
44
+ public on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): DispatcherEventListener {
42
45
  if (!this.listeners[eventName]) this.listeners[eventName] = [];
43
46
  this.listeners[eventName]!.push(callback);
44
47
 
package/src/SVGLoader.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import Color4 from './Color4';
2
2
  import AbstractComponent from './components/AbstractComponent';
3
+ import ImageBackground, { BackgroundType, imageBackgroundCSSClassName } from './components/ImageBackground';
3
4
  import ImageComponent from './components/ImageComponent';
4
5
  import Stroke from './components/Stroke';
5
6
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
@@ -186,6 +187,12 @@ export default class SVGLoader implements ImageLoader {
186
187
  await this.onAddComponent?.(elem);
187
188
  }
188
189
 
190
+ private async addBackground(node: SVGPathElement) {
191
+ const fill = Color4.fromString(node.getAttribute('fill') ?? node.style.fill ?? 'black');
192
+ const elem = new ImageBackground(BackgroundType.SolidColor, fill);
193
+ await this.onAddComponent?.(elem);
194
+ }
195
+
189
196
  // If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
190
197
  // to prevent storing duplicate transform information when saving the component.
191
198
  private getTransform(elem: SVGElement, supportedAttrs?: string[], computedStyles?: CSSStyleDeclaration): Mat33 {
@@ -359,7 +366,11 @@ export default class SVGLoader implements ImageLoader {
359
366
  // Continue -- visit the node's children.
360
367
  break;
361
368
  case 'path':
362
- await this.addPath(node as SVGPathElement);
369
+ if (node.classList.contains(imageBackgroundCSSClassName)) {
370
+ await this.addBackground(node as SVGPathElement);
371
+ } else {
372
+ await this.addPath(node as SVGPathElement);
373
+ }
363
374
  break;
364
375
  case 'text':
365
376
  await this.addText(node as SVGTextElement);