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
@@ -1,6 +1,6 @@
1
1
  import Editor from '../Editor';
2
2
  import { ToolType } from '../tools/ToolController';
3
- import { EditorEventType, StrokeDataPoint } from '../types';
3
+ import { EditorEventType } from '../types';
4
4
 
5
5
  import { coloris, init as colorisInit } from '@melloware/coloris';
6
6
  import Color4 from '../Color4';
@@ -9,25 +9,20 @@ import Eraser from '../tools/Eraser';
9
9
  import BaseTool from '../tools/BaseTool';
10
10
  import SelectionTool from '../tools/SelectionTool';
11
11
  import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
12
- import { Vec2 } from '../geometry/Vec2';
13
- import SVGRenderer from '../rendering/renderers/SVGRenderer';
14
- import Viewport from '../Viewport';
15
- import EventDispatcher from '../EventDispatcher';
16
12
  import { ComponentBuilderFactory } from '../components/builders/types';
17
13
  import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
18
14
  import { makeLineBuilder } from '../components/builders/LineBuilder';
19
15
  import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
20
16
  import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
17
+ import { ActionButtonIcon } from './types';
18
+ import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
19
+ import PanZoom, { PanZoomMode } from '../tools/PanZoom';
20
+ import Mat33 from '../geometry/Mat33';
21
+ import Viewport from '../Viewport';
22
+ import TextTool from '../tools/TextTool';
21
23
 
22
- const primaryForegroundFill = `
23
- style='fill: var(--primary-foreground-color);'
24
- `;
25
- const primaryForegroundStrokeFill = `
26
- style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
27
- `;
28
24
 
29
25
  const toolbarCSSPrefix = 'toolbar-';
30
- const svgNamespace = 'http://www.w3.org/2000/svg';
31
26
 
