js-draw 0.17.2 → 0.17.4

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 (51) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +17 -8
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +62 -1
  5. package/dist/src/Editor.js +75 -24
  6. package/dist/src/EditorImage.d.ts +4 -2
  7. package/dist/src/EditorImage.js +4 -2
  8. package/dist/src/SVGLoader.d.ts +4 -0
  9. package/dist/src/SVGLoader.js +4 -0
  10. package/dist/src/components/lib.d.ts +2 -2
  11. package/dist/src/components/lib.js +2 -2
  12. package/dist/src/lib.d.ts +2 -1
  13. package/dist/src/lib.js +2 -1
  14. package/dist/src/rendering/lib.d.ts +2 -0
  15. package/dist/src/rendering/lib.js +2 -0
  16. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +25 -0
  17. package/dist/src/rendering/renderers/CanvasRenderer.js +27 -0
  18. package/dist/src/rendering/renderers/SVGRenderer.d.ts +15 -0
  19. package/dist/src/rendering/renderers/SVGRenderer.js +27 -1
  20. package/dist/src/testing/lib.d.ts +2 -0
  21. package/dist/src/testing/lib.js +2 -0
  22. package/dist/src/testing/sendPenEvent.d.ts +12 -0
  23. package/dist/src/testing/sendPenEvent.js +19 -0
  24. package/dist/src/testing/sendTouchEvent.d.ts +36 -0
  25. package/dist/src/testing/sendTouchEvent.js +36 -0
  26. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +0 -1
  27. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +5 -20
  28. package/dist/src/toolbar/widgets/PenToolWidget.js +1 -0
  29. package/dist/src/toolbar/widgets/TextToolWidget.js +4 -1
  30. package/dist/src/tools/SelectionTool/SelectionTool.js +5 -6
  31. package/package.json +1 -1
  32. package/src/Editor.ts +78 -28
  33. package/src/EditorImage.ts +4 -2
  34. package/src/SVGLoader.ts +4 -0
  35. package/src/components/lib.ts +2 -1
  36. package/src/lib.ts +2 -1
  37. package/src/rendering/lib.ts +2 -0
  38. package/src/rendering/renderers/CanvasRenderer.ts +27 -0
  39. package/src/rendering/renderers/SVGRenderer.ts +32 -1
  40. package/src/testing/lib.ts +3 -0
  41. package/src/testing/sendPenEvent.ts +31 -0
  42. package/src/testing/sendTouchEvent.ts +36 -1
  43. package/src/toolbar/toolbar.css +5 -0
  44. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +5 -23
  45. package/src/toolbar/widgets/PenToolWidget.ts +1 -0
  46. package/src/toolbar/widgets/TextToolWidget.ts +4 -1
  47. package/src/tools/Eraser.test.ts +11 -10
  48. package/src/tools/PanZoom.test.ts +1 -1
  49. package/src/tools/Pen.test.ts +63 -62
  50. package/src/tools/SelectionTool/SelectionTool.test.ts +15 -14
  51. package/src/tools/SelectionTool/SelectionTool.ts +5 -7
@@ -2,5 +2,41 @@ import Editor from '../Editor';
2
2
  import { Vec2 } from '../math/Vec2';
3
3
  import Pointer from '../Pointer';
4
4
  import { InputEvtType } from '../types';
