js-draw 0.1.1 → 0.1.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 (86) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +21 -12
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +2 -1
  5. package/dist/src/Editor.js +24 -6
  6. package/dist/src/EditorImage.js +3 -0
  7. package/dist/src/Pointer.d.ts +3 -2
  8. package/dist/src/Pointer.js +12 -3
  9. package/dist/src/SVGLoader.d.ts +11 -0
  10. package/dist/src/SVGLoader.js +113 -4
  11. package/dist/src/Viewport.d.ts +1 -1
  12. package/dist/src/Viewport.js +12 -2
  13. package/dist/src/components/AbstractComponent.d.ts +6 -0
  14. package/dist/src/components/AbstractComponent.js +11 -0
  15. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  16. package/dist/src/components/Stroke.js +1 -1
  17. package/dist/src/components/Text.d.ts +30 -0
  18. package/dist/src/components/Text.js +111 -0
  19. package/dist/src/components/localization.d.ts +1 -0
  20. package/dist/src/components/localization.js +1 -0
  21. package/dist/src/geometry/Mat33.d.ts +1 -0
  22. package/dist/src/geometry/Mat33.js +30 -0
  23. package/dist/src/geometry/Path.js +105 -67
  24. package/dist/src/geometry/Rect2.d.ts +2 -0
  25. package/dist/src/geometry/Rect2.js +6 -0
  26. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
  27. package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  29. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  30. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  31. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  32. package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
  33. package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
  34. package/dist/src/testing/loadExpectExtensions.js +1 -4
  35. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  36. package/dist/src/toolbar/HTMLToolbar.js +242 -154
  37. package/dist/src/toolbar/icons.d.ts +12 -0
  38. package/dist/src/toolbar/icons.js +198 -0
  39. package/dist/src/toolbar/localization.d.ts +5 -1
  40. package/dist/src/toolbar/localization.js +5 -1
  41. package/dist/src/toolbar/types.d.ts +4 -0
  42. package/dist/src/tools/PanZoom.d.ts +9 -6
  43. package/dist/src/tools/PanZoom.js +30 -21
  44. package/dist/src/tools/Pen.js +8 -3
  45. package/dist/src/tools/SelectionTool.js +1 -1
  46. package/dist/src/tools/TextTool.d.ts +30 -0
  47. package/dist/src/tools/TextTool.js +173 -0
  48. package/dist/src/tools/ToolController.d.ts +5 -5
  49. package/dist/src/tools/ToolController.js +10 -9
  50. package/dist/src/tools/localization.d.ts +3 -0
  51. package/dist/src/tools/localization.js +3 -0
  52. package/dist-test/test-dist-bundle.html +8 -1
  53. package/package.json +1 -1
  54. package/src/Editor.css +2 -0
  55. package/src/Editor.ts +26 -7
  56. package/src/EditorImage.ts +4 -0
  57. package/src/Pointer.ts +13 -4
  58. package/src/SVGLoader.ts +146 -5
  59. package/src/Viewport.ts +15 -3
  60. package/src/components/AbstractComponent.ts +16 -1
  61. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  62. package/src/components/Stroke.ts +1 -1
  63. package/src/components/Text.ts +140 -0
  64. package/src/components/localization.ts +2 -0
  65. package/src/geometry/Mat33.test.ts +44 -0
  66. package/src/geometry/Mat33.ts +41 -0
  67. package/src/geometry/Path.fromString.test.ts +94 -4
  68. package/src/geometry/Path.toString.test.ts +7 -3
  69. package/src/geometry/Path.ts +110 -68
  70. package/src/geometry/Rect2.ts +8 -0
  71. package/src/rendering/renderers/AbstractRenderer.ts +18 -1
  72. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  73. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  74. package/src/rendering/renderers/SVGRenderer.ts +57 -10
  75. package/src/testing/loadExpectExtensions.ts +1 -4
  76. package/src/toolbar/HTMLToolbar.ts +294 -170
  77. package/src/toolbar/icons.ts +227 -0
  78. package/src/toolbar/localization.ts +11 -2
  79. package/src/toolbar/toolbar.css +27 -11
  80. package/src/toolbar/types.ts +5 -0
  81. package/src/tools/PanZoom.ts +37 -27
  82. package/src/tools/Pen.ts +7 -3
  83. package/src/tools/SelectionTool.ts +1 -1
  84. package/src/tools/TextTool.ts +225 -0
  85. package/src/tools/ToolController.ts +7 -5
  86. package/src/tools/localization.ts +7 -0