32
27
  abstract class ToolbarWidget {
33
28
  protected readonly container: HTMLElement;
@@ -57,11 +52,6 @@ abstract class ToolbarWidget {
57
52
  this.button.setAttribute('role', 'button');
58
53
  this.button.tabIndex = 0;
59
54
 
60
- this.button.onclick = () => {
61
- this.handleClick();
62
- };
63
-
64
-
65
55
  editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
66
56
  if (toolEvt.kind !== EditorEventType.ToolEnabled) {
67
57
  throw new Error('Incorrect event type! (Expected ToolEnabled)');
@@ -91,6 +81,12 @@ abstract class ToolbarWidget {
91
81
  // Returns true if such a menu should be created, false otherwise.
92
82
  protected abstract fillDropdown(dropdown: HTMLElement): boolean;
93
83
 
84
+ protected setupActionBtnClickListener(button: HTMLElement) {
85
+ button.onclick = () => {
86
+ this.handleClick();
87
+ };
88
+ }
89
+
94
90
  protected handleClick() {
95
91
  if (this.hasDropdown) {
96
92
  if (!this.targetTool.isEnabled()) {
@@ -107,6 +103,8 @@ abstract class ToolbarWidget {
107
103
  public addTo(parent: HTMLElement) {
108
104
  this.label.innerText = this.getTitle();
109
105
 
106
+ this.setupActionBtnClickListener(this.button);
107
+
110
108
  this.icon = null;
111
109
  this.updateIcon();
112
110
 
@@ -167,6 +165,21 @@ abstract class ToolbarWidget {
167
165
  this.localizationTable.dropdownHidden(this.targetTool.description)
168
166
  );
169
167
  }
168
+
169
+ this.repositionDropdown();
170
+ }
171
+
172
+ protected repositionDropdown() {
173
+ const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
174
+ const screenWidth = document.body.clientWidth;
175
+
176
+ if (dropdownBBox.left > screenWidth / 2) {
177
+ this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
178
+ this.dropdownContainer.style.transform = 'translate(-100%, 0)';
179
+ } else {
180
+ this.dropdownContainer.style.marginLeft = '';
181
+ this.dropdownContainer.style.transform = '';
182
+ }
170
183
  }
171
184
 
172
185
  protected isDropdownVisible(): boolean {
@@ -174,17 +187,8 @@ abstract class ToolbarWidget {
174
187
  }
175
188
 
176
189
  private createDropdownIcon(): Element {
177
- const icon = document.createElementNS(svgNamespace, 'svg');
178
- icon.innerHTML = `
179
- <g>
180
- <path
181
- d='M5,10 L50,90 L95,10 Z'
182
- ${primaryForegroundFill}
183
- />
184
- </g>
185
- `;
190
+ const icon = makeDropdownIcon();
186
191
  icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
187
- icon.setAttribute('viewBox', '0 0 100 100');
188
192
  return icon;
189
193
  }
190
194
  }
@@ -194,21 +198,7 @@ class EraserWidget extends ToolbarWidget {
194
198
  return this.localizationTable.eraser;
195
199
  }
196
200
  protected createIcon(): Element {
197
- const icon = document.createElementNS(svgNamespace, 'svg');
198
-
199
- // Draw an eraser-like shape
200
- icon.innerHTML = `
201
- <g>
202
- <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
203
- <rect
204
- x=10 y=10 width=80 height=50
205
- ${primaryForegroundFill}
206
- />
207
- </g>
208
- `;
209
- icon.setAttribute('viewBox', '0 0 100 100');
210
-
211
- return icon;
201
+ return makeEraserIcon();
212
202
  }
213
203
 
214
204
  protected fillDropdown(_dropdown: HTMLElement): boolean {
@@ -229,19 +219,9 @@ class SelectionWidget extends ToolbarWidget {
229
219
  }
230
220
 
231
221
  protected createIcon(): Element {
232
- const icon = document.createElementNS(svgNamespace, 'svg');
233
-
234
- // Draw a cursor-like shape
235
- icon.innerHTML = `
236
- <g>
237
- <rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
238
- <rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
239
- </g>
240
- `;
241
- icon.setAttribute('viewBox', '0 0 100 100');
242
-
243
- return icon;
222
+ return makeSelectionIcon();
244
223
  }
224
+
245
225
  protected fillDropdown(dropdown: HTMLElement): boolean {
246
226
  const container = document.createElement('div');
247
227
  const resizeButton = document.createElement('button');
@@ -284,42 +264,229 @@ class SelectionWidget extends ToolbarWidget {
284
264
  }
285
265
  }
286
266
 
287
- class TouchDrawingWidget extends ToolbarWidget {
267
+ const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
268
+ const zoomLevelRow = document.createElement('div');
269
+
270
+ const increaseButton = document.createElement('button');
271
+ const decreaseButton = document.createElement('button');
272
+ const zoomLevelDisplay = document.createElement('span');
273
+ increaseButton.innerText = '+';
274
+ decreaseButton.innerText = '-';
275
+ zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
276
+
277
+ zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
278
+ zoomLevelDisplay.classList.add('zoomDisplay');
279
+
280
+ let lastZoom: number|undefined;
281
+ const updateZoomDisplay = () => {
282
+ let zoomLevel = editor.viewport.getScaleFactor() * 100;
283
+
284
+ if (zoomLevel > 0.1) {
285
+ zoomLevel = Math.round(zoomLevel * 10) / 10;
286
+ } else {
287
+ zoomLevel = Math.round(zoomLevel * 1000) / 1000;
288
+ }
289
+
290
+ if (zoomLevel !== lastZoom) {
291
+ zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
292
+ lastZoom = zoomLevel;
293
+ }
294
+ };
295
+ updateZoomDisplay();
296
+
297
+ editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
298
+ if (event.kind === EditorEventType.ViewportChanged) {
299
+ updateZoomDisplay();
300
+ }
301
+ });
302
+
303
+ const zoomBy = (factor: number) => {
304
+ const screenCenter = editor.viewport.visibleRect.center;
305
+ const transformUpdate = Mat33.scaling2D(factor, screenCenter);
306
+ editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
307
+ };
308
+
309
+ increaseButton.onclick = () => {
310
+ zoomBy(5.0/4);
311
+ };
312
+
313
+ decreaseButton.onclick = () => {
314
+ zoomBy(4.0/5);
315
+ };
316
+
317
+ return zoomLevelRow;
318
+ };
319
+
320
+ class HandToolWidget extends ToolbarWidget {
321
+ public constructor(
322
+ editor: Editor, protected tool: PanZoom, localizationTable: ToolbarLocalization
323
+ ) {
324
+ super(editor, tool, localizationTable);
325
+ this.container.classList.add('dropdownShowable');
326
+ }
288
327
  protected getTitle(): string {
289
- return this.localizationTable.touchDrawing;
328
+ return this.localizationTable.handTool;
290
329
  }
291
330
 
292
331
  protected createIcon(): Element {
293
- const icon = document.createElementNS(svgNamespace, 'svg');
294
-
295
- // Draw a cursor-like shape
296
- icon.innerHTML = `
297
- <g>
298
- <path d='M11,-30 Q0,10 20,20 Q40,20 40,-30 Z' fill='blue' stroke='black'/>
299
- <path d='
300
- M0,90 L0,50 Q5,40 10,50
301
- L10,20 Q20,15 30,20
302
- L30,50 Q50,40 80,50
303
- L80,90 L10,90 Z'
304
-
305
- ${primaryForegroundStrokeFill}
306
- />
307
- </g>
308
- `;
309
- icon.setAttribute('viewBox', '-10 -30 100 100');
332
+ return makeHandToolIcon();
333
+ }
310
334
 
311
- return icon;
335
+ protected fillDropdown(dropdown: HTMLElement): boolean {
336
+ type OnToggle = (checked: boolean)=>void;
337
+ let idCounter = 0;
338
+ const addCheckbox = (label: string, onToggle: OnToggle) => {
339
+ const rowContainer = document.createElement('div');
340
+ const labelElem = document.createElement('label');
341
+ const checkboxElem = document.createElement('input');
342
+
343
+ checkboxElem.type = 'checkbox';
344
+ checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
345
+ labelElem.setAttribute('for', checkboxElem.id);
346
+
347
+ checkboxElem.oninput = () => {
348
+ onToggle(checkboxElem.checked);
349
+ };
350
+ labelElem.innerText = label;
351
+
352
+ rowContainer.replaceChildren(checkboxElem, labelElem);
353
+ dropdown.appendChild(rowContainer);
354
+
355
+ return checkboxElem;
356
+ };
357
+
358
+ const setModeFlag = (enabled: boolean, flag: PanZoomMode) => {
359
+ const mode = this.tool.getMode();
360
+ if (enabled) {
361
+ this.tool.setMode(mode | flag);
362
+ } else {
363
+ this.tool.setMode(mode & ~flag);
364
+ }
365
+ };
366
+
367
+ const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
368
+ setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
369
+ });
370
+
371
+ const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
372
+ setModeFlag(checked, PanZoomMode.SinglePointerGestures);
373
+ });
374
+
375
+ dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
376
+
377
+ const updateInputs = () => {
378
+ const mode = this.tool.getMode();
379
+ anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
380
+ if (anyDevicePanningCheckbox.checked) {
381
+ touchPanningCheckbox.checked = true;
382
+ touchPanningCheckbox.disabled = true;
383
+ } else {
384
+ touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
385
+ touchPanningCheckbox.disabled = false;
386
+ }
387
+ };
388
+
389
+ updateInputs();
390
+ this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
391
+ if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
392
+ updateInputs();
393
+ }
394
+ });
395
+
396
+ return true;
312
397
  }