5
+ /**
6
+ * Dispatch a touch event to the currently selected tool. Intended for unit tests.
7
+ *
8
+ * @see {@link sendPenEvent}
9
+ *
10
+ * @example
11
+ * **Simulating a horizontal swipe gesture:**
12
+ * ```ts
13
+ * sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
14
+ * for (let i = 1; i <= 10; i++) {
15
+ * jest.advanceTimersByTime(10);
16
+ * sendTouchEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(i * 10, 0));
17
+ * }
18
+ * ```
19
+ *
20
+ * @example
21
+ * **Simulating a pinch gesture.** This example assumes that you're using [Jest with timer mocks enabled](https://jestjs.io/docs/timer-mocks).
22
+ * ```ts
23
+ * let firstPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
24
+ * let secondPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(100, 0), [ firstPointer ]);
25
+ *
26
+ * // Simulate a pinch
27
+ * const maxIterations = 10;
28
+ * for (let i = 0; i < maxIterations; i++) {
29
+ * // Use the unit testing framework's tool for increasing the current time
30
+ * // returned by (new Date()).getTime(), etc.
31
+ * jest.advanceTimersByTime(100);
32
+ *
33
+ * const point1 = Vec2.of(-i * 5, 0);
34
+ * const point2 = Vec2.of(i * 5 + 100, 0);
35
+ *
36
+ * firstPointer = sendTouchEvent(editor, InputEvtType.PointerMoveEvt, point1, [ secondPointer ]);
37
+ * secondPointer = sendTouchEvent(editor, InputEvtType.PointerMoveEvt, point2, [ firstPointer ]);
38
+ * }
39
+ * ```
40
+ */
5
41
  declare const sendTouchEvent: (editor: Editor, eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, screenPos: Vec2, allOtherPointers?: Pointer[]) => Pointer;
6
42
  export default sendTouchEvent;
@@ -1,5 +1,41 @@
1
1
  import Pointer, { PointerDevice } from '../Pointer';
2
2
  import { InputEvtType } from '../types';
