js-draw 0.12.0 → 0.13.1

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 (73) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.d.ts +12 -0
  4. package/dist/src/Color4.js +16 -0
  5. package/dist/src/Editor.d.ts +33 -18
  6. package/dist/src/Editor.js +22 -19
  7. package/dist/src/EditorImage.d.ts +12 -0
  8. package/dist/src/EditorImage.js +12 -0
  9. package/dist/src/Pointer.d.ts +1 -0
  10. package/dist/src/Pointer.js +8 -0
  11. package/dist/src/SVGLoader.d.ts +5 -0
  12. package/dist/src/SVGLoader.js +6 -1
  13. package/dist/src/Viewport.d.ts +30 -1
  14. package/dist/src/Viewport.js +39 -9
  15. package/dist/src/commands/invertCommand.js +1 -1
  16. package/dist/src/components/AbstractComponent.d.ts +19 -0
  17. package/dist/src/components/AbstractComponent.js +17 -2
  18. package/dist/src/lib.d.ts +6 -3
  19. package/dist/src/lib.js +4 -1
  20. package/dist/src/math/Mat33.d.ts +1 -1
  21. package/dist/src/math/Mat33.js +1 -1
  22. package/dist/src/rendering/Display.d.ts +9 -11
  23. package/dist/src/rendering/Display.js +12 -14
  24. package/dist/src/rendering/lib.d.ts +3 -0
  25. package/dist/src/rendering/lib.js +3 -0
  26. package/dist/src/rendering/renderers/DummyRenderer.js +2 -2
  27. package/dist/src/rendering/renderers/SVGRenderer.js +4 -0
  28. package/dist/src/toolbar/IconProvider.d.ts +1 -1
  29. package/dist/src/toolbar/IconProvider.js +90 -29
  30. package/dist/src/toolbar/makeColorInput.js +8 -1
  31. package/dist/src/tools/PanZoom.d.ts +3 -1
  32. package/dist/src/tools/PanZoom.js +31 -6
  33. package/dist/src/tools/PasteHandler.d.ts +11 -4
  34. package/dist/src/tools/PasteHandler.js +12 -5
  35. package/dist/src/tools/Pen.d.ts +7 -2
  36. package/dist/src/tools/Pen.js +39 -6
  37. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
  38. package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
  39. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
  40. package/dist/src/tools/SelectionTool/SelectionTool.js +53 -15
  41. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  42. package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
  43. package/dist/src/tools/UndoRedoShortcut.js +2 -4
  44. package/package.json +2 -2
  45. package/src/Color4.test.ts +11 -0
  46. package/src/Color4.ts +22 -0
  47. package/src/Editor.ts +36 -22
  48. package/src/EditorImage.ts +12 -0
  49. package/src/Pointer.ts +19 -0
  50. package/src/SVGLoader.ts +6 -1
  51. package/src/Viewport.ts +50 -11
  52. package/src/commands/invertCommand.ts +1 -1
  53. package/src/components/AbstractComponent.ts +33 -2
  54. package/src/lib.ts +6 -3
  55. package/src/math/Mat33.ts +1 -1
  56. package/src/rendering/Display.ts +12 -15
  57. package/src/rendering/RenderingStyle.ts +1 -1
  58. package/src/rendering/lib.ts +4 -0
  59. package/src/rendering/renderers/DummyRenderer.ts +2 -3
  60. package/src/rendering/renderers/SVGRenderer.ts +4 -0
  61. package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
  62. package/src/toolbar/HTMLToolbar.ts +1 -1
  63. package/src/toolbar/IconProvider.ts +98 -31
  64. package/src/toolbar/makeColorInput.ts +9 -1
  65. package/src/tools/PanZoom.ts +37 -7
  66. package/src/tools/PasteHandler.ts +12 -6
  67. package/src/tools/Pen.test.ts +44 -1
  68. package/src/tools/Pen.ts +53 -8
  69. package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
  70. package/src/tools/SelectionTool/SelectionTool.ts +67 -15
  71. package/src/tools/ToolSwitcherShortcut.ts +10 -5
  72. package/src/tools/UndoRedoShortcut.ts +2 -5
  73. package/typedoc.json +2 -2