313
- protected fillDropdown(_dropdown: HTMLElement): boolean {
314
- // No dropdown
315
- return false;
398
+
399
+ protected updateSelected(_active: boolean) {
316
400
  }
317
- protected updateSelected(active: boolean) {
318
- if (active) {
319
- this.container.classList.remove('selected');
320
- } else {
321
- this.container.classList.add('selected');
322
- }
401
+
402
+ protected handleClick() {
403
+ this.setDropdownVisible(!this.isDropdownVisible());
404
+ }
405
+ }
406
+
407
+ class TextToolWidget extends ToolbarWidget {
408
+ private updateDropdownInputs: (()=>void)|null = null;
409
+ public constructor(editor: Editor, private tool: TextTool, localization: ToolbarLocalization) {
410
+ super(editor, tool, localization);
411
+
412
+ editor.notifier.on(EditorEventType.ToolUpdated, evt => {
413
+ if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
414
+ this.updateIcon();
415
+ this.updateDropdownInputs?.();
416
+ }
417
+ });
418
+ }
419
+
420
+ protected getTitle(): string {
421
+ return this.targetTool.description;
422
+ }
423
+
424
+ protected createIcon(): Element {
425
+ const textStyle = this.tool.getTextStyle();
426
+ return makeTextIcon(textStyle);
427
+ }
428
+
429
+ private static idCounter: number = 0;
430
+ protected fillDropdown(dropdown: HTMLElement): boolean {
431
+ const fontRow = document.createElement('div');
432
+ const colorRow = document.createElement('div');
433
+
434
+ const fontInput = document.createElement('select');
435
+ const fontLabel = document.createElement('label');
436
+
437
+ const colorInput = document.createElement('input');
438
+ const colorLabel = document.createElement('label');
439
+
440
+ const fontsInInput = new Set();
441
+ const addFontToInput = (fontName: string) => {
442
+ const option = document.createElement('option');
443
+ option.value = fontName;
444
+ option.textContent = fontName;
445
+ fontInput.appendChild(option);
446
+ fontsInInput.add(fontName);
447
+ };
448
+
449
+ fontLabel.innerText = this.localizationTable.fontLabel;
450
+ colorLabel.innerText = this.localizationTable.colorLabel;
451
+
452
+ colorInput.classList.add('coloris_input');
453
+ colorInput.type = 'button';
454
+ colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
455
+ colorLabel.setAttribute('for', colorInput.id);
456
+
457
+ addFontToInput('monospace');
458
+ addFontToInput('serif');
459
+ addFontToInput('sans-serif');
460
+ fontInput.id = `${toolbarCSSPrefix}-text-font-input-${TextToolWidget.idCounter++}`;
461
+ fontLabel.setAttribute('for', fontInput.id);
462
+
463
+ fontInput.onchange = () => {
464
+ this.tool.setFontFamily(fontInput.value);
465
+ };
466
+
467
+ colorInput.oninput = () => {
468
+ this.tool.setColor(Color4.fromString(colorInput.value));
469
+ };
470
+
471
+ colorRow.appendChild(colorLabel);
472
+ colorRow.appendChild(colorInput);
473
+
474
+ fontRow.appendChild(fontLabel);
475
+ fontRow.appendChild(fontInput);
476
+
477
+ this.updateDropdownInputs = () => {
478
+ const style = this.tool.getTextStyle();
479
+ colorInput.value = style.renderingStyle.fill.toHexString();
480
+
481
+ if (!fontsInInput.has(style.fontFamily)) {
482
+ addFontToInput(style.fontFamily);
483
+ }
484
+ fontInput.value = style.fontFamily;
485
+ };
486
+ this.updateDropdownInputs();
487
+
488
+ dropdown.replaceChildren(colorRow, fontRow);
489
+ return true;
323
490
  }
324
491
  }