3
+ /**
4
+ * Dispatch a touch event to the currently selected tool. Intended for unit tests.
5
+ *
6
+ * @see {@link sendPenEvent}
7
+ *
8
+ * @example
9
+ * **Simulating a horizontal swipe gesture:**
10
+ * ```ts
11
+ * sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
12
+ * for (let i = 1; i <= 10; i++) {
13
+ * jest.advanceTimersByTime(10);
14
+ * sendTouchEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(i * 10, 0));
15
+ * }
16
+ * ```
17
+ *
18
+ * @example
19
+ * **Simulating a pinch gesture.** This example assumes that you're using [Jest with timer mocks enabled](https://jestjs.io/docs/timer-mocks).
20
+ * ```ts
21
+ * let firstPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0));
22
+ * let secondPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(100, 0), [ firstPointer ]);
23
+ *
24
+ * // Simulate a pinch
25
+ * const maxIterations = 10;
26
+ * for (let i = 0; i < maxIterations; i++) {
27
+ * // Use the unit testing framework's tool for increasing the current time
28
+ * // returned by (new Date()).getTime(), etc.
29
+ * jest.advanceTimersByTime(100);
30
+ *
31
+ * const point1 = Vec2.of(-i * 5, 0);
32
+ * const point2 = Vec2.of(i * 5 + 100, 0);
33
+ *
34
+ * firstPointer = sendTouchEvent(editor, InputEvtType.PointerMoveEvt, point1, [ secondPointer ]);
35
+ * secondPointer = sendTouchEvent(editor, InputEvtType.PointerMoveEvt, point2, [ firstPointer ]);
36
+ * }
37
+ * ```
38
+ */
3
39
  const sendTouchEvent = (editor, eventType, screenPos, allOtherPointers) => {
4
40
  const canvasPos = editor.viewport.screenToCanvas(screenPos);
5
41
  let ptrId = 0;
@@ -10,7 +10,6 @@ export default class DocumentPropertiesWidget extends BaseWidget {
10
10
  private dropdownUpdateQueued;
11
11
  private queueDropdownUpdate;
12
12
  private updateDropdown;
13
- private getBackgroundElem;
14
13
  private setBackgroundColor;
15
14
  private getBackgroundColor;
16
15
  private updateImportExportRectSize;
@@ -1,8 +1,7 @@
1
- import Color4 from '../../Color4';
2
- import ImageBackground from '../../components/ImageBackground';
3
1
  import { EditorImageEventType } from '../../EditorImage';
4
2
  import Rect2 from '../../math/Rect2';
5
3
  import { EditorEventType } from '../../types';
4
+ import { toolbarCSSPrefix } from '../HTMLToolbar';
6
5
  import makeColorInput from '../makeColorInput';
7
6
  import BaseWidget from './BaseWidget';
8
7
  export default class DocumentPropertiesWidget extends BaseWidget {
@@ -41,26 +40,11 @@ export default class DocumentPropertiesWidget extends BaseWidget {
41
40
  this.updateDropdownContent();
42
41
  }
43
42
  }
44
- getBackgroundElem() {
45
- const backgroundComponents = [];
46
- for (const component of this.editor.image.getBackgroundComponents()) {
47
- if (component instanceof ImageBackground) {
48
- backgroundComponents.push(component);
49
- }
50
- }
51
- if (backgroundComponents.length === 0) {
52
- return null;
53
- }
54
- // Return the last background component in the list — the component with highest z-index.
55
- return backgroundComponents[backgroundComponents.length - 1];
56
- }
57
43
  setBackgroundColor(color) {
58
44
  this.editor.dispatch(this.editor.setBackgroundColor(color));
59
45
  }
60
46
  getBackgroundColor() {
61
- var _a, _b;
62
- const background = this.getBackgroundElem();
63
- return (_b = (_a = background === null || background === void 0 ? void 0 : background.getStyle()) === null || _a === void 0 ? void 0 : _a.color) !== null && _b !== void 0 ? _b : Color4.transparent;
47
+ return this.editor.estimateBackgroundColor();
64
48
  }
65
49
  updateImportExportRectSize(size) {
66
50
  const filterDimension = (dim) => {
@@ -78,6 +62,7 @@ export default class DocumentPropertiesWidget extends BaseWidget {
78
62
  }
79
63
  fillDropdown(dropdown) {
80
64
  const container = document.createElement('div');
65
+ container.classList.add(`${toolbarCSSPrefix}spacedList`);
81
66
  const backgroundColorRow = document.createElement('div');
82
67
  const backgroundColorLabel = document.createElement('label');
83
68
  backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
@@ -86,7 +71,7 @@ export default class DocumentPropertiesWidget extends BaseWidget {
86
71
  this.setBackgroundColor(color);
87
72
  }
88
73
  });
89
- colorInput.id = `document-properties-color-input-${DocumentPropertiesWidget.idCounter++}`;
74
+ colorInput.id = `${toolbarCSSPrefix}docPropertiesColorInput-${DocumentPropertiesWidget.idCounter++}`;
90
75
  backgroundColorLabel.htmlFor = colorInput.id;
91
76
  backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
92
77
  const addDimensionRow = (labelContent, onChange) => {
@@ -97,7 +82,7 @@ export default class DocumentPropertiesWidget extends BaseWidget {
97
82
  label.innerText = labelContent;
98
83
  input.type = 'number';
99
84
  input.min = '0';
100
- input.id = `document-properties-dimension-row-${DocumentPropertiesWidget.idCounter++}`;
85
+ input.id = `${toolbarCSSPrefix}docPropertiesDimensionRow-${DocumentPropertiesWidget.idCounter++}`;
101
86
  label.htmlFor = input.id;
102
87
  spacer.style.flexGrow = '1';
103
88
  input.style.flexGrow = '2';
@@ -97,6 +97,7 @@ export default class PenToolWidget extends BaseToolWidget {
97
97
  }
98
98
  fillDropdown(dropdown) {
99
99
  const container = document.createElement('div');
100
+ container.classList.add(`${toolbarCSSPrefix}spacedList`);
100
101
  const thicknessRow = document.createElement('div');
101
102
  const objectTypeRow = document.createElement('div');
102
103
  // Thickness: Value of the input is squared to allow for finer control/larger values.
@@ -24,6 +24,8 @@ export default class TextToolWidget extends BaseToolWidget {
24
24
  return this.editor.icons.makeTextIcon(textStyle);
25
25
  }
26
26
  fillDropdown(dropdown) {
27
+ const container = document.createElement('div');
28
+ container.classList.add(`${toolbarCSSPrefix}spacedList`);
27
29
  const fontRow = document.createElement('div');
28
30
  const colorRow = document.createElement('div');
29
31
  const sizeRow = document.createElement('div');
@@ -83,7 +85,8 @@ export default class TextToolWidget extends BaseToolWidget {
83
85
  sizeInput.value = `${style.size}`;
84
86
  };
85
87
  this.updateDropdownInputs();
86
- dropdown.replaceChildren(colorRow, sizeRow, fontRow);
88
+ container.replaceChildren(colorRow, sizeRow, fontRow);
89
+ dropdown.appendChild(container);
87
90
  return true;
88
91
  }
89
92
  serializeState() {
@@ -306,19 +306,18 @@ export default class SelectionTool extends BaseTool {
306
306
  }
307
307
  const exportViewport = new Viewport(() => { });
308
308
  exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
309
- exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
310
- const svgNameSpace = 'http://www.w3.org/2000/svg';
311
- const exportElem = document.createElementNS(svgNameSpace, 'svg');
309
+ exportViewport.resetTransform(Mat33.translation(bbox.topLeft.times(-1)));
312
310
  const sanitize = true;
313
- const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
311
+ const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer.fromViewport(exportViewport, sanitize);
314
312
  const text = [];
315
313
  for (const elem of selectedElems) {
316
- elem.render(renderer);
314
+ elem.render(svgRenderer);
317
315
  if (elem instanceof TextComponent) {
318
316
  text.push(elem.getText());
319
317
  }
320
318
  }
321
- event.setData('image/svg+xml', exportElem.outerHTML);
319
+ event.setData('image/svg+xml', svgExportElem.outerHTML);
320
+ event.setData('text/html', svgExportElem.outerHTML);
322
321
  if (text.length > 0) {
323
322
  event.setData('text/plain', text.join('\n'));
324
323
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
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",
package/src/Editor.ts CHANGED
@@ -28,6 +28,7 @@ import SelectionTool from './tools/SelectionTool/SelectionTool';
28
28
  import AbstractComponent from './components/AbstractComponent';
29
29
  import Erase from './commands/Erase';
30
30
  import ImageBackground, { BackgroundType } from './components/ImageBackground';
31
+ import sendPenEvent from './testing/sendPenEvent';
31
32
 
32
33
  type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
33
34
  type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
@@ -67,6 +68,9 @@ export interface EditorSettings {
67
68
  * // Do something with saveData...
68
69
  * });
69
70
  * ```
71
+ *
72
+ * See also
73
+ * [`docs/example/example.ts`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/example/example.ts#L15).
70
74
  */
71
75
  export class Editor {
72
76
  // Wrapper around the viewport and toolbar
@@ -156,6 +160,8 @@ export class Editor {
156
160
  *
157
161
  * // Add the default toolbar
158
162
  * const toolbar = editor.addToolbar();
163
+ *
164
+ * // Add a save button
159
165
  * toolbar.addActionButton({
160
166
  * label: 'Save'
161
167
  * icon: createSaveIcon(),
@@ -260,6 +266,7 @@ export class Editor {
260
266
  *
261
267
  * @example
262
268
  * ```
269
+ * // Set the editor's height to 500px
263
270
  * editor.getRootElement().style.height = '500px';
264
271
  * ```
265
272
  */
@@ -282,8 +289,10 @@ export class Editor {
282
289
 
283
290
  private previousAccessibilityAnnouncement: string = '';
284
291
 
285
- // Announce `message` for screen readers. If `message` is the same as the previous
286
- // message, it is re-announced.
292
+ /**
293
+ * Announce `message` for screen readers. If `message` is the same as the previous
294
+ * message, it is re-announced.
295
+ */
287
296
  public announceForAccessibility(message: string) {
288
297
  // Force re-announcing an announcement if announced again.
289
298
  if (message === this.previousAccessibilityAnnouncement) {
@@ -576,6 +585,25 @@ export class Editor {
576
585
  }
577
586
  }
578
587
 
588
+ /**
589
+ * Forward pointer events from `elem` to this editor. Such that right-click/right-click drag
590
+ * events are also forwarded, `elem`'s contextmenu is disabled.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * const overlay = document.createElement('div');
595
+ * editor.createHTMLOverlay(overlay);
596
+ *
597
+ * // Send all pointer events that don't have the control key pressed
598
+ * // to the editor.
599
+ * editor.handlePointerEventsFrom(overlay, (event) => {
600
+ * if (event.ctrlKey) {
601
+ * return false;
602
+ * }
603
+ * return true;
604
+ * });
605
+ * ```
606
+ */
579
607
  public handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter) {
580
608
  // May be required to prevent text selection on iOS/Safari:
581
609
  // See https://stackoverflow.com/a/70992717/17055750
@@ -741,7 +769,7 @@ export class Editor {
741
769
  * Schedule a re-render for some time in the near future. Does not schedule an additional
742
770
  * re-render if a re-render is already queued.
743
771
  *
744
- * @returns a promise that resolves when
772
+ * @returns a promise that resolves when re-rendering has completed.
745
773
  */
746
774
  public queueRerender(): Promise<void> {
747
775
  if (!this.rerenderQueued) {
@@ -766,6 +794,11 @@ export class Editor {
766
794
  return this.rerenderQueued;
767
795
  }
768
796
 
797
+ /**
798
+ * Re-renders the entire image.
799
+ *
800
+ * @see {@link Editor.queueRerender}
801
+ */
769
802
  public rerender(showImageBounds: boolean = true) {
770
803
  this.display.startRerender();
771
804
 
@@ -795,6 +828,9 @@ export class Editor {
795
828
  }
796
829
 
797
830
  /**
831
+ * Draws the given path onto the wet ink renderer. The given path will
832
+ * be displayed on top of the main image.
833
+ *
798
834
  * @see {@link Display.getWetInkRenderer} {@link Display.flatten}
799
835
  */
800
836
  public drawWetInk(...path: RenderablePathSpec[]) {
@@ -804,19 +840,28 @@ export class Editor {
804
840
  }
805
841
 
806
842
  /**
843
+ * Clears the wet ink display.
844
+ *
807
845
  * @see {@link Display.getWetInkRenderer}
808
846
  */
809
847
  public clearWetInk() {
810
848
  this.display.getWetInkRenderer().clear();
811
849
  }
812
850
 
813
- // Focuses the region used for text input/key commands.
851
+ /**
852
+ * Focuses the region used for text input/key commands.
853
+ */
814
854
  public focus() {
815
855
  this.renderingRegion.focus();
816
856
  }
817
857
 
818
- // Creates an element that will be positioned on top of the dry/wet ink
819
- // renderers.
858
+ /**
859
+ * Creates an element that will be positioned on top of the dry/wet ink
860
+ * renderers.
861
+ *
862
+ * This is useful for displaying content on top of the rendered content
863
+ * (e.g. a selection box).
864
+ */
820
865
  public createHTMLOverlay(overlay: HTMLElement) {
821
866
  overlay.classList.add('overlay');
822
867
  this.container.appendChild(overlay);
@@ -850,8 +895,13 @@ export class Editor {
850
895
  });
851
896
  }
852
897
 
853
- // Dispatch a pen event to the currently selected tool.
854
- // Intended primarially for unit tests.
898
+ /**
899
+ * Dispatch a pen event to the currently selected tool.
900
+ * Intended primarially for unit tests.
901
+ *
902
+ * @deprecated
903
+ * @see {@link sendPenEvent} {@link sendTouchEvent}
904
+ */
855
905
  public sendPenEvent(
856
906
  eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
857
907
  point: Point2,
@@ -859,16 +909,7 @@ export class Editor {
859
909
  // @deprecated
860
910
  allPointers?: Pointer[]
861
911
  ) {
862
- const mainPointer = Pointer.ofCanvasPoint(
863
- point, eventType !== InputEvtType.PointerUpEvt, this.viewport
864
- );
865
- this.toolController.dispatchInputEvent({
866
- kind: eventType,
867
- allPointers: allPointers ?? [
868
- mainPointer,
869
- ],
870
- current: mainPointer,
871
- });
912
+ sendPenEvent(this, eventType, point, allPointers);
872
913
  }
873
914
 
874
915
  public async addAndCenterComponents(components: AbstractComponent[], selectComponents: boolean = true) {
@@ -947,9 +988,9 @@ export class Editor {
947
988
 
948
989
  public toSVG(): SVGElement {
949
990
  const importExportViewport = this.image.getImportExportViewport().getTemporaryClone();
950
- const svgNameSpace = 'http://www.w3.org/2000/svg';
951
- const result = document.createElementNS(svgNameSpace, 'svg');
952
- const renderer = new SVGRenderer(result, importExportViewport);
991
+
992
+ const sanitize = false;
993
+ const { element: result, renderer } = SVGRenderer.fromViewport(importExportViewport, sanitize);
953
994
 
954
995
  const origTransform = importExportViewport.canvasToScreenTransform;
955
996
  // Render with (0,0) at (0,0) — we'll handle translation with
@@ -967,13 +1008,6 @@ export class Editor {
967
1008
  result.setAttribute('width', toRoundedString(rect.w));
968
1009
  result.setAttribute('height', toRoundedString(rect.h));
969
1010
 
970
- // Ensure the image can be identified as an SVG if downloaded.
971
- // See https://jwatt.org/svg/authoring/
972
- result.setAttribute('version', '1.1');
973
- result.setAttribute('baseProfile', 'full');
974
- result.setAttribute('xmlns', svgNameSpace);
975
-
976
-
977
1011
  return result;
978
1012
  }
979
1013
 
@@ -1047,6 +1081,22 @@ export class Editor {
1047
1081
  }
1048
1082
  }
1049
1083
 
1084
+ /**
1085
+ * @returns the average of the colors of all background components. Use this to get the current background
1086
+ * color.
1087
+ */
1088
+ public estimateBackgroundColor(): Color4 {
1089
+ const backgroundColors = [];
1090
+
1091
+ for (const component of this.image.getBackgroundComponents()) {
1092
+ if (component instanceof ImageBackground) {
1093
+ backgroundColors.push(component.getStyle().color ?? Color4.transparent);
1094
+ }
1095
+ }
1096
+
1097
+ return Color4.average(backgroundColors);
1098
+ }
1099
+
1050
1100
  // Returns the size of the visible region of the output SVG
1051
1101
  public getImportExportRect(): Rect2 {
1052
1102
  return this.image.getImportExportViewport().visibleRect;
@@ -94,8 +94,10 @@ export default class EditorImage {
94
94
  }
95
95
 
96
96
  /**
97
- * Renders all nodes visible from `viewport` (or all nodes if `viewport = null`)
98
- * @internal
97
+ * Renders all nodes visible from `viewport` (or all nodes if `viewport = null`).
98
+ *
99
+ * `viewport` is used to improve rendering performance. If given, it must match
100
+ * the viewport used by the `renderer` (if any).
99
101
  */
100
102
  public render(renderer: AbstractRenderer, viewport: Viewport|null) {
101
103
  this.background.render(renderer, viewport?.visibleRect);
package/src/SVGLoader.ts CHANGED
@@ -450,6 +450,10 @@ export default class SVGLoader implements ImageLoader {
450
450
  }
451
451
 
452
452
  /**
453
+ * Create an `SVGLoader` from the content of an SVG image. SVGs are loaded within a sandboxed
454
+ * iframe with `sandbox="allow-same-origin"`
455
+ * [thereby disabling JavaScript](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox).
456
+ *
453
457
  * @see {@link Editor.loadFrom}
454
458
  * @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
455
459
  * @param sanitize - if `true`, don't store unknown attributes.
@@ -8,7 +8,7 @@ export { default as AbstractComponent } from './AbstractComponent';
8
8
  import Stroke from './Stroke';
9
9
  import TextComponent from './TextComponent';
10
10
  import ImageComponent from './ImageComponent';
11
- import RestyleableComponent, { createRestyleComponentCommand } from './RestylableComponent';
11
+ import RestyleableComponent, { createRestyleComponentCommand, isRestylableComponent } from './RestylableComponent';
12
12
  import ImageBackground from './ImageBackground';
13
13
 
14
14
  export {
@@ -16,6 +16,7 @@ export {
16
16
  TextComponent as Text,
17
17
  RestyleableComponent,
18
18
  createRestyleComponentCommand,
19
+ isRestylableComponent,
19
20
 
20
21
  TextComponent,
21
22
  Stroke as StrokeComponent,
package/src/lib.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The main entrypoint for the NPM package. Everything exported by this file
3
- * is available through the `js-draw` package.
3
+ * is available through the [`js-draw` package](https://www.npmjs.com/package/js-draw).
4
4
  *
5
5
  * @example
6
6
  * ```
@@ -28,6 +28,7 @@ export * from './commands/lib';
28
28
  export * from './tools/lib';
29
29
  export * from './toolbar/lib';
30
30
  export * from './rendering/lib';
31
+ export * from './testing/lib';
31
32
  export { default as Pointer, PointerDevice } from './Pointer';
32
33
  export { default as HTMLToolbar } from './toolbar/HTMLToolbar';
33
34
  export { default as UndoRedoHistory } from './UndoRedoHistory';
@@ -1,4 +1,6 @@
1
1
 
2
2
  export { default as AbstractRenderer } from './renderers/AbstractRenderer';
3
3
  export { default as DummyRenderer } from './renderers/DummyRenderer';
4
+ export { default as SVGRenderer } from './renderers/SVGRenderer';
5
+ export { default as CanvasRenderer } from './renderers/CanvasRenderer';
4
6
  export { default as Display } from './Display';
@@ -10,6 +10,26 @@ import RenderingStyle from '../RenderingStyle';
10
10
  import TextStyle from '../TextRenderingStyle';
11
11
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
12
12
 
13
+ /**
14
+ * Renders onto a `CanvasRenderingContext2D`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const editor = new Editor(document.body);
19
+ *
20
+ * const canvas = document.createElement('canvas');
21
+ * const ctx = canvas.getContext('2d');
22
+ *
23
+ * // Ensure that the canvas can fit the entire rendering
24
+ * const viewport = editor.image.getImportExportViewport();
25
+ * canvas.width = viewport.getScreenRectSize().x;
26
+ * canvas.height = viewport.getScreenRectSize().y;
27
+ *
28
+ * // Render editor.image onto the renderer
29
+ * const renderer = new CanvasRenderer(ctx, viewport);
30
+ * editor.image.render(renderer, viewport);
31
+ * ```
32
+ */
13
33
  export default class CanvasRenderer extends AbstractRenderer {
14
34
  private ignoreObjectsAboveLevel: number|null = null;
15
35
  private ignoringObject: boolean = false;
@@ -26,6 +46,11 @@ export default class CanvasRenderer extends AbstractRenderer {
26
46
  private minRenderSizeAnyDimen: number;
27
47
  private minRenderSizeBothDimens: number;
28
48
 
49
+ /**
50
+ * Creates a new `CanvasRenderer` that renders to the given rendering context.
51
+ * The `viewport` is used to determine the translation/rotation/scaling of the content
52
+ * to draw.
53
+ */
29
54
  public constructor(private ctx: CanvasRenderingContext2D, viewport: Viewport) {
30
55
  super(viewport);
31
56
  this.setDraftMode(false);
@@ -231,6 +256,7 @@ export default class CanvasRenderer extends AbstractRenderer {
231
256
  }
232
257
  }
233
258
 
259
+ // @internal
234
260
  public drawPoints(...points: Point2[]) {
235
261
  const pointRadius = 10;
236
262
 
@@ -255,6 +281,7 @@ export default class CanvasRenderer extends AbstractRenderer {
255
281
  }
256
282
  }
257
283
 
284
+ // @internal
258
285
  public isTooSmallToRender(rect: Rect2): boolean {
259
286
  // Should we ignore all objects within this object's bbox?
260
287
  const diagonal = this.getCanvasToScreenTransform().transformVec3(rect.size);
@@ -14,6 +14,12 @@ import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './Abstrac
14
14
  export const renderedStylesheetId = 'js-draw-style-sheet';
15
15
 
16
16
  const svgNameSpace = 'http://www.w3.org/2000/svg';
17
+
18
+ /**
19
+ * Renders onto an `SVGElement`.
20
+ *
21
+ * @see {@link Editor.toSVG}
22
+ */
17
23
  export default class SVGRenderer extends AbstractRenderer {
18
24
  private lastPathStyle: RenderingStyle|null = null;
19
25
  private lastPathString: string[] = [];
@@ -21,7 +27,12 @@ export default class SVGRenderer extends AbstractRenderer {
21
27
 
22
28
  private overwrittenAttrs: Record<string, string|null> = {};
23
29
 
24
- // Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
30
+ /**
31
+ * Creates a renderer that renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
32
+ *
33
+ * `viewport` is used to determine the translation/rotation/scaling/output size of the rendered
34
+ * data.
35
+ */
25
36
  public constructor(private elem: SVGSVGElement, viewport: Viewport, private sanitize: boolean = false) {
26
37
  super(viewport);
27
38
  this.clear();
@@ -320,4 +331,24 @@ export default class SVGRenderer extends AbstractRenderer {
320
331
  public isTooSmallToRender(_rect: Rect2): boolean {
321
332
  return false;
322
333
  }
334
+
335
+ // Creates a new SVG element and SVGRenerer with attributes set for the given Viewport.
336
+ public static fromViewport(viewport: Viewport, sanitize: boolean = true) {
337
+ const svgNameSpace = 'http://www.w3.org/2000/svg';
338
+ const result = document.createElementNS(svgNameSpace, 'svg');
339
+
340
+ const rect = viewport.getScreenRectSize();
341
+ // rect.x -> size of rect in x direction, rect.y -> size of rect in y direction.
342
+ result.setAttribute('viewBox', [0, 0, rect.x, rect.y].map(part => toRoundedString(part)).join(' '));
343
+ result.setAttribute('width', toRoundedString(rect.x));
344
+ result.setAttribute('height', toRoundedString(rect.y));
345
+
346
+ // Ensure the image can be identified as an SVG if downloaded.
347
+ // See https://jwatt.org/svg/authoring/
348
+ result.setAttribute('version', '1.1');
349
+ result.setAttribute('baseProfile', 'full');
350
+ result.setAttribute('xmlns', svgNameSpace);
351
+
352
+ return { element: result, renderer: new SVGRenderer(result, viewport, sanitize) };
353
+ }
323
354
  }
@@ -0,0 +1,3 @@
1
+
2
+ export { default as sendPenEvent } from './sendPenEvent';
3
+ export { default as sendTouchEvent } from './sendTouchEvent';
@@ -0,0 +1,31 @@
1
+ import Editor from '../Editor';
2
+ import { Point2 } from '../math/Vec2';
3
+ import Pointer from '../Pointer';
4
+ import { InputEvtType } from '../types';
5
+
6
+ /**
7
+ * Dispatch a pen event to the currently selected tool.
8
+ * Intended for unit tests.
9
+ *
10
+ * @see {@link sendTouchEvent}
11
+ */
12
+ const sendPenEvent = (
13
+ editor: Editor,
14
+ eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
15
+ point: Point2,
16
+
17
+ allPointers?: Pointer[]
18
+ ) => {
19
+ const mainPointer = Pointer.ofCanvasPoint(
20
+ point, eventType !== InputEvtType.PointerUpEvt, editor.viewport
21
+ );
22
+
23
+ editor.toolController.dispatchInputEvent({
24
+ kind: eventType,
25
+ allPointers: allPointers ?? [
26
+ mainPointer,
27
+ ],
28
+ current: mainPointer,
29
+ });
30
+ };
31
+ export default sendPenEvent;