js-draw 0.15.1 → 0.16.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 (117) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +56 -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 +11 -2
  7. package/dist/src/Editor.js +66 -33
  8. package/dist/src/EditorImage.d.ts +28 -3
  9. package/dist/src/EditorImage.js +109 -18
  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 +8 -3
  14. package/dist/src/Viewport.js +15 -8
  15. package/dist/src/components/AbstractComponent.d.ts +6 -1
  16. package/dist/src/components/AbstractComponent.js +15 -2
  17. package/dist/src/components/ImageBackground.d.ts +42 -0
  18. package/dist/src/components/ImageBackground.js +139 -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/localizations/es.js +1 -1
  30. package/dist/src/math/Mat33.js +43 -5
  31. package/dist/src/math/Path.d.ts +5 -0
  32. package/dist/src/math/Path.js +80 -28
  33. package/dist/src/math/Vec3.js +1 -1
  34. package/dist/src/rendering/Display.js +1 -1
  35. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
  36. package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
  37. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  38. package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
  39. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  40. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  41. package/dist/src/testing/sendTouchEvent.d.ts +6 -0
  42. package/dist/src/testing/sendTouchEvent.js +26 -0
  43. package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
  44. package/dist/src/toolbar/HTMLToolbar.js +127 -15
  45. package/dist/src/toolbar/IconProvider.d.ts +2 -0
  46. package/dist/src/toolbar/IconProvider.js +45 -2
  47. package/dist/src/toolbar/localization.d.ts +5 -0
  48. package/dist/src/toolbar/localization.js +5 -0
  49. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
  50. package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
  51. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
  52. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
  53. package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
  54. package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
  55. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
  56. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
  57. package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
  58. package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
  59. package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
  60. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  61. package/dist/src/toolbar/widgets/lib.js +1 -0
  62. package/dist/src/tools/Eraser.js +5 -2
  63. package/dist/src/tools/PanZoom.js +12 -0
  64. package/dist/src/tools/PasteHandler.js +2 -2
  65. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
  66. package/dist/src/tools/SelectionTool/Selection.js +3 -2
  67. package/dist/src/tools/SelectionTool/SelectionTool.js +5 -1
  68. package/package.json +1 -1
  69. package/src/Color4.test.ts +6 -0
  70. package/src/Color4.ts +6 -1
  71. package/src/Editor.loadFrom.test.ts +24 -0
  72. package/src/Editor.ts +73 -39
  73. package/src/EditorImage.ts +136 -21
  74. package/src/EventDispatcher.ts +4 -1
  75. package/src/SVGLoader.ts +12 -1
  76. package/src/Viewport.ts +17 -7
  77. package/src/components/AbstractComponent.ts +17 -1
  78. package/src/components/ImageBackground.test.ts +35 -0
  79. package/src/components/ImageBackground.ts +176 -0
  80. package/src/components/ImageComponent.ts +2 -0
  81. package/src/components/builders/ArrowBuilder.ts +44 -41
  82. package/src/components/builders/LineBuilder.ts +26 -28
  83. package/src/components/builders/RectangleBuilder.ts +1 -1
  84. package/src/components/lib.ts +2 -0
  85. package/src/components/localization.ts +4 -0
  86. package/src/localizations/es.ts +8 -0
  87. package/src/math/Mat33.test.ts +47 -3
  88. package/src/math/Mat33.ts +47 -5
  89. package/src/math/Path.ts +87 -28
  90. package/src/math/Vec3.test.ts +4 -0
  91. package/src/math/Vec3.ts +1 -1
  92. package/src/rendering/Display.ts +1 -1
  93. package/src/rendering/renderers/AbstractRenderer.ts +20 -3
  94. package/src/rendering/renderers/CanvasRenderer.ts +17 -4
  95. package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
  96. package/src/rendering/renderers/SVGRenderer.ts +8 -1
  97. package/src/testing/sendTouchEvent.ts +43 -0
  98. package/src/toolbar/HTMLToolbar.ts +164 -16
  99. package/src/toolbar/IconProvider.ts +47 -2
  100. package/src/toolbar/localization.ts +10 -0
  101. package/src/toolbar/toolbar.css +2 -0
  102. package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
  103. package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
  104. package/src/toolbar/widgets/BaseWidget.ts +34 -2
  105. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
  106. package/src/toolbar/widgets/HandToolWidget.ts +1 -1
  107. package/src/toolbar/widgets/OverflowWidget.css +9 -0
  108. package/src/toolbar/widgets/OverflowWidget.ts +83 -0
  109. package/src/toolbar/widgets/lib.ts +2 -1
  110. package/src/tools/Eraser.test.ts +24 -1
  111. package/src/tools/Eraser.ts +6 -2
  112. package/src/tools/PanZoom.test.ts +267 -23
  113. package/src/tools/PanZoom.ts +15 -1
  114. package/src/tools/PasteHandler.ts +3 -2
  115. package/src/tools/SelectionTool/Selection.ts +3 -2
  116. package/src/tools/SelectionTool/SelectionTool.ts +6 -1
  117. package/src/types.ts +1 -0