@@ -1,6 +1,3 @@
1
- // Allows users to select/transform portions of the `EditorImage`.
2
- // With respect to `extend`ing, `SelectionTool` is not stable.
3
- // @packageDocumentation
4
1
  import Mat33 from '../../math/Mat33';
5
2
  import { Vec2 } from '../../math/Vec2';
6
3
  import { EditorEventType } from '../../types';
@@ -10,7 +7,8 @@ import SVGRenderer from '../../rendering/renderers/SVGRenderer';
10
7
  import Selection from './Selection';
11
8
  import TextComponent from '../../components/TextComponent';
12
9
  export const cssPrefix = 'selection-tool-';
13
- // {@inheritDoc SelectionTool!}
10
+ // Allows users to select/transform portions of the `EditorImage`.
11
+ // With respect to `extend`ing, `SelectionTool` is not stable.
14
12
  export default class SelectionTool extends BaseTool {
15
13
  constructor(editor, description) {
16
14
  super(editor.notifier, description);
@@ -18,6 +16,7 @@ export default class SelectionTool extends BaseTool {
18
16
  this.lastEvtTarget = null;
19
17
  this.expandingSelectionBox = false;
20
18
  this.shiftKeyPressed = false;
19
+ this.ctrlKeyPressed = false;
21
20
  this.selectionBoxHandlingEvt = false;
22
21
  this.handleOverlay = document.createElement('div');
23
22
  editor.createHTMLOverlay(this.handleOverlay);
@@ -45,17 +44,37 @@ export default class SelectionTool extends BaseTool {
45
44
  }
46
45
  this.selectionBox.addTo(this.handleOverlay);
47
46
  }
48
- onPointerDown(event) {
49
- var _a;
50
- if (event.allPointers.length === 1 && event.current.isPrimary) {
51
- if (this.lastEvtTarget && ((_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragStart(event.current, this.lastEvtTarget))) {
52
- this.selectionBoxHandlingEvt = true;
53
- this.expandingSelectionBox = false;
47
+ snapSelectionToGrid() {
48
+ if (!this.selectionBox)
49
+ throw new Error('No selection to snap!');
50
+ const topLeftOfBBox = this.selectionBox.region.topLeft;
51
+ const snapDistance = this.editor.viewport.snapToGrid(topLeftOfBBox).minus(topLeftOfBBox);
52
+ const oldTransform = this.selectionBox.getTransform();
53
+ this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDistance)));
54
+ this.selectionBox.finalizeTransform();
55
+ }
56
+ onPointerDown({ allPointers, current }) {
57
+ const snapToGrid = this.ctrlKeyPressed;
58
+ if (snapToGrid) {
59
+ current = current.snappedToGrid(this.editor.viewport);
60
+ }
61
+ if (allPointers.length === 1 && current.isPrimary) {
62
+ let transforming = false;
63
+ if (this.lastEvtTarget && this.selectionBox) {
64
+ if (snapToGrid) {
65
+ this.snapSelectionToGrid();
66
+ }
67
+ const dragStartResult = this.selectionBox.onDragStart(current, this.lastEvtTarget);
68
+ if (dragStartResult) {
69
+ transforming = true;
70
+ this.selectionBoxHandlingEvt = true;
71
+ this.expandingSelectionBox = false;
72
+ }
54
73
  }
55
- else {
74
+ if (!transforming) {
56
75
  // Shift key: Combine the new and old selection boxes at the end of the gesture.
57
76
  this.expandingSelectionBox = this.shiftKeyPressed;
58
- this.makeSelectionBox(event.current.canvasPos);
77
+ this.makeSelectionBox(current.canvasPos);
59
78
  }
60
79
  return true;
61
80
  }
@@ -64,11 +83,15 @@ export default class SelectionTool extends BaseTool {
64
83
  onPointerMove(event) {
65
84
  if (!this.selectionBox)
66
85
  return;
86
+ let currentPointer = event.current;
87
+ if (this.ctrlKeyPressed) {
88
+ currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
89
+ }
67
90
  if (this.selectionBoxHandlingEvt) {
68
- this.selectionBox.onDragUpdate(event.current);
91
+ this.selectionBox.onDragUpdate(currentPointer);
69
92
  }
70
93
  else {
71
- this.selectionBox.setToPoint(event.current.canvasPos);
94
+ this.selectionBox.setToPoint(currentPointer.canvasPos);
72
95
  }
73
96
  }
74
97
  onSelectionUpdated() {
@@ -113,7 +136,11 @@ export default class SelectionTool extends BaseTool {
113
136
  onPointerUp(event) {
114
137
  if (!this.selectionBox)
115
138
  return;
116
- this.selectionBox.setToPoint(event.current.canvasPos);
139
+ let currentPointer = event.current;
140
+ if (this.ctrlKeyPressed) {
141
+ currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
142
+ }
143
+ this.selectionBox.setToPoint(currentPointer.canvasPos);
117
144
  // Were we expanding the previous selection?
118
145
  if (this.expandingSelectionBox && this.prevSelectionBox) {
119
146
  // If so, finish expanding.
@@ -142,6 +169,10 @@ export default class SelectionTool extends BaseTool {
142
169
  this.expandingSelectionBox = false;
143
170
  }
144
171
  onKeyPress(event) {
172
+ if (event.key === 'Control') {
173
+ this.ctrlKeyPressed = true;
174
+ return true;
175
+ }
145
176
  if (this.selectionBox && event.ctrlKey && event.key === 'd') {
146
177
  // Handle duplication on key up — we don't want to accidentally duplicate
147
178
  // many times.
@@ -235,6 +266,10 @@ export default class SelectionTool extends BaseTool {
235
266
  return handled;
236
267
  }
237
268
  onKeyUp(evt) {
269
+ if (evt.key === 'Control') {
270
+ this.ctrlKeyPressed = false;
271
+ return true;
272
+ }
238
273
  if (evt.key === 'Shift') {
239
274
  this.shiftKeyPressed = false;
240
275
  return true;
@@ -291,6 +326,8 @@ export default class SelectionTool extends BaseTool {
291
326
  // Clear the selection
292
327
  this.handleOverlay.replaceChildren();
293
328
  this.selectionBox = null;
329
+ this.shiftKeyPressed = false;
330
+ this.ctrlKeyPressed = false;
294
331
  this.handleOverlay.style.display = enabled ? 'block' : 'none';
295
332
  if (enabled) {
296
333
  this.handleOverlay.tabIndex = 0;
@@ -355,4 +392,5 @@ SelectionTool.handleableKeys = [
355
392
  'e', 'j', 'ArrowDown',
356
393
  'r', 'R',
357
394
  'i', 'I', 'o', 'O',
395
+ 'Control',
358
396
  ];
@@ -1,6 +1,14 @@
1
1
  import Editor from '../Editor';
2
2
  import { KeyPressEvent } from '../types';
3
3
  import BaseTool from './BaseTool';
4
+ /**
5
+ * Handles keyboard events used, by default, to select tools. By default,
6
+ * 1 maps to the first primary tool, 2 to the second primary tool, ... .
7
+ *
8
+ * This is in the default set of {@link ToolController} tools.
9
+ *
10
+ * @deprecated This may be replaced in the future.
11
+ */
4
12
  export default class ToolSwitcherShortcut extends BaseTool {
5
13
  private editor;
6
14
  constructor(editor: Editor);
@@ -1,12 +1,18 @@
1
- // Handles ctrl+1, ctrl+2, ctrl+3, ..., shortcuts for switching tools.
2
- // @packageDocumentation
3
1
  import BaseTool from './BaseTool';
4
- // {@inheritDoc ToolSwitcherShortcut!}
2
+ /**
3
+ * Handles keyboard events used, by default, to select tools. By default,
4
+ * 1 maps to the first primary tool, 2 to the second primary tool, ... .
5
+ *
6
+ * This is in the default set of {@link ToolController} tools.
7
+ *
8
+ * @deprecated This may be replaced in the future.
9
+ */
5
10
  export default class ToolSwitcherShortcut extends BaseTool {
6
11
  constructor(editor) {
7
12
  super(editor.notifier, editor.localization.changeTool);
8
13
  this.editor = editor;
9
14
  }
15
+ // @internal
10
16
  onKeyPress({ key }) {
11
17
  const toolController = this.editor.toolController;
12
18
  const primaryTools = toolController.getPrimaryTools();
@@ -1,13 +1,11 @@
1
- // Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
2
- // @packageDocumentation
3
1
  import BaseTool from './BaseTool';
4
- // {@inheritDoc UndoRedoShortcut!}
2
+ // Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
5
3
  export default class UndoRedoShortcut extends BaseTool {
6
4
  constructor(editor) {
7
5
  super(editor.notifier, editor.localization.undoRedoTool);
8
6
  this.editor = editor;
9
7
  }
10
- // Activate undo/redo
8
+ // @internal
11
9
  onKeyPress({ key, ctrlKey }) {
12
10
  if (ctrlKey) {
13
11
  if (key === 'z') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
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",
@@ -101,7 +101,7 @@
101
101
  "ts-jest": "^29.0.3",
102
102
  "ts-loader": "^9.4.2",
103
103
  "ts-node": "^10.9.1",
104
- "typedoc": "^0.23.23",
104
+ "typedoc": "^0.23.24",
105
105
  "typescript": "^4.9.4",
106
106
  "webpack": "^5.75.0"
107
107
  },
@@ -16,4 +16,15 @@ describe('Color4', () => {
16
16
  expect(Color4.fromString('rgba ( 255, 0,\t 0, 0.5)')).objEq(Color4.ofRGBA(1, 0, 0, 0.5));
17
17
  expect(Color4.fromString('rgba( 0, 0, 128, 0)')).objEq(Color4.ofRGBA(0, 0, 128/255, 0));
18
18
  });
19
+
20
+ it('should mix blue and red to get dark purple', () => {
21
+ expect(Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 0, 1), 0.5)).objEq(Color4.ofRGB(0.5, 0, 0.5));
22
+ expect(Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 0, 1), 0.1)).objEq(Color4.ofRGB(0.9, 0, 0.1));
23
+ });
24
+
25
+ it('should mix red and green to get yellow', () => {
26
+ expect(Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.3)).objEq(
27
+ Color4.ofRGB(0.7, 0.3, 0)
28
+ );
29
+ });
19
30
  });
package/src/Color4.ts CHANGED
@@ -129,6 +129,28 @@ export default class Color4 {
129
129
  return this.toHexString() === other.toHexString();
130
130
  }
131
131
 
132
+ /**
133
+ * If `fractionTo` is not in the range [0, 1], it will be clamped to the nearest number
134
+ * in that range. For example, `a.mix(b, -1)` is equivalent to `a.mix(b, 0)`.
135
+ *
136
+ * @returns a color `fractionTo` of the way from this color to `other`.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.1) // -> Color4(0.9, 0.1, 0)
141
+ * ```
142
+ */
143
+ public mix(other: Color4, fractionTo: number): Color4 {
144
+ fractionTo = Math.min(Math.max(fractionTo, 0), 1);
145
+ const fractionOfThis = 1 - fractionTo;
146
+ return new Color4(
147
+ this.r * fractionOfThis + other.r * fractionTo,
148
+ this.g * fractionOfThis + other.g * fractionTo,
149
+ this.b * fractionOfThis + other.b * fractionTo,
150
+ this.a * fractionOfThis + other.a * fractionTo,
151
+ );
152
+ }
153
+
132
154
  private hexString: string|null = null;
133
155
 
134
156
  /**
package/src/Editor.ts CHANGED
@@ -1,22 +1,3 @@
1
- /**
2
- * The main entrypoint for the full editor.
3
- *
4
- * @example
5
- * To create an editor with a toolbar,
6
- * ```
7
- * const editor = new Editor(document.body);
8
- *
9
- * const toolbar = editor.addToolbar();
10
- * toolbar.addActionButton('Save', () => {
11
- * const saveData = editor.toSVG().outerHTML;
12
- * // Do something with saveData...
13
- * });
14
- * ```
15
- *
16
- * @packageDocumentation
17
- */
18
-
19
-
20
1
  import EditorImage from './EditorImage';
21
2
  import ToolController from './tools/ToolController';
22
3
  import { InputEvtType, PointerEvt, EditorNotifier, EditorEventType, ImageLoader } from './types';
@@ -70,12 +51,27 @@ export interface EditorSettings {
70
51
  iconProvider: IconProvider,
71
52
  }
72
53
 
73
- // { @inheritDoc Editor! }
54
+ /**
55
+ * The main entrypoint for the full editor.
56
+ *
57
+ * @example
58
+ * To create an editor with a toolbar,
59
+ * ```
60
+ * const editor = new Editor(document.body);
61
+ *
62
+ * const toolbar = editor.addToolbar();
63
+ * toolbar.addActionButton('Save', () => {
64
+ * const saveData = editor.toSVG().outerHTML;
65
+ * // Do something with saveData...
66
+ * });
67
+ * ```
68
+ */
74
69
  export class Editor {
75
70
  // Wrapper around the viewport and toolbar
76
71
  private container: HTMLElement;
77
72
  private renderingRegion: HTMLElement;
78
73
 
74
+ /** Manages drawing surfaces/{@link lib!AbstractRenderer}s. */
79
75
  public display: Display;
80
76
 
81
77
  /**
@@ -116,11 +112,23 @@ export class Editor {
116
112
  /** Viewport for the exported/imported image. */
117
113
  private importExportViewport: Viewport;
118
114
 
115
+ /**
116
+ * Allows transforming the view and querying information about
117
+ * what is currently visible.
118
+ */
119
+ public readonly viewport: Viewport;
120
+
119
121
  /** @internal */
120
122
  public readonly localization: EditorLocalization;
121
123
 
124
+ /** {@link lib!EditorSettings.iconProvider} */
122
125
  public readonly icons: IconProvider;
123
- public readonly viewport: Viewport;
126
+
127
+ /**
128
+ * Controls the list of tools. See
129
+ * [the custom tool example](https://github.com/personalizedrefrigerator/js-draw/tree/main/docs/example-custom-tools)
130
+ * for more.
131
+ */
124
132
  public readonly toolController: ToolController;
125
133
 
126
134
  /**
@@ -765,12 +773,18 @@ export class Editor {
765
773
  this.nextRerenderListeners = [];
766
774
  }
767
775
 
776
+ /**
777
+ * @see {@link Display.getWetInkRenderer} {@link Display.flatten}
778
+ */
768
779
  public drawWetInk(...path: RenderablePathSpec[]) {
769
780
  for (const part of path) {
770
781
  this.display.getWetInkRenderer().drawPath(part);
771
782
  }
772
783
  }
773
784
 
785
+ /**
786
+ * @see {@link Display.getWetInkRenderer}
787
+ */
774
788
  public clearWetInk() {
775
789
  this.display.getWetInkRenderer().clear();
776
790
  }
@@ -895,7 +909,7 @@ export class Editor {
895
909
  public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
896
910
  const canvas = document.createElement('canvas');
897
911
 
898
- const resolution = this.importExportViewport.getResolution();
912
+ const resolution = this.importExportViewport.getScreenRectSize();
899
913
 
900
914
  canvas.width = resolution.x;
901
915
  canvas.height = resolution.y;
@@ -75,6 +75,11 @@ export default class EditorImage {
75
75
  delete this.componentsById[elem.getId()];
76
76
  }
77
77
 
78
+ /**
79
+ * @returns the AbstractComponent with `id`, if it exists.
80
+ *
81
+ * @see {@link AbstractComponent.getId}
82
+ */
78
83
  public lookupElement(id: string): AbstractComponent|null {
79
84
  return this.componentsById[id] ?? null;
80
85
  }
@@ -84,6 +89,13 @@ export default class EditorImage {
84
89
  return this.root.addLeaf(elem);
85
90
  }
86
91
 
92
+ /**
93
+ * Returns a command that adds the given element to the `EditorImage`.
94
+ * If `applyByFlattening` is true, the content of the wet ink renderer is
95
+ * rendered onto the main rendering canvas instead of doing a full re-render.
96
+ *
97
+ * @see {@link Display.flatten}
98
+ */
87
99
  public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): SerializableCommand {
88
100
  return new EditorImage.AddElementCommand(elem, applyByFlattening);
89
101
  }
package/src/Pointer.ts CHANGED
@@ -36,6 +36,25 @@ export default class Pointer {
36
36
  ) {
37
37
  }
38
38
 
39
+ // Snaps this pointer to the nearest grid point (rounds the coordinates of this
40
+ // pointer based on the current zoom). Returns a new Pointer and does not modify
41
+ // this.
42
+ public snappedToGrid(viewport: Viewport): Pointer {
43
+ const snappedCanvasPos = viewport.snapToGrid(this.canvasPos);
44
+ const snappedScreenPos = viewport.canvasToScreen(snappedCanvasPos);
45
+
46
+ return new Pointer(
47
+ snappedScreenPos,
48
+ snappedCanvasPos,
49
+ this.pressure,
50
+ this.isPrimary,
51
+ this.down,
52
+ this.device,
53
+ this.id,
54
+ this.timeStamp,
55
+ );
56
+ }
57
+
39
58
  // Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
40
59
  // considered the top left of `relativeTo`.
41
60
  public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer {
package/src/SVGLoader.ts CHANGED
@@ -31,6 +31,7 @@ export type SVGLoaderUnknownStyleAttribute = { key: string, value: string, prior
31
31
 
32
32
  const supportedStrokeFillStyleAttrs = [ 'stroke', 'fill', 'stroke-width' ];
33
33
 
34
+ // Handles loading images from SVG.
34
35
  export default class SVGLoader implements ImageLoader {
35
36
  private onAddComponent: ComponentAddedListener|null = null;
36
37
  private onProgress: OnProgressListener|null = null;
@@ -422,7 +423,11 @@ export default class SVGLoader implements ImageLoader {
422
423
  this.onFinish?.();
423
424
  }
424
425
 
425
- // @param sanitize - if `true`, don't store unknown attributes.
426
+ /**
427
+ * @see {@link Editor.loadFrom}
428
+ * @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
429
+ * @param sanitize - if `true`, don't store unknown attributes.
430
+ */
426
431
  public static fromString(text: string, sanitize: boolean = false): SVGLoader {
427
432
  const sandbox = document.createElement('iframe');
428
433
  sandbox.src = 'about:blank';
package/src/Viewport.ts CHANGED
@@ -92,26 +92,30 @@ export class Viewport {
92
92
  this.screenRect = this.screenRect.resizedTo(screenSize);
93
93
  }
94
94
 
95
- // Get the screen's visible region transformed into canvas space.
95
+ /** Get the screen's visible region transformed into canvas space. */
96
96
  public get visibleRect(): Rect2 {
97
97
  return this.screenRect.transformedBoundingBox(this.inverseTransform);
98
98
  }
99
99
 
100
- // the given point, but in canvas coordinates
100
+ /** @returns the given point, but in canvas coordinates */
101
101
  public screenToCanvas(screenPoint: Point2): Point2 {
102
102
  return this.inverseTransform.transformVec2(screenPoint);
103
103
  }
104
104
 
105
+ /** @returns the given point transformed into screen coordinates. */
105
106
  public canvasToScreen(canvasPoint: Point2): Point2 {
106
107
  return this.transform.transformVec2(canvasPoint);
107
108
  }
108
109
 
110
+ /** @returns a command that transforms the canvas by `transform`. */
109
111
  public static transformBy(transform: Mat33): ViewportTransform {
110
112
  return new Viewport.ViewportTransform(transform);
111
113
  }
112
114
 
113
- // Updates the transformation directly. Using `transformBy` is preferred.
114
- // [newTransform] should map from canvas coordinates to screen coordinates.
115
+ /**
116
+ * Updates the transformation directly. Using `transformBy` is preferred.
117
+ * @param newTransform - should map from canvas coordinates to screen coordinates.
118
+ */
115
119
  public resetTransform(newTransform: Mat33 = Mat33.identity) {
116
120
  const oldTransform = this.transform;
117
121
  this.transform = newTransform;
@@ -131,29 +135,64 @@ export class Viewport {
131
135
  return this.transform;
132
136
  }
133
137
 
134
- public getResolution(): Vec2 {
138
+ /** @returns the size of the visible region in pixels. */
139
+ public getScreenRectSize(): Vec2 {
135
140
  return this.screenRect.size;
136
141
  }
137
142
 
138
- // Returns the amount a vector on the canvas is scaled to become a vector on the screen.
143
+ /** Alias for `getScreenRectSize`. @deprecated */
144
+ public getResolution() {
145
+ return this.getScreenRectSize();
146
+ }
147
+
148
+ /** @returns the amount a vector on the canvas is scaled to become a vector on the screen. */
139
149
  public getScaleFactor(): number {
140
150
  // Use transformVec3 to avoid translating the vector
141
151
  return this.transform.transformVec3(Vec3.unitX).magnitude();
142
152
  }
143
153
 
144
- // Returns the size of one screen pixel in canvas units.
154
+ /**
155
+ * @returns `getScaleFactor()` rounded to the nearest power of 10.
156
+ * For example, if `getScaleFactor()` returns 101, `getScaleFactorToNearestPowerOfTen()`
157
+ * should return `100` because `100` is the nearest power of 10 to 101.
158
+ */
159
+ public getScaleFactorToNearestPowerOfTen() {
160
+ const scaleFactor = this.getScaleFactor();
161
+ return Math.pow(10, Math.round(Math.log10(scaleFactor)));
162
+ }
163
+
164
+ public snapToGrid(canvasPos: Point2) {
165
+ const snapCoordinate = (coordinate: number) => {
166
+ const scaleFactor = this.getScaleFactorToNearestPowerOfTen();
167
+ const roundFactor = scaleFactor / 100;
168
+ const snapped = Math.round(coordinate * roundFactor) / roundFactor;
169
+
170
+ return snapped;
171
+ };
172
+
173
+ const snappedCanvasPos = Vec2.of(
174
+ snapCoordinate(canvasPos.x), snapCoordinate(canvasPos.y)
175
+ );
176
+ return snappedCanvasPos;
177
+ }
178
+
179
+ /** Returns the size of one screen pixel in canvas units. */
145
180
  public getSizeOfPixelOnCanvas(): number {
146
181
  return 1/this.getScaleFactor();
147
182
  }
148
183
 
149
- // Returns the angle of the canvas in radians.
150
- // This is the angle by which the canvas is rotated relative to the screen.
184
+ /**
185
+ * @returns the angle of the canvas in radians.
186
+ * This is the angle by which the canvas is rotated relative to the screen.
187
+ */
151
188
  public getRotationAngle(): number {
152
189
  return this.transform.transformVec3(Vec3.unitX).angle();
153
190
  }
154
191
 
155
- // Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
156
- // its original location. This is useful for preparing data for base-10 conversion.
192
+ /**
193
+ * Rounds the given `point` to a multiple of 10 such that it is within `tolerance` of
194
+ * its original location. This is useful for preparing data for base-10 conversion.
195
+ */
157
196
  public static roundPoint<T extends Point2|number>(
158
197
  point: T, tolerance: number,
159
198
  ): PointDataType<T>;
@@ -3,7 +3,7 @@ import { EditorLocalization } from '../localization';
3
3
  import Command from './Command';
4
4
  import SerializableCommand from './SerializableCommand';
5
5
 
6
- // Returns a command taht does the opposite of the given command --- `result.apply()` calls
6
+ // Returns a command that does the opposite of the given command --- `result.apply()` calls
7
7
  // `command.unapply()` and `result.unapply()` calls `command.apply()`.
8
8
  const invertCommand = <T extends Command> (command: T): T extends SerializableCommand ? SerializableCommand : Command => {
9
9
  if (command instanceof SerializableCommand) {
@@ -13,9 +13,20 @@ export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
13
13
  export type DeserializeCallback = (data: string)=>AbstractComponent;
14
14
  type ComponentId = string;
15
15
 
16
+ /**
17
+ * A base class for everything that can be added to an {@link EditorImage}.
18
+ */
16
19
  export default abstract class AbstractComponent {
20
+ // The timestamp (milliseconds) at which the component was
21
+ // last changed (i.e. created/translated).
22
+ // @deprecated
17
23
  protected lastChangedTime: number;
24
+
25
+ // The bounding box of this component.
26
+ // {@link getBBox}, by default, returns `contentBBox`.
27
+ // This must be set by components.
18
28
  protected abstract contentBBox: Rect2;
29
+
19
30
  private zIndex: number;
20
31
  private id: string;
21
32
 
@@ -38,7 +49,7 @@ export default abstract class AbstractComponent {
38
49
  }
39
50
 
40
51
  // Returns a unique ID for this element.
41
- // @see { @link EditorImage!default.lookupElement }
52
+ // @see { @link lib!EditorImage.lookupElement }
42
53
  public getId() {
43
54
  return this.id;
44
55
  }
@@ -55,14 +66,22 @@ export default abstract class AbstractComponent {
55
66
  this.deserializationCallbacks[componentKind] = deserialize ?? null;
56
67
  }
57
68
 
58
- // Get and manage data attached by a loader.
69
+ // Stores data attached by a loader.
59
70
  private loadSaveData: LoadSaveDataTable = {};
71
+
72
+ /**
73
+ * Attach data that can be used while exporting the component (e.g. to SVG).
74
+ *
75
+ * This is intended for use by a {@link ImageLoader}.
76
+ */
60
77
  public attachLoadSaveData(key: string, data: LoadSaveData) {
61
78
  if (!this.loadSaveData[key]) {
62
79
  this.loadSaveData[key] = [];
63
80
  }
64
81
  this.loadSaveData[key].push(data);
65
82
  }
83
+
84
+ /** See {@link attachLoadSaveData} */
66
85
  public getLoadSaveData(): LoadSaveDataTable {
67
86
  return this.loadSaveData;
68
87
  }
@@ -71,13 +90,20 @@ export default abstract class AbstractComponent {
71
90
  return this.zIndex;
72
91
  }
73
92
 
93
+ /** @returns the bounding box of */
74
94
  public getBBox(): Rect2 {
75
95
  return this.contentBBox;
76
96
  }
77
97
 
78
98
  public abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
99
+
100
+ /** @return true if `lineSegment` intersects this component. */
79
101
  public abstract intersects(lineSegment: LineSegment2): boolean;
80
102
 
103
+ /**
104
+ * @returns true if this component intersects `rect` -- it is entirely contained
105
+ * within the rectangle or one of the rectangle's edges intersects this component.
106
+ */
81
107
  public intersectsRect(rect: Rect2): boolean {
82
108
  // If this component intersects rect,
83
109
  // it is either contained entirely within rect or intersects one of rect's edges.
@@ -200,6 +226,7 @@ export default abstract class AbstractComponent {
200
226
  }
201
227
 
202
228
  this.component.applyTransformation(newTransfm);
229
+ this.component.lastChangedTime = (new Date()).getTime();
203
230
 
204
231
  // Add the element back to the document.
205
232
  if (hadParent) {
@@ -250,6 +277,10 @@ export default abstract class AbstractComponent {
250
277
  }
251
278
  };
252
279
 
280
+ /**
281
+ * @return a description that could be read by a screen reader
282
+ * (e.g. when adding/erasing the component)
283
+ */
253
284
  public abstract description(localizationTable: ImageComponentLocalization): string;
254
285
 
255
286
  // Component-specific implementation of {@link clone}.
package/src/lib.ts CHANGED
@@ -8,25 +8,28 @@
8
8
  * ```
9
9
  *
10
10
  * @see
11
- * {@link Editor!}
11
+ * {@link Editor}
12
12
  *
13
13
  * @packageDocumentation
14
14
  */
15
15
 
16
- import Editor from './Editor';
16
+ import Editor, { EditorSettings } from './Editor';
17
17
  export { default as EditorImage } from './EditorImage';
18
18
  export * from './types';
19
19
  export { default as getLocalizationTable } from './localizations/getLocalizationTable';
20
20
  export * from './localization';
21
21
 
22
22
  export { default as Color4 } from './Color4';
23
+ export { default as SVGLoader } from './SVGLoader';
24
+ export { default as Viewport } from './Viewport';
23
25
  export * from './math/lib';
24
26
  export * from './components/lib';
25
27
  export * from './commands/lib';
26
28
  export * from './tools/lib';
27
29
  export * from './toolbar/lib';
30
+ export * from './rendering/lib';
28
31
  export { default as Pointer, PointerDevice } from './Pointer';
29
32
  export { default as HTMLToolbar } from './toolbar/HTMLToolbar';
30
33
 
31
- export { Editor };
34
+ export { Editor, EditorSettings };
32
35
  export default Editor;