js-draw 0.0.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 (156) hide show
  1. package/.eslintrc.js +57 -0
  2. package/.husky/pre-commit +4 -0
  3. package/LICENSE +21 -0
  4. package/README.md +74 -0
  5. package/__mocks__/coloris.ts +8 -0
  6. package/__mocks__/styleMock.js +1 -0
  7. package/dist/__mocks__/coloris.d.ts +2 -0
  8. package/dist/__mocks__/coloris.js +5 -0
  9. package/dist/build_tools/BundledFile.d.ts +12 -0
  10. package/dist/build_tools/BundledFile.js +153 -0
  11. package/dist/scripts/bundle.d.ts +1 -0
  12. package/dist/scripts/bundle.js +19 -0
  13. package/dist/scripts/watchBundle.d.ts +1 -0
  14. package/dist/scripts/watchBundle.js +9 -0
  15. package/dist/src/Color4.d.ts +23 -0
  16. package/dist/src/Color4.js +102 -0
  17. package/dist/src/Display.d.ts +22 -0
  18. package/dist/src/Display.js +93 -0
  19. package/dist/src/Editor.d.ts +55 -0
  20. package/dist/src/Editor.js +366 -0
  21. package/dist/src/EditorImage.d.ts +44 -0
  22. package/dist/src/EditorImage.js +243 -0
  23. package/dist/src/EventDispatcher.d.ts +11 -0
  24. package/dist/src/EventDispatcher.js +39 -0
  25. package/dist/src/Pointer.d.ts +22 -0
  26. package/dist/src/Pointer.js +57 -0
  27. package/dist/src/SVGLoader.d.ts +21 -0
  28. package/dist/src/SVGLoader.js +204 -0
  29. package/dist/src/StrokeBuilder.d.ts +35 -0
  30. package/dist/src/StrokeBuilder.js +275 -0
  31. package/dist/src/UndoRedoHistory.d.ts +17 -0
  32. package/dist/src/UndoRedoHistory.js +46 -0
  33. package/dist/src/Viewport.d.ts +39 -0
  34. package/dist/src/Viewport.js +134 -0
  35. package/dist/src/commands/Command.d.ts +15 -0
  36. package/dist/src/commands/Command.js +29 -0
  37. package/dist/src/commands/Erase.d.ts +11 -0
  38. package/dist/src/commands/Erase.js +37 -0
  39. package/dist/src/commands/localization.d.ts +19 -0
  40. package/dist/src/commands/localization.js +17 -0
  41. package/dist/src/components/AbstractComponent.d.ts +19 -0
  42. package/dist/src/components/AbstractComponent.js +46 -0
  43. package/dist/src/components/Stroke.d.ts +16 -0
  44. package/dist/src/components/Stroke.js +79 -0
  45. package/dist/src/components/UnknownSVGObject.d.ts +15 -0
  46. package/dist/src/components/UnknownSVGObject.js +25 -0
  47. package/dist/src/components/localization.d.ts +5 -0
  48. package/dist/src/components/localization.js +4 -0
  49. package/dist/src/geometry/LineSegment2.d.ts +19 -0
  50. package/dist/src/geometry/LineSegment2.js +100 -0
  51. package/dist/src/geometry/Mat33.d.ts +31 -0
  52. package/dist/src/geometry/Mat33.js +187 -0
  53. package/dist/src/geometry/Path.d.ts +55 -0
  54. package/dist/src/geometry/Path.js +364 -0
  55. package/dist/src/geometry/Rect2.d.ts +47 -0
  56. package/dist/src/geometry/Rect2.js +148 -0
  57. package/dist/src/geometry/Vec2.d.ts +13 -0
  58. package/dist/src/geometry/Vec2.js +13 -0
  59. package/dist/src/geometry/Vec3.d.ts +32 -0
  60. package/dist/src/geometry/Vec3.js +98 -0
  61. package/dist/src/localization.d.ts +12 -0
  62. package/dist/src/localization.js +5 -0
  63. package/dist/src/main.d.ts +3 -0
  64. package/dist/src/main.js +4 -0
  65. package/dist/src/rendering/AbstractRenderer.d.ts +38 -0
  66. package/dist/src/rendering/AbstractRenderer.js +108 -0
  67. package/dist/src/rendering/CanvasRenderer.d.ts +23 -0
  68. package/dist/src/rendering/CanvasRenderer.js +108 -0
  69. package/dist/src/rendering/DummyRenderer.d.ts +25 -0
  70. package/dist/src/rendering/DummyRenderer.js +65 -0
  71. package/dist/src/rendering/SVGRenderer.d.ts +27 -0
  72. package/dist/src/rendering/SVGRenderer.js +122 -0
  73. package/dist/src/testing/loadExpectExtensions.d.ts +17 -0
  74. package/dist/src/testing/loadExpectExtensions.js +27 -0
  75. package/dist/src/toolbar/HTMLToolbar.d.ts +12 -0
  76. package/dist/src/toolbar/HTMLToolbar.js +444 -0
  77. package/dist/src/toolbar/types.d.ts +17 -0
  78. package/dist/src/toolbar/types.js +5 -0
  79. package/dist/src/tools/BaseTool.d.ts +20 -0
  80. package/dist/src/tools/BaseTool.js +44 -0
  81. package/dist/src/tools/Eraser.d.ts +16 -0
  82. package/dist/src/tools/Eraser.js +53 -0
  83. package/dist/src/tools/PanZoom.d.ts +40 -0
  84. package/dist/src/tools/PanZoom.js +191 -0
  85. package/dist/src/tools/Pen.d.ts +25 -0
  86. package/dist/src/tools/Pen.js +97 -0
  87. package/dist/src/tools/SelectionTool.d.ts +49 -0
  88. package/dist/src/tools/SelectionTool.js +437 -0
  89. package/dist/src/tools/ToolController.d.ts +18 -0
  90. package/dist/src/tools/ToolController.js +110 -0
  91. package/dist/src/tools/ToolEnabledGroup.d.ts +6 -0
  92. package/dist/src/tools/ToolEnabledGroup.js +11 -0
  93. package/dist/src/tools/localization.d.ts +10 -0
  94. package/dist/src/tools/localization.js +9 -0
  95. package/dist/src/types.d.ts +88 -0
  96. package/dist/src/types.js +20 -0
  97. package/jest.config.js +22 -0
  98. package/lint-staged.config.js +6 -0
  99. package/package.json +82 -0
  100. package/src/Color4.test.ts +12 -0
  101. package/src/Color4.ts +122 -0
  102. package/src/Display.ts +118 -0
  103. package/src/Editor.css +58 -0
  104. package/src/Editor.ts +469 -0
  105. package/src/EditorImage.test.ts +90 -0
  106. package/src/EditorImage.ts +297 -0
  107. package/src/EventDispatcher.test.ts +123 -0
  108. package/src/EventDispatcher.ts +53 -0
  109. package/src/Pointer.ts +93 -0
  110. package/src/SVGLoader.ts +230 -0
  111. package/src/StrokeBuilder.ts +362 -0
  112. package/src/UndoRedoHistory.ts +61 -0
  113. package/src/Viewport.ts +168 -0
  114. package/src/commands/Command.ts +43 -0
  115. package/src/commands/Erase.ts +52 -0
  116. package/src/commands/localization.ts +38 -0
  117. package/src/components/AbstractComponent.ts +73 -0
  118. package/src/components/Stroke.test.ts +18 -0
  119. package/src/components/Stroke.ts +102 -0
  120. package/src/components/UnknownSVGObject.ts +36 -0
  121. package/src/components/localization.ts +9 -0
  122. package/src/editorStyles.js +3 -0
  123. package/src/geometry/LineSegment2.test.ts +77 -0
  124. package/src/geometry/LineSegment2.ts +127 -0
  125. package/src/geometry/Mat33.test.ts +144 -0
  126. package/src/geometry/Mat33.ts +268 -0
  127. package/src/geometry/Path.fromString.test.ts +146 -0
  128. package/src/geometry/Path.test.ts +96 -0
  129. package/src/geometry/Path.toString.test.ts +31 -0
  130. package/src/geometry/Path.ts +456 -0
  131. package/src/geometry/Rect2.test.ts +121 -0
  132. package/src/geometry/Rect2.ts +215 -0
  133. package/src/geometry/Vec2.test.ts +32 -0
  134. package/src/geometry/Vec2.ts +18 -0
  135. package/src/geometry/Vec3.test.ts +29 -0
  136. package/src/geometry/Vec3.ts +133 -0
  137. package/src/localization.ts +27 -0
  138. package/src/rendering/AbstractRenderer.ts +164 -0
  139. package/src/rendering/CanvasRenderer.ts +141 -0
  140. package/src/rendering/DummyRenderer.ts +80 -0
  141. package/src/rendering/SVGRenderer.ts +159 -0
  142. package/src/testing/loadExpectExtensions.ts +43 -0
  143. package/src/toolbar/HTMLToolbar.ts +551 -0
  144. package/src/toolbar/toolbar.css +110 -0
  145. package/src/toolbar/types.ts +20 -0
  146. package/src/tools/BaseTool.ts +58 -0
  147. package/src/tools/Eraser.ts +67 -0
  148. package/src/tools/PanZoom.ts +253 -0
  149. package/src/tools/Pen.ts +121 -0
  150. package/src/tools/SelectionTool.test.ts +85 -0
  151. package/src/tools/SelectionTool.ts +545 -0
  152. package/src/tools/ToolController.ts +126 -0
  153. package/src/tools/ToolEnabledGroup.ts +14 -0
  154. package/src/tools/localization.ts +22 -0
  155. package/src/types.ts +133 -0
  156. package/tsconfig.json +28 -0