325
492
 
@@ -348,92 +515,17 @@ class PenWidget extends ToolbarWidget {
348
515
  return this.targetTool.description;
349
516
  }
350
517
 
351
- private makePenIcon(elem: SVGSVGElement) {
352
- // Use a square-root scale to prevent the pen's tip from overflowing.
353
- const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
354
- const color = this.tool.getColor();
355
-
356
- // Draw a pen-like shape
357
- const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
358
- const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
359
- elem.innerHTML = `
360
- <defs>
361
- <pattern
362
- id='checkerboard'
363
- viewBox='0,0,10,10'
364
- width='20%'
365
- height='20%'
366
- patternUnits='userSpaceOnUse'
367
- >
368
- <rect x=0 y=0 width=10 height=10 fill='white'/>
369
- <rect x=0 y=0 width=5 height=5 fill='gray'/>
370
- <rect x=5 y=5 width=5 height=5 fill='gray'/>
371
- </pattern>
372
- </defs>
373
- <g>
374
- <!-- Pen grip -->
375
- <path
376
- d='M10,10 L90,10 L90,60 L${50 + scale},80 L${50 - scale},80 L10,60 Z'
377
- ${primaryForegroundStrokeFill}
378
- />
379
- </g>
380
- <g>
381
- <!-- Checkerboard background for slightly transparent pens -->
382
- <path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
383
-
384
- <!-- Actual pen tip -->
385
- <path
386
- d='${primaryStrokeTipPath}'
387
- fill='${color.toHexString()}'
388
- stroke='${color.toHexString()}'
389
- />
390
- </g>
391
- `;
392
- }
393
-
394
- // Draws an icon with the pen.
395
- private makeDrawnIcon(icon: SVGSVGElement) {
396
- const strokeFactory = this.tool.getStrokeFactory();
397
-
398
- const toolThickness = this.tool.getThickness();
399
-
400
- const nowTime = (new Date()).getTime();
401
- const startPoint: StrokeDataPoint = {
402
- pos: Vec2.of(10, 10),
403
- width: toolThickness / 5,
404
- color: this.tool.getColor(),
405
- time: nowTime - 100,
406
- };
407
- const endPoint: StrokeDataPoint = {
408
- pos: Vec2.of(90, 90),
409
- width: toolThickness / 5,
410
- color: this.tool.getColor(),
411
- time: nowTime,
412
- };
413
-
414
- const builder = strokeFactory(startPoint, this.editor.viewport);
415
- builder.addPoint(endPoint);
416
-
417
- const viewport = new Viewport(new EventDispatcher());
418
- viewport.updateScreenSize(Vec2.of(100, 100));
419
- const renderer = new SVGRenderer(icon, viewport);
420
- builder.preview(renderer);
421
- }
422
-
423
518
  protected createIcon(): Element {
424
- // We need to use createElementNS to embed an SVG element in HTML.
425
- // See http://zhangwenli.com/blog/2017/07/26/createelementns/
426
- const icon = document.createElementNS(svgNamespace, 'svg');
427
- icon.setAttribute('viewBox', '0 0 100 100');
428
-
429
519
  const strokeFactory = this.tool.getStrokeFactory();
430
520
  if (strokeFactory === makeFreehandLineBuilder) {
431
- this.makePenIcon(icon);
521
+ // Use a square-root scale to prevent the pen's tip from overflowing.
522
+ const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
523
+ const color = this.tool.getColor();
524
+ return makePenIcon(scale, color.toHexString());
432
525
  } else {
433
- this.makeDrawnIcon(icon);
526
+ const strokeFactory = this.tool.getStrokeFactory();
527
+ return makeIconFromFactory(this.tool, strokeFactory);
434
528
  }
435
-
436
- return icon;
437
529
  }