@@ -6,22 +6,16 @@ import Pen from '../tools/Pen';
6
6
  import Eraser from '../tools/Eraser';
7
7
  import SelectionTool from '../tools/SelectionTool';
8
8
  import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
9
- import { Vec2 } from '../geometry/Vec2';
10
- import SVGRenderer from '../rendering/renderers/SVGRenderer';
11
- import Viewport from '../Viewport';
12
- import EventDispatcher from '../EventDispatcher';
13
9
  import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
14
10
  import { makeLineBuilder } from '../components/builders/LineBuilder';
15
11
  import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
16
12
  import { defaultToolbarLocalization } from './localization';
17
- const primaryForegroundFill = `
18
- style='fill: var(--primary-foreground-color);'
19
- `;
20
- const primaryForegroundStrokeFill = `
21
- style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
22
- `;
13
+ import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
14
+ import PanZoom, { PanZoomMode } from '../tools/PanZoom';
15
+ import Mat33 from '../geometry/Mat33';
16
+ import Viewport from '../Viewport';
17
+ import TextTool from '../tools/TextTool';
23
18
  const toolbarCSSPrefix = 'toolbar-';
24
- const svgNamespace = 'http://www.w3.org/2000/svg';
25
19
  class ToolbarWidget {
26
20
  constructor(editor, targetTool, localizationTable) {
27
21
  this.editor = editor;
@@ -39,9 +33,6 @@ class ToolbarWidget {
39
33
  this.label = document.createElement('label');
40
34
  this.button.setAttribute('role', 'button');
41
35
  this.button.tabIndex = 0;
42
- this.button.onclick = () => {
43
- this.handleClick();
44
- };
45
36
  editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
46
37
  if (toolEvt.kind !== EditorEventType.ToolEnabled) {
47
38
  throw new Error('Incorrect event type! (Expected ToolEnabled)');
@@ -60,6 +51,11 @@ class ToolbarWidget {
60
51
  }
61
52
  });
62
53
  }
54
+ setupActionBtnClickListener(button) {
55
+ button.onclick = () => {
56
+ this.handleClick();
57
+ };
58
+ }
63
59
  handleClick() {
64
60
  if (this.hasDropdown) {
65
61
  if (!this.targetTool.isEnabled()) {
@@ -76,6 +72,7 @@ class ToolbarWidget {
76
72
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
77
73
  addTo(parent) {
78
74
  this.label.innerText = this.getTitle();
75
+ this.setupActionBtnClickListener(this.button);
79
76
  this.icon = null;
80
77
  this.updateIcon();
81
78
  this.updateSelected(this.targetTool.isEnabled());
@@ -126,22 +123,26 @@ class ToolbarWidget {
126
123
  this.container.classList.remove('dropdownVisible');
127
124
  this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.targetTool.description));
128
125
  }
126
+ this.repositionDropdown();
127
+ }
128
+ repositionDropdown() {
129
+ const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
130
+ const screenWidth = document.body.clientWidth;
131
+ if (dropdownBBox.left > screenWidth / 2) {
132
+ this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
133
+ this.dropdownContainer.style.transform = 'translate(-100%, 0)';
134
+ }
135
+ else {
136
+ this.dropdownContainer.style.marginLeft = '';
137
+ this.dropdownContainer.style.transform = '';
138
+ }
129
139
  }
130
140
  isDropdownVisible() {
131
141
  return !this.dropdownContainer.classList.contains('hidden');
132
142
  }
133
143
  createDropdownIcon() {
134
- const icon = document.createElementNS(svgNamespace, 'svg');
135
- icon.innerHTML = `
136
- <g>
137
- <path
138
- d='M5,10 L50,90 L95,10 Z'
139
- ${primaryForegroundFill}
140
- />
141
- </g>
142
- `;
144
+ const icon = makeDropdownIcon();
143
145
  icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
144
- icon.setAttribute('viewBox', '0 0 100 100');
145
146
  return icon;
146
147
  }
147
148
  }
@@ -150,19 +151,7 @@ class EraserWidget extends ToolbarWidget {
150
151
  return this.localizationTable.eraser;
151
152
  }
152
153
  createIcon() {
153
- const icon = document.createElementNS(svgNamespace, 'svg');
154
- // Draw an eraser-like shape
155
- icon.innerHTML = `
156
- <g>
157
- <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
158
- <rect
159
- x=10 y=10 width=80 height=50
160
- ${primaryForegroundFill}
161
- />
162
- </g>
163
- `;
164
- icon.setAttribute('viewBox', '0 0 100 100');
165
- return icon;
154
+ return makeEraserIcon();
166
155
  }
167
156
  fillDropdown(_dropdown) {
168
157
  // No dropdown associated with the eraser
@@ -178,16 +167,7 @@ class SelectionWidget extends ToolbarWidget {
178
167
  return this.localizationTable.select;
179
168
  }
180
169
  createIcon() {
181
- const icon = document.createElementNS(svgNamespace, 'svg');
182
- // Draw a cursor-like shape
183
- icon.innerHTML = `
184
- <g>
185
- <rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
186
- <rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
187
- </g>
188
- `;
189
- icon.setAttribute('viewBox', '0 0 100 100');
190
- return icon;
170
+ return makeSelectionIcon();
191
171
  }
192
172
  fillDropdown(dropdown) {
193
173
  const container = document.createElement('div');
@@ -223,42 +203,190 @@ class SelectionWidget extends ToolbarWidget {
223
203
  return true;
224
204
  }
225
205
  }
226
- class TouchDrawingWidget extends ToolbarWidget {
206
+ const makeZoomControl = (localizationTable, editor) => {
207
+ const zoomLevelRow = document.createElement('div');
208
+ const increaseButton = document.createElement('button');
209
+ const decreaseButton = document.createElement('button');
210
+ const zoomLevelDisplay = document.createElement('span');
211
+ increaseButton.innerText = '+';
212
+ decreaseButton.innerText = '-';
213
+ zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
214
+ zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
215
+ zoomLevelDisplay.classList.add('zoomDisplay');
216
+ let lastZoom;
217
+ const updateZoomDisplay = () => {
218
+ let zoomLevel = editor.viewport.getScaleFactor() * 100;
219
+ if (zoomLevel > 0.1) {
220
+ zoomLevel = Math.round(zoomLevel * 10) / 10;
221
+ }
222
+ else {
223
+ zoomLevel = Math.round(zoomLevel * 1000) / 1000;
224
+ }
225
+ if (zoomLevel !== lastZoom) {
226
+ zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
227
+ lastZoom = zoomLevel;
228
+ }
229
+ };
230
+ updateZoomDisplay();
231
+ editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
232
+ if (event.kind === EditorEventType.ViewportChanged) {
233
+ updateZoomDisplay();
234
+ }
235
+ });
236
+ const zoomBy = (factor) => {
237
+ const screenCenter = editor.viewport.visibleRect.center;
238
+ const transformUpdate = Mat33.scaling2D(factor, screenCenter);
239
+ editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
240
+ };
241
+ increaseButton.onclick = () => {
242
+ zoomBy(5.0 / 4);
243
+ };
244
+ decreaseButton.onclick = () => {
245
+ zoomBy(4.0 / 5);
246
+ };
247
+ return zoomLevelRow;
248
+ };
249
+ class HandToolWidget extends ToolbarWidget {
250
+ constructor(editor, tool, localizationTable) {
251
+ super(editor, tool, localizationTable);
252
+ this.tool = tool;
253
+ this.container.classList.add('dropdownShowable');
254
+ }
227
255
  getTitle() {
228
- return this.localizationTable.touchDrawing;
256
+ return this.localizationTable.handTool;
229
257
  }
230
258
  createIcon() {
231
- const icon = document.createElementNS(svgNamespace, 'svg');
232
- // Draw a cursor-like shape
233
- icon.innerHTML = `
234
- <g>
235
- <path d='M11,-30 Q0,10 20,20 Q40,20 40,-30 Z' fill='blue' stroke='black'/>
236
- <path d='
237
- M0,90 L0,50 Q5,40 10,50
238
- L10,20 Q20,15 30,20
239
- L30,50 Q50,40 80,50
240
- L80,90 L10,90 Z'
241
-
242
- ${primaryForegroundStrokeFill}
243
- />
244
- </g>
245
- `;
246
- icon.setAttribute('viewBox', '-10 -30 100 100');
247
- return icon;
259
+ return makeHandToolIcon();
248
260
  }
249
- fillDropdown(_dropdown) {
250
- // No dropdown
251
- return false;
261
+ fillDropdown(dropdown) {
262
+ let idCounter = 0;
263
+ const addCheckbox = (label, onToggle) => {
264
+ const rowContainer = document.createElement('div');
265
+ const labelElem = document.createElement('label');
266
+ const checkboxElem = document.createElement('input');
267
+ checkboxElem.type = 'checkbox';
268
+ checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
269
+ labelElem.setAttribute('for', checkboxElem.id);
270
+ checkboxElem.oninput = () => {
271
+ onToggle(checkboxElem.checked);
272
+ };
273
+ labelElem.innerText = label;
274
+ rowContainer.replaceChildren(checkboxElem, labelElem);
275
+ dropdown.appendChild(rowContainer);
276
+ return checkboxElem;
277
+ };
278
+ const setModeFlag = (enabled, flag) => {
279
+ const mode = this.tool.getMode();
280
+ if (enabled) {
281
+ this.tool.setMode(mode | flag);
282
+ }
283
+ else {
284
+ this.tool.setMode(mode & ~flag);
285
+ }
286
+ };
287
+ const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
288
+ setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
289
+ });
290
+ const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
291
+ setModeFlag(checked, PanZoomMode.SinglePointerGestures);
292
+ });
293
+ dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
294
+ const updateInputs = () => {
295
+ const mode = this.tool.getMode();
296
+ anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
297
+ if (anyDevicePanningCheckbox.checked) {
298
+ touchPanningCheckbox.checked = true;
299
+ touchPanningCheckbox.disabled = true;
300
+ }
301
+ else {
302
+ touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
303
+ touchPanningCheckbox.disabled = false;
304
+ }
305
+ };
306
+ updateInputs();
307
+ this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
308
+ if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
309
+ updateInputs();
310
+ }
311
+ });
312
+ return true;
252
313
  }