@@ -0,0 +1,27 @@
1
+ export const loadExpectExtensions = () => {
2
+ // Custom matchers. See
3
+ // https://jestjs.io/docs/expect#expectextendmatchers
4
+ expect.extend({
5
+ // Determine whether expected = actual based on the objects'
6
+ // .eq methods
7
+ objEq(actual, expected, ...eqArgs) {
8
+ let pass = false;
9
+ if ((expected !== null && expected !== void 0 ? expected : null) === null) {
10
+ pass = actual.eq(expected, ...eqArgs);
11
+ }
12
+ else {
13
+ pass = expected.eq(actual, ...eqArgs);
14
+ }
15
+ return {
16
+ pass,
17
+ message: () => {
18
+ if (pass) {
19
+ return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
20
+ }
21
+ return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
22
+ },
23
+ };
24
+ },
25
+ });
26
+ };
27
+ export default loadExpectExtensions;
@@ -0,0 +1,12 @@
1
+ import Editor from '../Editor';
2
+ import { ToolbarLocalization } from './types';
3
+ export default class HTMLToolbar {
4
+ private editor;
5
+ private localizationTable;
6
+ private container;
7
+ static defaultLocalization: ToolbarLocalization;
8
+ constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
9
+ addActionButton(text: string, command: () => void, parent?: Element): HTMLButtonElement;
10
+ private addUndoRedoButtons;
11
+ private addElements;
12
+ }
@@ -0,0 +1,444 @@
1
+ import { ToolType } from '../tools/ToolController';
2
+ import { EditorEventType } from '../types';
3
+ import { coloris, init as colorisInit } from '@melloware/coloris';
4
+ import Color4 from '../Color4';
5
+ import Pen from '../tools/Pen';
6
+ import Eraser from '../tools/Eraser';
7
+ import SelectionTool from '../tools/SelectionTool';
8
+ const primaryForegroundFill = `
9
+ style='fill: var(--primary-foreground-color);'
10
+ `;
11
+ const primaryForegroundStrokeFill = `
12
+ style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
13
+ `;
14
+ const toolbarCSSPrefix = 'toolbar-';
15
+ class ToolbarWidget {
16
+ constructor(editor, targetTool, localizationTable) {
17
+ this.editor = editor;
18
+ this.targetTool = targetTool;
19
+ this.localizationTable = localizationTable;
20
+ this.icon = null;
21
+ this.container = document.createElement('div');
22
+ this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
23
+ this.dropdownContainer = document.createElement('div');
24
+ this.dropdownContainer.classList.add(`${toolbarCSSPrefix}dropdown`);
25
+ this.dropdownContainer.classList.add('hidden');
26
+ this.hasDropdown = false;
27
+ this.button = document.createElement('div');
28
+ this.button.classList.add(`${toolbarCSSPrefix}button`);
29
+ this.label = document.createElement('label');
30
+ this.button.setAttribute('role', 'button');
31
+ this.button.tabIndex = 0;
32
+ this.button.onclick = () => {
33
+ this.handleClick();
34
+ };
35
+ editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
36
+ if (toolEvt.kind !== EditorEventType.ToolEnabled) {
37
+ throw new Error('Incorrect event type! (Expected ToolEnabled)');
38
+ }
39
+ if (toolEvt.tool === targetTool) {
40
+ this.updateSelected(true);
41
+ }
42
+ });
43
+ editor.notifier.on(EditorEventType.ToolDisabled, toolEvt => {
44
+ if (toolEvt.kind !== EditorEventType.ToolDisabled) {
45
+ throw new Error('Incorrect event type! (Expected ToolDisabled)');
46
+ }
47
+ if (toolEvt.tool === targetTool) {
48
+ this.updateSelected(false);
49
+ this.setDropdownVisible(false);
50
+ }
51
+ });
52
+ }
53
+ handleClick() {
54
+ if (this.hasDropdown) {
55
+ if (!this.targetTool.isEnabled()) {
56
+ this.targetTool.setEnabled(true);
57
+ }
58
+ else {
59
+ this.setDropdownVisible(!this.isDropdownVisible());
60
+ }
61
+ }
62
+ else {
63
+ this.targetTool.setEnabled(!this.targetTool.isEnabled());
64
+ }
65
+ }
66
+ // Adds this to [parent]. This can only be called once for each ToolbarWidget.
67
+ addTo(parent) {
68
+ this.label.innerText = this.getTitle();
69
+ this.icon = null;
70
+ this.updateIcon();
71
+ this.updateSelected(this.targetTool.isEnabled());
72
+ this.button.replaceChildren(this.icon, this.label);
73
+ this.container.appendChild(this.button);
74
+ this.hasDropdown = this.fillDropdown(this.dropdownContainer);
75
+ if (this.hasDropdown) {
76
+ this.dropdownIcon = this.createDropdownIcon();
77
+ this.button.appendChild(this.dropdownIcon);
78
+ this.container.appendChild(this.dropdownContainer);
79
+ }
80
+ this.setDropdownVisible(false);
81
+ parent.appendChild(this.container);
82
+ }
83
+ updateIcon() {
84
+ var _a;
85
+ const newIcon = this.createIcon();
86
+ (_a = this.icon) === null || _a === void 0 ? void 0 : _a.replaceWith(newIcon);
87
+ this.icon = newIcon;
88
+ this.icon.classList.add(`${toolbarCSSPrefix}icon`);
89
+ }
90
+ updateSelected(selected) {
91
+ const currentlySelected = this.container.classList.contains('selected');
92
+ if (currentlySelected === selected) {
93
+ return;
94
+ }
95
+ if (selected) {
96
+ this.container.classList.add('selected');
97
+ this.button.ariaSelected = 'true';
98
+ }
99
+ else {
100
+ this.container.classList.remove('selected');
101
+ this.button.ariaSelected = 'false';
102
+ }
103
+ }
104
+ setDropdownVisible(visible) {
105
+ const currentlyVisible = this.container.classList.contains('dropdownVisible');
106
+ if (currentlyVisible === visible) {
107
+ return;
108
+ }
109
+ if (visible) {
110
+ this.dropdownContainer.classList.remove('hidden');
111
+ this.container.classList.add('dropdownVisible');
112
+ this.editor.announceForAccessibility(this.localizationTable.dropdownShown(this.targetTool.description));
113
+ }
114
+ else {
115
+ this.dropdownContainer.classList.add('hidden');
116
+ this.container.classList.remove('dropdownVisible');
117
+ this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.targetTool.description));
118
+ }
119
+ }
120
+ isDropdownVisible() {
121
+ return !this.dropdownContainer.classList.contains('hidden');
122
+ }
123
+ createDropdownIcon() {
124
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
125
+ icon.innerHTML = `
126
+ <g>
127
+ <path
128
+ d='M5,10 L50,90 L95,10 Z'
129
+ ${primaryForegroundFill}
130
+ />
131
+ </g>
132
+ `;
133
+ icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
134
+ icon.setAttribute('viewBox', '0 0 100 100');
135
+ return icon;
136
+ }
137
+ }
138
+ class EraserWidget extends ToolbarWidget {
139
+ getTitle() {
140
+ return this.localizationTable.eraser;
141
+ }
142
+ createIcon() {
143
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
144
+ // Draw an eraser-like shape
145
+ icon.innerHTML = `
146
+ <g>
147
+ <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
148
+ <rect
149
+ x=10 y=10 width=80 height=50
150
+ ${primaryForegroundFill}
151
+ />
152
+ </g>
153
+ `;
154
+ icon.setAttribute('viewBox', '0 0 100 100');
155
+ return icon;
156
+ }
157
+ fillDropdown(_dropdown) {
158
+ // No dropdown associated with the eraser
159
+ return false;
160
+ }
161
+ }
162
+ class SelectionWidget extends ToolbarWidget {
163
+ constructor(editor, tool, localization) {
164
+ super(editor, tool, localization);
165
+ this.tool = tool;
166
+ }
167
+ getTitle() {
168
+ return this.localizationTable.select;
169
+ }
170
+ createIcon() {
171
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
172
+ // Draw a cursor-like shape
173
+ icon.innerHTML = `
174
+ <g>
175
+ <rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
176
+ <rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
177
+ </g>
178
+ `;
179
+ icon.setAttribute('viewBox', '0 0 100 100');
180
+ return icon;
181
+ }
182
+ fillDropdown(dropdown) {
183
+ const container = document.createElement('div');
184
+ const resizeButton = document.createElement('button');
185
+ resizeButton.innerText = this.localizationTable.resizeImageToSelection;
186
+ resizeButton.disabled = true;
187
+ resizeButton.onclick = () => {
188
+ const selection = this.tool.getSelection();
189
+ this.editor.dispatch(this.editor.setImportExportRect(selection.region));
190
+ };
191
+ // Enable/disable actions based on whether items are selected
192
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
193
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
194
+ throw new Error('Invalid event type!');
195
+ }
196
+ if (toolEvt.tool === this.tool) {
197
+ const selection = this.tool.getSelection();
198
+ const hasSelection = selection && selection.region.area > 0;
199
+ resizeButton.disabled = !hasSelection;
200
+ }
201
+ });
202
+ container.replaceChildren(resizeButton);
203
+ dropdown.appendChild(container);
204
+ return true;
205
+ }
206
+ }
207
+ class TouchDrawingWidget extends ToolbarWidget {
208
+ getTitle() {
209
+ return this.localizationTable.touchDrawing;
210
+ }
211
+ createIcon() {
212
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
213
+ // Draw a cursor-like shape
214
+ icon.innerHTML = `
215
+ <g>
216
+ <path d='M11,-30 Q0,10 20,20 Q40,20 40,-30 Z' fill='blue' stroke='black'/>
217
+ <path d='
218
+ M0,90 L0,50 Q5,40 10,50
219
+ L10,20 Q20,15 30,20
220
+ L30,50 Q50,40 80,50
221
+ L80,90 L10,90 Z'
222
+
223
+ ${primaryForegroundStrokeFill}
224
+ />
225
+ </g>
226
+ `;
227
+ icon.setAttribute('viewBox', '-10 -30 100 100');
228
+ return icon;
229
+ }
230
+ fillDropdown(_dropdown) {
231
+ // No dropdown
232
+ return false;
233
+ }
234
+ updateSelected(active) {
235
+ if (active) {
236
+ this.container.classList.remove('selected');
237
+ }
238
+ else {
239
+ this.container.classList.add('selected');
240
+ }
241
+ }
242
+ }
243
+ class PenWidget extends ToolbarWidget {
244
+ constructor(editor, tool, localization) {
245
+ super(editor, tool, localization);
246
+ this.tool = tool;
247
+ this.updateInputs = () => { };
248
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
249
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
250
+ throw new Error('Invalid event type!');
251
+ }
252
+ // The button icon may depend on tool properties.
253
+ if (toolEvt.tool === this.tool) {
254
+ this.updateIcon();
255
+ this.updateInputs();
256
+ }
257
+ });
258
+ }
259
+ getTitle() {
260
+ return this.targetTool.description;
261
+ }
262
+ createIcon() {
263
+ // We need to use createElementNS to embed an SVG element in HTML.
264
+ // See http://zhangwenli.com/blog/2017/07/26/createelementns/
265
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
266
+ // Use a square-root scale to prevent the pen's tip from overflowing.
267
+ const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
268
+ const color = this.tool.getColor();
269
+ // Draw a pen-like shape
270
+ const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
271
+ const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
272
+ icon.innerHTML = `
273
+ <defs>
274
+ <pattern
275
+ id='checkerboard'
276
+ viewBox='0,0,10,10'
277
+ width='20%'
278
+ height='20%'
279
+ patternUnits='userSpaceOnUse'
280
+ >
281
+ <rect x=0 y=0 width=10 height=10 fill='white'/>
282
+ <rect x=0 y=0 width=5 height=5 fill='gray'/>
283
+ <rect x=5 y=5 width=5 height=5 fill='gray'/>
284
+ </pattern>
285
+ </defs>
286
+ <g>
287
+ <!-- Pen grip -->
288
+ <path
289
+ d='M10,10 L90,10 L90,60 L${50 + scale},80 L${50 - scale},80 L10,60 Z'
290
+ ${primaryForegroundStrokeFill}
291
+ />
292
+ </g>
293
+ <g>
294
+ <!-- Checkerboard background for slightly transparent pens -->
295
+ <path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
296
+
297
+ <!-- Actual pen tip -->
298
+ <path
299
+ d='${primaryStrokeTipPath}'
300
+ fill='${color.toHexString()}'
301
+ stroke='${color.toHexString()}'
302
+ />
303
+ </g>
304
+ `;
305
+ icon.setAttribute('viewBox', '0 0 100 100');
306
+ return icon;
307
+ }
308
+ fillDropdown(dropdown) {
309
+ const container = document.createElement('div');
310
+ // Thickness: Value of the input is squared to allow for finer control/larger values.
311
+ const thicknessRow = document.createElement('div');
312
+ const thicknessLabel = document.createElement('label');
313
+ const thicknessInput = document.createElement('input');
314
+ thicknessInput.id = `${toolbarCSSPrefix}thicknessInput${PenWidget.idCounter++}`;
315
+ thicknessLabel.innerText = this.localizationTable.thicknessLabel;
316
+ thicknessLabel.setAttribute('for', thicknessInput.id);
317
+ thicknessInput.type = 'range';
318
+ thicknessInput.min = '1';
319
+ thicknessInput.max = '20';
320
+ thicknessInput.step = '1';
321
+ thicknessInput.oninput = () => {
322
+ this.tool.setThickness(Math.pow(parseFloat(thicknessInput.value), 2));
323
+ };
324
+ thicknessRow.appendChild(thicknessLabel);
325
+ thicknessRow.appendChild(thicknessInput);
326
+ const colorRow = document.createElement('div');
327
+ const colorLabel = document.createElement('label');
328
+ const colorInput = document.createElement('input');
329
+ colorInput.id = `${toolbarCSSPrefix}colorInput${PenWidget.idCounter++}`;
330
+ colorLabel.innerText = this.localizationTable.colorLabel;
331
+ colorLabel.setAttribute('for', colorInput.id);
332
+ colorInput.className = 'coloris_input';
333
+ colorInput.type = 'button';
334
+ colorInput.oninput = () => {
335
+ this.tool.setColor(Color4.fromHex(colorInput.value));
336
+ };
337
+ colorRow.appendChild(colorLabel);
338
+ colorRow.appendChild(colorInput);
339
+ this.updateInputs = () => {
340
+ colorInput.value = this.tool.getColor().toHexString();
341
+ thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
342
+ };
343
+ this.updateInputs();
344
+ container.replaceChildren(colorRow, thicknessRow);
345
+ dropdown.replaceChildren(container);
346
+ return true;
347
+ }
348
+ }
349
+ PenWidget.idCounter = 0;
350
+ export default class HTMLToolbar {
351
+ constructor(editor, parent, localizationTable = HTMLToolbar.defaultLocalization) {
352
+ this.editor = editor;
353
+ this.localizationTable = localizationTable;
354
+ this.container = document.createElement('div');
355
+ this.container.classList.add(`${toolbarCSSPrefix}root`);
356
+ this.container.setAttribute('role', 'toolbar');
357
+ this.addElements();
358
+ parent.appendChild(this.container);
359
+ // Initialize color choosers
360
+ colorisInit();
361
+ coloris({
362
+ el: '.coloris_input',
363
+ format: 'hex',
364
+ selectInput: false,
365
+ focusInput: false,
366
+ themeMode: 'auto',
367
+ swatches: [
368
+ Color4.red.toHexString(),
369
+ Color4.purple.toHexString(),
370
+ Color4.blue.toHexString(),
371
+ Color4.clay.toHexString(),
372
+ Color4.black.toHexString(),
373
+ Color4.white.toHexString(),
374
+ ],
375
+ });
376
+ }
377
+ addActionButton(text, command, parent) {
378
+ const button = document.createElement('button');
379
+ button.innerText = text;
380
+ button.classList.add(`${toolbarCSSPrefix}toolButton`);
381
+ button.onclick = command;
382
+ (parent !== null && parent !== void 0 ? parent : this.container).appendChild(button);
383
+ return button;
384
+ }
385
+ addUndoRedoButtons() {
386
+ const undoRedoGroup = document.createElement('div');
387
+ undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
388
+ const undoButton = this.addActionButton('Undo', () => {
389
+ this.editor.history.undo();
390
+ }, undoRedoGroup);
391
+ const redoButton = this.addActionButton('Redo', () => {
392
+ this.editor.history.redo();
393
+ }, undoRedoGroup);
394
+ this.container.appendChild(undoRedoGroup);
395
+ undoButton.disabled = true;
396
+ redoButton.disabled = true;
397
+ this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, event => {
398
+ if (event.kind !== EditorEventType.UndoRedoStackUpdated) {
399
+ throw new Error('Wrong event type!');
400
+ }
401
+ undoButton.disabled = event.undoStackSize === 0;
402
+ redoButton.disabled = event.redoStackSize === 0;
403
+ });
404
+ }
405
+ addElements() {
406
+ const toolController = this.editor.toolController;
407
+ for (const tool of toolController.getMatchingTools(ToolType.Pen)) {
408
+ if (!(tool instanceof Pen)) {
409
+ throw new Error('All `Pen` tools must have kind === ToolType.Pen');
410
+ }
411
+ const widget = new PenWidget(this.editor, tool, this.localizationTable);
412
+ widget.addTo(this.container);
413
+ }
414
+ for (const tool of toolController.getMatchingTools(ToolType.Eraser)) {
415
+ if (!(tool instanceof Eraser)) {
416
+ throw new Error('All Erasers must have kind === ToolType.Eraser!');
417
+ }
418
+ (new EraserWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
419
+ }
420
+ for (const tool of toolController.getMatchingTools(ToolType.Selection)) {
421
+ if (!(tool instanceof SelectionTool)) {
422
+ throw new Error('All SelectionTools must have kind === ToolType.Selection');
423
+ }
424
+ (new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
425
+ }
426
+ for (const tool of toolController.getMatchingTools(ToolType.TouchPanZoom)) {
427
+ (new TouchDrawingWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
428
+ }
429
+ this.addUndoRedoButtons();
430
+ }
431
+ }
432
+ HTMLToolbar.defaultLocalization = {
433
+ pen: 'Pen',
434
+ eraser: 'Eraser',
435
+ select: 'Select',
436
+ touchDrawing: 'Touch Drawing',
437
+ thicknessLabel: 'Thickness: ',
438
+ colorLabel: 'Color: ',
439
+ resizeImageToSelection: 'Resize image to selection',
440
+ undo: 'Undo',
441
+ redo: 'Redo',
442
+ dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
443
+ dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
444
+ };
@@ -0,0 +1,17 @@
1
+ export declare enum ToolbarButtonType {
2
+ ToggleButton = 0,
3
+ ActionButton = 1
4
+ }
5
+ export interface ToolbarLocalization {
6
+ colorLabel: string;
7
+ pen: string;
8
+ eraser: string;
9
+ select: string;
10
+ touchDrawing: string;
11
+ thicknessLabel: string;
12
+ resizeImageToSelection: string;
13
+ undo: string;
14
+ redo: string;
15
+ dropdownShown: (toolName: string) => string;
16
+ dropdownHidden: (toolName: string) => string;
17
+ }
@@ -0,0 +1,5 @@
1
+ export var ToolbarButtonType;
2
+ (function (ToolbarButtonType) {
3
+ ToolbarButtonType[ToolbarButtonType["ToggleButton"] = 0] = "ToggleButton";
4
+ ToolbarButtonType[ToolbarButtonType["ActionButton"] = 1] = "ActionButton";
5
+ })(ToolbarButtonType || (ToolbarButtonType = {}));
@@ -0,0 +1,20 @@
1
+ import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent } from '../types';
2
+ import { ToolType } from './ToolController';
3
+ import ToolEnabledGroup from './ToolEnabledGroup';
4
+ export default abstract class BaseTool implements PointerEvtListener {
5
+ private notifier;
6
+ readonly description: string;
7
+ private enabled;
8
+ private group;
9
+ abstract onPointerDown(event: PointerEvt): boolean;
10
+ abstract onPointerMove(event: PointerEvt): void;
11
+ abstract onPointerUp(event: PointerEvt): void;
12
+ abstract onGestureCancel(): void;
13
+ abstract readonly kind: ToolType;
14
+ protected constructor(notifier: EditorNotifier, description: string);
15
+ onWheel(_event: WheelEvt): boolean;
16
+ onKeyPress(_event: KeyPressEvent): boolean;
17
+ setEnabled(enabled: boolean): void;
18
+ isEnabled(): boolean;
19
+ setToolGroup(group: ToolEnabledGroup): void;
20
+ }
@@ -0,0 +1,44 @@
1
+ import { EditorEventType } from '../types';
2
+ export default class BaseTool {
3
+ constructor(notifier, description) {
4
+ this.notifier = notifier;
5
+ this.description = description;
6
+ this.enabled = true;
7
+ this.group = null;
8
+ }
9
+ onWheel(_event) {
10
+ return false;
11
+ }
12
+ onKeyPress(_event) {
13
+ return false;
14
+ }
15
+ setEnabled(enabled) {
16
+ var _a;
17
+ this.enabled = enabled;
18
+ // Ensure that at most one tool in the group is enabled.
19
+ if (enabled) {
20
+ (_a = this.group) === null || _a === void 0 ? void 0 : _a.notifyEnabled(this);
21
+ this.notifier.dispatch(EditorEventType.ToolEnabled, {
22
+ kind: EditorEventType.ToolEnabled,
23
+ tool: this,
24
+ });
25
+ }
26
+ else {
27
+ this.notifier.dispatch(EditorEventType.ToolDisabled, {
28
+ kind: EditorEventType.ToolDisabled,
29
+ tool: this,
30
+ });
31
+ }
32
+ }
33
+ isEnabled() {
34
+ return this.enabled;
35
+ }
36
+ // Connect this tool to a set of other tools, ensuring that at most one
37
+ // of the tools in the group is enabled.
38
+ setToolGroup(group) {
39
+ if (this.isEnabled()) {
40
+ group.notifyEnabled(this);
41
+ }
42
+ this.group = group;
43
+ }
44
+ }
@@ -0,0 +1,16 @@
1
+ import { PointerEvt } from '../types';
2
+ import BaseTool from './BaseTool';
3
+ import Editor from '../Editor';
4
+ import { ToolType } from './ToolController';
5
+ export default class Eraser extends BaseTool {
6
+ private editor;
7
+ private lastPoint;
8
+ private command;
9
+ kind: ToolType;
10
+ private toRemove;
11
+ constructor(editor: Editor, description: string);
12
+ onPointerDown(event: PointerEvt): boolean;
13
+ onPointerMove(event: PointerEvt): void;
14
+ onPointerUp(_event: PointerEvt): void;
15
+ onGestureCancel(): void;
16
+ }
@@ -0,0 +1,53 @@
1
+ import BaseTool from './BaseTool';
2
+ import LineSegment2 from '../geometry/LineSegment2';
3
+ import Erase from '../commands/Erase';
4
+ import { ToolType } from './ToolController';
5
+ import { PointerDevice } from '../Pointer';
6
+ export default class Eraser extends BaseTool {
7
+ constructor(editor, description) {
8
+ super(editor.notifier, description);
9
+ this.editor = editor;
10
+ this.command = null;
11
+ this.kind = ToolType.Eraser;
12
+ }
13
+ onPointerDown(event) {
14
+ if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
15
+ this.lastPoint = event.current.canvasPos;
16
+ this.toRemove = [];
17
+ return true;
18
+ }
19
+ return false;
20
+ }
21
+ onPointerMove(event) {
22
+ var _a;
23
+ const currentPoint = event.current.canvasPos;
24
+ if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
25
+ return;
26
+ }
27
+ const line = new LineSegment2(this.lastPoint, currentPoint);
28
+ const region = line.bbox;
29
+ // Remove any intersecting elements.
30
+ this.toRemove.push(...this.editor.image
31
+ .getElementsIntersectingRegion(region).filter(component => {
32
+ return component.intersects(line);
33
+ }));
34
+ (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
35
+ this.command = new Erase(this.toRemove);
36
+ this.command.apply(this.editor);
37
+ this.lastPoint = currentPoint;
38
+ }
39
+ onPointerUp(_event) {
40
+ var _a;
41
+ if (this.command && this.toRemove.length > 0) {
42
+ (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
43
+ // Dispatch the command to make it undo-able
44
+ this.editor.dispatch(this.command);
45
+ }
46
+ this.command = null;
47
+ }
48
+ onGestureCancel() {
49
+ var _a;
50
+ (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
51
+ this.command = null;
52
+ }
53
+ }
@@ -0,0 +1,40 @@
1
+ import { Editor } from '../Editor';
2
+ import { Point2 } from '../geometry/Vec2';
3
+ import Pointer from '../Pointer';
4
+ import { KeyPressEvent, PointerEvt, WheelEvt } from '../types';
5
+ import BaseTool from './BaseTool';
6
+ import { ToolType } from './ToolController';
7
+ interface PinchData {
8
+ canvasCenter: Point2;
9
+ screenCenter: Point2;
10
+ angle: number;
11
+ dist: number;
12
+ }
13
+ export declare enum PanZoomMode {
14
+ OneFingerGestures = 1,
15
+ TwoFingerGestures = 2,
16
+ AnyDevice = 4
17
+ }
18
+ export default class PanZoom extends BaseTool {
19
+ private editor;
20
+ private mode;
21
+ readonly kind: ToolType.PanZoom | ToolType.TouchPanZoom;
22
+ private transform;
23
+ private lastAngle;
24
+ private lastDist;
25
+ private lastScreenCenter;
26
+ constructor(editor: Editor, mode: PanZoomMode, description: string);
27
+ computePinchData(p1: Pointer, p2: Pointer): PinchData;
28
+ private pointersHaveCorrectDeviceType;
29
+ onPointerDown({ allPointers }: PointerEvt): boolean;
30
+ private getCenterDelta;
31
+ private handleTwoFingerMove;
32
+ private handleOneFingerMove;
33
+ onPointerMove({ allPointers }: PointerEvt): void;
34
+ onPointerUp(_event: PointerEvt): void;
35
+ onGestureCancel(): void;
36
+ private updateTransform;
37
+ onWheel({ delta, screenPos }: WheelEvt): boolean;
38
+ onKeyPress({ key }: KeyPressEvent): boolean;
39
+ }
40
+ export {};