@@ -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');
@@ -608,4 +607,48 @@ export default class IconProvider {
608
607
  svg.setAttribute('viewBox', '0 0 100 100');
609
608
  return svg;
610
609
  }
610
+ makeConfigureDocumentIcon() {
611
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
612
+ svg.innerHTML = `
613
+ <path
614
+ d='
615
+ M 5,5 V 95 H 95 V 5 Z m 5,5 H 90 V 90 H 10 Z
616
+ m 5,10 V 30 H 50 V 25 H 20 v -5 z
617
+ m 40,0 V 50 H 85 V 20 Z
618
+ m 2,2 H 83 V 39 L 77,28 70,42 64,35 57,45 Z
619
+ m 8.5,5 C 64.67,27 64,27.67 64,28.5 64,29.33 64.67,30 65.5,30 66.33,30 67,29.33 67,28.5 67,27.67 66.33,27 65.5,27 Z
620
+ M 15,40 v 5 h 35 v -5 z
621
+ m 0,15 v 5 h 70 v -5 z
622
+ m 0,15 v 5 h 70 v -5 z
623
+ '
624
+ style='fill: var(--icon-color);'
625
+ />
626
+ `;
627
+ svg.setAttribute('viewBox', '0 0 100 100');
628
+ return svg;
629
+ }
630
+ makeOverflowIcon() {
631
+ return this.makeIconFromPath(`
632
+ M 15 40
633
+ A 12.5 12.5 0 0 0 2.5 52.5
634
+ A 12.5 12.5 0 0 0 15 65
635
+ A 12.5 12.5 0 0 0 27.5 52.5
636
+ A 12.5 12.5 0 0 0 15 40
637
+ z
638
+
639
+ M 50 40
640
+ A 12.5 12.5 0 0 0 37.5 52.5
641
+ A 12.5 12.5 0 0 0 50 65
642
+ A 12.5 12.5 0 0 0 62.5 52.5
643
+ A 12.5 12.5 0 0 0 50 40
644
+ z
645
+
646
+ M 85 40
647
+ A 12.5 12.5 0 0 0 72.5 52.5
648
+ A 12.5 12.5 0 0 0 85 65
649
+ A 12.5 12.5 0 0 0 97.5 52.5
650
+ A 12.5 12.5 0 0 0 85 40
651
+ z
652
+ `);
653
+ }
611
654
  }