253
- updateSelected(active) {
254
- if (active) {
255
- this.container.classList.remove('selected');
256
- }
257
- else {
258
- this.container.classList.add('selected');
259
- }
314
+ updateSelected(_active) {
315
+ }
316
+ handleClick() {
317
+ this.setDropdownVisible(!this.isDropdownVisible());
260
318
  }
261
319
  }
320
+ class TextToolWidget extends ToolbarWidget {
321
+ constructor(editor, tool, localization) {
322
+ super(editor, tool, localization);
323
+ this.tool = tool;
324
+ this.updateDropdownInputs = null;
325
+ editor.notifier.on(EditorEventType.ToolUpdated, evt => {
326
+ var _a;
327
+ if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
328
+ this.updateIcon();
329
+ (_a = this.updateDropdownInputs) === null || _a === void 0 ? void 0 : _a.call(this);
330
+ }
331
+ });
332
+ }
333
+ getTitle() {
334
+ return this.targetTool.description;
335
+ }
336
+ createIcon() {
337
+ const textStyle = this.tool.getTextStyle();
338
+ return makeTextIcon(textStyle);
339
+ }
340
+ fillDropdown(dropdown) {
341
+ const fontRow = document.createElement('div');
342
+ const colorRow = document.createElement('div');
343
+ const fontInput = document.createElement('select');
344
+ const fontLabel = document.createElement('label');
345
+ const colorInput = document.createElement('input');
346
+ const colorLabel = document.createElement('label');
347
+ const fontsInInput = new Set();
348
+ const addFontToInput = (fontName) => {
349
+ const option = document.createElement('option');
350
+ option.value = fontName;
351
+ option.textContent = fontName;
352
+ fontInput.appendChild(option);
353
+ fontsInInput.add(fontName);
354
+ };
355
+ fontLabel.innerText = this.localizationTable.fontLabel;
356
+ colorLabel.innerText = this.localizationTable.colorLabel;
357
+ colorInput.classList.add('coloris_input');
358
+ colorInput.type = 'button';
359
+ colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
360
+ colorLabel.setAttribute('for', colorInput.id);
361
+ addFontToInput('monospace');
362
+ addFontToInput('serif');
363
+ addFontToInput('sans-serif');
364
+ fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
365
+ fontLabel.setAttribute('for', fontInput.id);
366
+ fontInput.onchange = () => {
367
+ this.tool.setFontFamily(fontInput.value);
368
+ };
369
+ colorInput.oninput = () => {
370
+ this.tool.setColor(Color4.fromString(colorInput.value));
371
+ };
372
+ colorRow.appendChild(colorLabel);
373
+ colorRow.appendChild(colorInput);
374
+ fontRow.appendChild(fontLabel);
375
+ fontRow.appendChild(fontInput);
376
+ this.updateDropdownInputs = () => {
377
+ const style = this.tool.getTextStyle();
378
+ colorInput.value = style.renderingStyle.fill.toHexString();
379
+ if (!fontsInInput.has(style.fontFamily)) {
380
+ addFontToInput(style.fontFamily);
381
+ }
382
+ fontInput.value = style.fontFamily;
383
+ };
384
+ this.updateDropdownInputs();
385
+ dropdown.replaceChildren(colorRow, fontRow);
386
+ return true;
387
+ }
388
+ }
389
+ TextToolWidget.idCounter = 0;
262
390
  class PenWidget extends ToolbarWidget {
263
391
  constructor(editor, tool, localization, penTypes) {
264
392
  super(editor, tool, localization);
@@ -279,84 +407,18 @@ class PenWidget extends ToolbarWidget {
279
407
  getTitle() {
280
408
  return this.targetTool.description;
281
409
  }
282
- makePenIcon(elem) {
283
- // Use a square-root scale to prevent the pen's tip from overflowing.
284
- const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
285
- const color = this.tool.getColor();
286
- // Draw a pen-like shape
287
- const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
288
- const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
289
- elem.innerHTML = `
290
- <defs>
291
- <pattern
292
- id='checkerboard'
293
- viewBox='0,0,10,10'
294
- width='20%'
295
- height='20%'
296
- patternUnits='userSpaceOnUse'
297
- >
298
- <rect x=0 y=0 width=10 height=10 fill='white'/>
299
- <rect x=0 y=0 width=5 height=5 fill='gray'/>
300
- <rect x=5 y=5 width=5 height=5 fill='gray'/>
301
- </pattern>
302
- </defs>
303
- <g>
304
- <!-- Pen grip -->
305
- <path
306
- d='M10,10 L90,10 L90,60 L${50 + scale},80 L${50 - scale},80 L10,60 Z'
307
- ${primaryForegroundStrokeFill}
308
- />
309
- </g>
310
- <g>
311
- <!-- Checkerboard background for slightly transparent pens -->
312
- <path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
313
-
314
- <!-- Actual pen tip -->
315
- <path
316
- d='${primaryStrokeTipPath}'
317
- fill='${color.toHexString()}'
318
- stroke='${color.toHexString()}'
319
- />
320
- </g>
321
- `;
322
- }
323
- // Draws an icon with the pen.
324
- makeDrawnIcon(icon) {
325
- const strokeFactory = this.tool.getStrokeFactory();
326
- const toolThickness = this.tool.getThickness();
327
- const nowTime = (new Date()).getTime();
328
- const startPoint = {
329
- pos: Vec2.of(10, 10),
330
- width: toolThickness / 5,
331
- color: this.tool.getColor(),
332
- time: nowTime - 100,
333
- };
334
- const endPoint = {
335
- pos: Vec2.of(90, 90),
336
- width: toolThickness / 5,
337
- color: this.tool.getColor(),
338
- time: nowTime,
339
- };
340
- const builder = strokeFactory(startPoint, this.editor.viewport);
341
- builder.addPoint(endPoint);
342
- const viewport = new Viewport(new EventDispatcher());
343
- viewport.updateScreenSize(Vec2.of(100, 100));
344
- const renderer = new SVGRenderer(icon, viewport);
345
- builder.preview(renderer);
346
- }
347
410
  createIcon() {
348
- // We need to use createElementNS to embed an SVG element in HTML.
349
- // See http://zhangwenli.com/blog/2017/07/26/createelementns/
350
- const icon = document.createElementNS(svgNamespace, 'svg');
351
- icon.setAttribute('viewBox', '0 0 100 100');
352
411
  const strokeFactory = this.tool.getStrokeFactory();
353
412
  if (strokeFactory === makeFreehandLineBuilder) {
354
- this.makePenIcon(icon);
413
+ // Use a square-root scale to prevent the pen's tip from overflowing.
414
+ const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
415
+ const color = this.tool.getColor();
416
+ return makePenIcon(scale, color.toHexString());
355
417
  }
356
418
  else {
357
- this.makeDrawnIcon(icon);
419
+ const strokeFactory = this.tool.getStrokeFactory();
420
+ return makeIconFromFactory(this.tool, strokeFactory);
358
421
  }
359
- return icon;
360
422
  }
361
423
  fillDropdown(dropdown) {
362
424
  const container = document.createElement('div');
@@ -502,10 +564,21 @@ export default class HTMLToolbar {
502
564
  closePickerOverlay.style.display = event.open ? 'block' : 'none';
503
565
  });
504
566
  }
505
- addActionButton(text, command, parent) {
567
+ addActionButton(title, command, parent) {
506
568
  const button = document.createElement('button');
507
- button.innerText = text;
508
569
  button.classList.add(`${toolbarCSSPrefix}toolButton`);
570
+ if (typeof title === 'string') {
571
+ button.innerText = title;
572
+ }
573
+ else {
574
+ const iconElem = title.icon.cloneNode(true);
575
+ const labelElem = document.createElement('label');
576
+ // Use the label to describe the icon -- no additional description should be necessary.
577
+ iconElem.setAttribute('alt', '');
578
+ labelElem.innerText = title.label;
579
+ iconElem.classList.add('toolbar-icon');
580
+ button.replaceChildren(iconElem, labelElem);
581
+ }
509
582
  button.onclick = command;
510
583
  (parent !== null && parent !== void 0 ? parent : this.container).appendChild(button);
511
584
  return button;
@@ -513,10 +586,16 @@ export default class HTMLToolbar {
513
586
  addUndoRedoButtons() {
514
587
  const undoRedoGroup = document.createElement('div');
515
588
  undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
516
- const undoButton = this.addActionButton('Undo', () => {
589
+ const undoButton = this.addActionButton({
590
+ label: 'Undo',
591
+ icon: makeUndoIcon()
592
+ }, () => {
517
593
  this.editor.history.undo();
518
594
  }, undoRedoGroup);
519
- const redoButton = this.addActionButton('Redo', () => {
595
+ const redoButton = this.addActionButton({
596
+ label: 'Redo',
597
+ icon: makeRedoIcon(),
598
+ }, () => {
520
599
  this.editor.history.redo();
521
600
  }, undoRedoGroup);
522
601
  this.container.appendChild(undoRedoGroup);
@@ -551,8 +630,17 @@ export default class HTMLToolbar {
551
630
  }
552
631
  (new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
553
632
  }
554
- for (const tool of toolController.getMatchingTools(ToolType.TouchPanZoom)) {
555
- (new TouchDrawingWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
633
+ for (const tool of toolController.getMatchingTools(ToolType.Text)) {
634
+ if (!(tool instanceof TextTool)) {
635
+ throw new Error('All text tools must have kind === ToolType.Text');
636
+ }
637
+ (new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
638
+ }
639
+ for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
640
+ if (!(tool instanceof PanZoom)) {
641
+ throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
642
+ }
643
+ (new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
556
644
  }
557
645
  this.setupColorPickers();
558
646
  }
@@ -0,0 +1,12 @@
1
+ import { ComponentBuilderFactory } from '../components/builders/types';
2
+ import { TextStyle } from '../components/Text';
3
+ import Pen from '../tools/Pen';
4
+ export declare const makeUndoIcon: () => SVGSVGElement;
5
+ export declare const makeRedoIcon: (mirror?: boolean) => SVGSVGElement;
6
+ export declare const makeDropdownIcon: () => SVGSVGElement;
7
+ export declare const makeEraserIcon: () => SVGSVGElement;
8
+ export declare const makeSelectionIcon: () => SVGSVGElement;
9
+ export declare const makeHandToolIcon: () => SVGSVGElement;
10
+ export declare const makeTextIcon: (textStyle: TextStyle) => SVGSVGElement;
11
+ export declare const makePenIcon: (tipThickness: number, color: string) => SVGSVGElement;
12
+ export declare const makeIconFromFactory: (pen: Pen, factory: ComponentBuilderFactory) => SVGSVGElement;