438
530
 
439
531
  private static idCounter: number = 0;
@@ -614,10 +706,24 @@ export default class HTMLToolbar {
614
706
  });
615
707
  }
616
708
 
617
- public addActionButton(text: string, command: ()=> void, parent?: Element) {
709
+ public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
618
710
  const button = document.createElement('button');
619
- button.innerText = text;
620
711
  button.classList.add(`${toolbarCSSPrefix}toolButton`);
712
+
713
+ if (typeof title === 'string') {
714
+ button.innerText = title;
715
+ } else {
716
+ const iconElem = title.icon.cloneNode(true) as HTMLElement;
717
+ const labelElem = document.createElement('label');
718
+
719
+ // Use the label to describe the icon -- no additional description should be necessary.
720
+ iconElem.setAttribute('alt', '');
721
+ labelElem.innerText = title.label;
722
+ iconElem.classList.add('toolbar-icon');
723
+
724
+ button.replaceChildren(iconElem, labelElem);
725
+ }
726
+
621
727
  button.onclick = command;
622
728
  (parent ?? this.container).appendChild(button);
623
729
 
@@ -628,10 +734,16 @@ export default class HTMLToolbar {
628
734
  const undoRedoGroup = document.createElement('div');
629
735
  undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
630
736
 
631
- const undoButton = this.addActionButton('Undo', () => {
737
+ const undoButton = this.addActionButton({
738
+ label: 'Undo',
739
+ icon: makeUndoIcon()
740
+ }, () => {
632
741
  this.editor.history.undo();
633
742
  }, undoRedoGroup);
634
- const redoButton = this.addActionButton('Redo', () => {
743
+ const redoButton = this.addActionButton({
744
+ label: 'Redo',
745
+ icon: makeRedoIcon(),
746
+ }, () => {
635
747
  this.editor.history.redo();
636
748
  }, undoRedoGroup);
637
749
  this.container.appendChild(undoRedoGroup);
@@ -677,8 +789,20 @@ export default class HTMLToolbar {
677
789
  (new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
678
790
  }
679
791
 
680
- for (const tool of toolController.getMatchingTools(ToolType.TouchPanZoom)) {
681
- (new TouchDrawingWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
792
+ for (const tool of toolController.getMatchingTools(ToolType.Text)) {
793
+ if (!(tool instanceof TextTool)) {
794
+ throw new Error('All text tools must have kind === ToolType.Text');
795
+ }
796
+
797
+ (new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
798
+ }
799
+
800
+ for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
801
+ if (!(tool instanceof PanZoom)) {
802
+ throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
803
+ }
804
+
805
+ (new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
682
806
  }
683
807
 
684
808
  this.setupColorPickers();