@@ -33,6 +33,11 @@ export interface ToolbarLocalization {
33
33
  resetView: string;
34
34
  selectionToolKeyboardShortcuts: string;
35
35
  paste: string;
36
+ documentProperties: string;
37
+ backgroundColor: string;
38
+ imageWidthOption: string;
39
+ imageHeightOption: string;
40
+ toggleOverflow: string;
36
41
  errorImageHasZeroSize: string;
37
42
  dropdownShown: (toolName: string) => string;
38
43
  dropdownHidden: (toolName: string) => string;
@@ -24,6 +24,11 @@ export const defaultToolbarLocalization = {
24
24
  pickColorFromScreen: 'Pick color from screen',
25
25
  clickToPickColorAnnouncement: 'Click on the screen to pick a color',
26
26
  selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
27
+ documentProperties: 'Document',
28
+ backgroundColor: 'Background Color: ',
29
+ imageWidthOption: 'Width: ',
30
+ imageHeightOption: 'Height: ',
31
+ toggleOverflow: 'More',
27
32
  touchPanning: 'Touchscreen panning',
28
33
  freehandPen: 'Freehand',
29
34
  pressureSensitiveFreehandPen: 'Freehand (pressure sensitive)',
@@ -5,9 +5,11 @@ export default class ActionButtonWidget extends BaseWidget {
5
5
  protected makeIcon: () => Element | null;
6
6
  protected title: string;
7
7
  protected clickAction: () => void;
8
- constructor(editor: Editor, id: string, makeIcon: () => Element | null, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization);
8
+ protected mustBeToplevel: boolean;
9
+ constructor(editor: Editor, id: string, makeIcon: () => Element | null, title: string, clickAction: () => void, localizationTable?: ToolbarLocalization, mustBeToplevel?: boolean);
9
10
  protected handleClick(): void;
10
11
  protected getTitle(): string;
11
12
  protected createIcon(): Element | null;
12
13
  protected fillDropdown(_dropdown: HTMLElement): boolean;
14
+ canBeInOverflowMenu(): boolean;
13
15
  }
@@ -1,10 +1,11 @@
1
1
  import BaseWidget from './BaseWidget';
2
2
  export default class ActionButtonWidget extends BaseWidget {
3
- constructor(editor, id, makeIcon, title, clickAction, localizationTable) {
3
+ constructor(editor, id, makeIcon, title, clickAction, localizationTable, mustBeToplevel = false) {
4
4
  super(editor, id, localizationTable);
5
5
  this.makeIcon = makeIcon;
6
6
  this.title = title;
7
7
  this.clickAction = clickAction;
8
+ this.mustBeToplevel = mustBeToplevel;
8
9
  }
9
10
  handleClick() {
10
11
  this.clickAction();
@@ -18,4 +19,7 @@ export default class ActionButtonWidget extends BaseWidget {
18
19
  fillDropdown(_dropdown) {
19
20
  return false;
20
21
  }
22
+ canBeInOverflowMenu() {
23
+ return !this.mustBeToplevel;
24
+ }
21
25
  }
@@ -7,5 +7,5 @@ export default abstract class BaseToolWidget extends BaseWidget {
7
7
  protected targetTool: BaseTool;
8
8
  constructor(editor: Editor, targetTool: BaseTool, id: string, localizationTable?: ToolbarLocalization);
9
9
  protected handleClick(): void;
10
- addTo(parent: HTMLElement): void;
10
+ addTo(parent: HTMLElement): HTMLElement;
11
11
  }
@@ -37,7 +37,8 @@ export default class BaseToolWidget extends BaseWidget {
37
37
  }
38
38
  }
39
39
  addTo(parent) {
40
- super.addTo(parent);
40
+ const result = super.addTo(parent);
41
41
  this.setSelected(this.targetTool.isEnabled());
42
+ return result;
42
43
  }
43
44
  }
@@ -36,14 +36,19 @@ export default abstract class BaseWidget {
36
36
  protected abstract handleClick(): void;
37
37
  protected get hasDropdown(): boolean;
38
38
  protected addSubWidget(widget: BaseWidget): void;
39
- addTo(parent: HTMLElement): void;
39
+ private toolbarWidgetToggleListener;
40
+ addTo(parent: HTMLElement): HTMLElement;
40
41
  protected updateIcon(): void;
41
42
  setDisabled(disabled: boolean): void;
42
43
  setSelected(selected: boolean): void;
43
44
  protected setDropdownVisible(visible: boolean): void;
45
+ canBeInOverflowMenu(): boolean;
46
+ getButtonWidth(): number;
47
+ isHidden(): boolean;
48
+ setHidden(hidden: boolean): void;
44
49
  protected repositionDropdown(): void;
45
50
  /** Set whether the widget is contained within another. @internal */
46
- protected setIsToplevel(toplevel: boolean): void;
51
+ setIsToplevel(toplevel: boolean): void;
47
52
  protected isDropdownVisible(): boolean;
48
53
  protected isSelected(): boolean;
49
54
  private createDropdownIcon;
@@ -22,6 +22,7 @@ export default class BaseWidget {
22
22
  // Maps subWidget IDs to subWidgets.
23
23
  this.subWidgets = {};
24
24
  this.toplevel = true;
25
+ this.toolbarWidgetToggleListener = null;
25
26
  this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
26
27
  this.icon = null;
27
28
  this.container = document.createElement('div');
@@ -135,12 +136,14 @@ export default class BaseWidget {
135
136
  this.subWidgets[id] = widget;
136
137
  }
137
138
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
139
+ // Returns the element that was just added to `parent`.
138
140
  // @internal
139
141
  addTo(parent) {
140
142
  this.label.innerText = this.getTitle();
141
143
  this.setupActionBtnClickListener(this.button);
142
144
  this.icon = null;
143
145
  this.updateIcon();
146
+ this.container.replaceChildren();
144
147
  this.button.replaceChildren(this.icon, this.label);
145
148
  this.container.appendChild(this.button);
146
149
  __classPrivateFieldSet(this, _BaseWidget_hasDropdown, this.fillDropdown(this.dropdownContainer), "f");
@@ -148,7 +151,10 @@ export default class BaseWidget {
148
151
  this.dropdownIcon = this.createDropdownIcon();
149
152
  this.button.appendChild(this.dropdownIcon);
150
153
  this.container.appendChild(this.dropdownContainer);
151
- this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
154
+ if (this.toolbarWidgetToggleListener) {
155
+ this.toolbarWidgetToggleListener.remove();
156
+ }
157
+ this.toolbarWidgetToggleListener = this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
152
158
  if (evt.kind === EditorEventType.ToolbarDropdownShown
153
159
  && evt.parentWidget !== this
154
160
  // Don't hide if a submenu wash shown (it might be a submenu of
@@ -159,7 +165,11 @@ export default class BaseWidget {
159
165
  });
160
166
  }
161
167
  this.setDropdownVisible(false);
168
+ if (this.container.parentElement) {
169
+ this.container.remove();
170
+ }
162
171
  parent.appendChild(this.container);
172
+ return this.container;
163
173
  }
164
174
  updateIcon() {
165
175
  var _a, _b;
@@ -219,6 +229,18 @@ export default class BaseWidget {
219
229
  }
220
230
  this.repositionDropdown();
221
231
  }
232
+ canBeInOverflowMenu() {
233
+ return true;
234
+ }
235
+ getButtonWidth() {
236
+ return this.button.clientWidth;
237
+ }
238
+ isHidden() {
239
+ return this.container.style.display === 'none';
240
+ }
241
+ setHidden(hidden) {
242
+ this.container.style.display = hidden ? 'none' : '';
243
+ }
222
244
  repositionDropdown() {
223
245
  const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
224
246
  const screenWidth = document.body.clientWidth;
@@ -0,0 +1,19 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import BaseWidget from './BaseWidget';
4
+ export default class DocumentPropertiesWidget extends BaseWidget {
5
+ private updateDropdownContent;
6
+ constructor(editor: Editor, localizationTable?: ToolbarLocalization);
7
+ protected getTitle(): string;
8
+ protected createIcon(): Element;
9
+ protected handleClick(): void;
10
+ private dropdownUpdateQueued;
11
+ private queueDropdownUpdate;
12
+ private updateDropdown;
13
+ private getBackgroundElem;
14
+ private setBackgroundColor;
15
+ private getBackgroundColor;
16
+ private updateImportExportRectSize;
17
+ private static idCounter;
18
+ protected fillDropdown(dropdown: HTMLElement): boolean;
19
+ }
@@ -0,0 +1,135 @@
1
+ import Color4 from '../../Color4';
2
+ import ImageBackground from '../../components/ImageBackground';
3
+ import { EditorImageEventType } from '../../EditorImage';
4
+ import Rect2 from '../../math/Rect2';
5
+ import { EditorEventType } from '../../types';
6
+ import makeColorInput from '../makeColorInput';
7
+ import BaseWidget from './BaseWidget';
8
+ export default class DocumentPropertiesWidget extends BaseWidget {
9
+ constructor(editor, localizationTable) {
10
+ super(editor, 'zoom-widget', localizationTable);
11
+ this.updateDropdownContent = () => { };
12
+ this.dropdownUpdateQueued = false;
13
+ // Make it possible to open the dropdown, even if this widget isn't selected.
14
+ this.container.classList.add('dropdownShowable');
15
+ this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, () => {
16
+ this.queueDropdownUpdate();
17
+ });
18
+ this.editor.image.notifier.on(EditorImageEventType.ExportViewportChanged, () => {
19
+ this.queueDropdownUpdate();
20
+ });
21
+ }
22
+ getTitle() {
23
+ return this.localizationTable.documentProperties;
24
+ }
25
+ createIcon() {
26
+ return this.editor.icons.makeConfigureDocumentIcon();
27
+ }
28
+ handleClick() {
29
+ this.setDropdownVisible(!this.isDropdownVisible());
30
+ this.queueDropdownUpdate();
31
+ }
32
+ queueDropdownUpdate() {
33
+ if (!this.dropdownUpdateQueued) {
34
+ requestAnimationFrame(() => this.updateDropdown());
35
+ this.dropdownUpdateQueued = true;
36
+ }
37
+ }
38
+ updateDropdown() {
39
+ this.dropdownUpdateQueued = false;
40
+ if (this.isDropdownVisible()) {
41
+ this.updateDropdownContent();
42
+ }
43
+ }
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
+ setBackgroundColor(color) {
58
+ this.editor.dispatch(this.editor.setBackgroundColor(color));
59
+ }
60
+ 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;
64
+ }
65
+ updateImportExportRectSize(size) {
66
+ const filterDimension = (dim) => {
67
+ if (dim !== undefined && (!isFinite(dim) || dim <= 0)) {
68
+ dim = 100;
69
+ }
70
+ return dim;
71
+ };
72
+ const width = filterDimension(size.width);
73
+ const height = filterDimension(size.height);
74
+ const currentRect = this.editor.getImportExportRect();
75
+ const newRect = new Rect2(currentRect.x, currentRect.y, width !== null && width !== void 0 ? width : currentRect.w, height !== null && height !== void 0 ? height : currentRect.h);
76
+ this.editor.dispatch(this.editor.image.setImportExportRect(newRect));
77
+ this.editor.queueRerender();
78
+ }
79
+ fillDropdown(dropdown) {
80
+ const container = document.createElement('div');
81
+ const backgroundColorRow = document.createElement('div');
82
+ const backgroundColorLabel = document.createElement('label');
83
+ backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
84
+ const [colorInput, backgroundColorInputContainer, setBgColorInputValue] = makeColorInput(this.editor, color => {
85
+ if (!color.eq(this.getBackgroundColor())) {
86
+ this.setBackgroundColor(color);
87
+ }
88
+ });
89
+ colorInput.id = `document-properties-color-input-${DocumentPropertiesWidget.idCounter++}`;
90
+ backgroundColorLabel.htmlFor = colorInput.id;
91
+ backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
92
+ const addDimensionRow = (labelContent, onChange) => {
93
+ const row = document.createElement('div');
94
+ const label = document.createElement('label');
95
+ const spacer = document.createElement('span');
96
+ const input = document.createElement('input');
97
+ label.innerText = labelContent;
98
+ input.type = 'number';
99
+ input.min = '0';
100
+ input.id = `document-properties-dimension-row-${DocumentPropertiesWidget.idCounter++}`;
101
+ label.htmlFor = input.id;
102
+ spacer.style.flexGrow = '1';
103
+ input.style.flexGrow = '2';
104
+ input.style.width = '25px';
105
+ row.style.display = 'flex';
106
+ input.oninput = () => {
107
+ onChange(parseFloat(input.value));
108
+ };
109
+ row.replaceChildren(label, spacer, input);
110
+ return {
111
+ setValue: (value) => {
112
+ input.value = value.toString();
113
+ },
114
+ element: row,
115
+ };
116
+ };
117
+ const imageWidthRow = addDimensionRow(this.localizationTable.imageWidthOption, (value) => {
118
+ this.updateImportExportRectSize({ width: value });
119
+ });
120
+ const imageHeightRow = addDimensionRow(this.localizationTable.imageHeightOption, (value) => {
121
+ this.updateImportExportRectSize({ height: value });
122
+ });
123
+ this.updateDropdownContent = () => {
124
+ setBgColorInputValue(this.getBackgroundColor());
125
+ const importExportRect = this.editor.getImportExportRect();
126
+ imageWidthRow.setValue(importExportRect.width);
127
+ imageHeightRow.setValue(importExportRect.height);
128
+ };
129
+ this.updateDropdownContent();
130
+ container.replaceChildren(backgroundColorRow, imageWidthRow.element, imageHeightRow.element);
131
+ dropdown.replaceChildren(container);
132
+ return true;
133
+ }
134
+ }
135
+ DocumentPropertiesWidget.idCounter = 0;
@@ -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
  }
@@ -0,0 +1,25 @@
1
+ import Editor from '../../Editor';
2
+ import { ToolbarLocalization } from '../localization';
3
+ import BaseWidget from './BaseWidget';
4
+ export default class OverflowWidget extends BaseWidget {
5
+ private overflowChildren;
6
+ private overflowContainer;
7
+ constructor(editor: Editor, localizationTable?: ToolbarLocalization);
8
+ protected getTitle(): string;
9
+ protected createIcon(): Element | null;
10
+ protected handleClick(): void;
11
+ protected fillDropdown(dropdown: HTMLElement): boolean;
12
+ /**
13
+ * Removes all `BaseWidget`s from this and returns them.
14
+ */
15
+ clearChildren(): BaseWidget[];
16
+ getChildWidgets(): BaseWidget[];
17
+ hasAsChild(widget: BaseWidget): boolean;
18
+ /**
19
+ * Adds `widget` to this.
20
+ * `widget`'s previous parent is still responsible
21
+ * for serializing/deserializing its state.
22
+ */
23
+ addToOverflow(widget: BaseWidget): void;
24
+ canBeInOverflowMenu(): boolean;
25
+ }
@@ -0,0 +1,65 @@
1
+ import BaseWidget from './BaseWidget';
2
+ export default class OverflowWidget extends BaseWidget {
3
+ constructor(editor, localizationTable) {
4
+ var _a;
5
+ super(editor, 'overflow-widget', localizationTable);
6
+ this.overflowChildren = [];
7
+ // Make the dropdown openable
8
+ this.container.classList.add('dropdownShowable');
9
+ (_a = this.overflowContainer) !== null && _a !== void 0 ? _a : (this.overflowContainer = document.createElement('div'));
10
+ }
11
+ getTitle() {
12
+ return this.localizationTable.toggleOverflow;
13
+ }
14
+ createIcon() {
15
+ return this.editor.icons.makeOverflowIcon();
16
+ }
17
+ handleClick() {
18
+ this.setDropdownVisible(!this.isDropdownVisible());
19
+ }
20
+ fillDropdown(dropdown) {
21
+ var _a;
22
+ (_a = this.overflowContainer) !== null && _a !== void 0 ? _a : (this.overflowContainer = document.createElement('div'));
23
+ if (this.overflowContainer.parentElement) {
24
+ this.overflowContainer.remove();
25
+ }
26
+ this.overflowContainer.classList.add('toolbar-overflow-widget-overflow-list');
27
+ dropdown.appendChild(this.overflowContainer);
28
+ return true;
29
+ }
30
+ /**
31
+ * Removes all `BaseWidget`s from this and returns them.
32
+ */
33
+ clearChildren() {
34
+ this.overflowContainer.replaceChildren();
35
+ const overflowChildren = this.overflowChildren;
36
+ this.overflowChildren = [];
37
+ return overflowChildren;
38
+ }
39
+ getChildWidgets() {
40
+ return [...this.overflowChildren];
41
+ }
42
+ hasAsChild(widget) {
43
+ for (const otherWidget of this.overflowChildren) {
44
+ if (widget === otherWidget) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Adds `widget` to this.
52
+ * `widget`'s previous parent is still responsible
53
+ * for serializing/deserializing its state.
54
+ */
55
+ addToOverflow(widget) {
56
+ this.overflowChildren.push(widget);
57
+ widget.addTo(this.overflowContainer);
58
+ widget.setIsToplevel(false);
59
+ }
60
+ // This always returns false.
61
+ // Don't try to move the overflow menu to itself.
62
+ canBeInOverflowMenu() {
63
+ return false;
64
+ }
65
+ }
@@ -7,3 +7,4 @@ export { default as HandToolWidget } from './HandToolWidget';
7
7
  export { default as SelectionToolWidget } from './SelectionToolWidget';
8
8
  export { default as EraserToolWidget } from './EraserToolWidget';
9
9
  export { default as InsertImageWidget } from './InsertImageWidget';
10
+ export { default as DocumentPropertiesWidget } from './DocumentPropertiesWidget';
@@ -7,3 +7,4 @@ export { default as HandToolWidget } from './HandToolWidget';
7
7
  export { default as SelectionToolWidget } from './SelectionToolWidget';
8
8
  export { default as EraserToolWidget } from './EraserToolWidget';
9
9
  export { default as InsertImageWidget } from './InsertImageWidget';
10
+ export { default as DocumentPropertiesWidget } from './DocumentPropertiesWidget';
@@ -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
  }
@@ -7,9 +7,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { TextComponent } from '../components/lib';
10
+ import TextComponent from '../components/TextComponent';
11
11
  import SVGLoader from '../SVGLoader';
12
- import { Mat33 } from '../math/lib';
12
+ import Mat33 from '../math/Mat33';
13
13
  import BaseTool from './BaseTool';
14
14
  import TextTool from './TextTool';
15
15
  import Color4 from '../Color4';
@@ -3,7 +3,8 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  import Editor from '../../Editor';
6
- import { Mat33, Rect2 } from '../../math/lib';
6
+ import Mat33 from '../../math/Mat33';
7
+ import Rect2 from '../../math/Rect2';
7
8
  import { Point2 } from '../../math/Vec2';
8
9
  import Pointer from '../../Pointer';
9
10
  import AbstractComponent from '../../components/AbstractComponent';
@@ -13,7 +13,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
13
13
  };
14
14
  var _a;
15
15
  import SerializableCommand from '../../commands/SerializableCommand';
16
- import { Mat33, Rect2 } from '../../math/lib';
16
+ import Mat33 from '../../math/Mat33';
17
+ import Rect2 from '../../math/Rect2';
17
18
  import { Vec2 } from '../../math/Vec2';
18
19
  import SelectionHandle, { HandleShape, handleSize } from './SelectionHandle';
19
20
  import { cssPrefix } from './SelectionTool';
@@ -157,7 +158,7 @@ export default class Selection {
157
158
  singleItemSelectionMode = true;
158
159
  }
159
160
  this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
160
- return elem.intersectsRect(this.region);
161
+ return elem.intersectsRect(this.region) && elem.isSelectable();
161
162
  });
162
163
  if (singleItemSelectionMode && this.selectedElems.length > 0) {
163
164
  this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
@@ -284,6 +284,10 @@ export default class SelectionTool extends BaseTool {
284
284
  });
285
285
  return true;
286
286
  }
287
+ if (evt.key === 'a') {
288
+ // Selected all in onKeyDown. Don't finalizeTransform.
289
+ return true;
290
+ }
287
291
  }
288
292
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
289
293
  this.selectionBox.finalizeTransform();
@@ -300,7 +304,7 @@ export default class SelectionTool extends BaseTool {
300
304
  if (selectedElems.length === 0) {
301
305
  return false;
302
306
  }
303
- const exportViewport = new Viewport(this.editor.notifier);
307
+ const exportViewport = new Viewport(() => { });
304
308
  exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
305
309
  exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
306
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.1",
3
+ "version": "0.16.0",
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