js-draw 0.0.3 → 0.0.6

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 (87) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +108 -0
  3. package/build_tools/BundledFile.ts +167 -0
  4. package/build_tools/bundle.ts +11 -0
  5. package/dist/build_tools/BundledFile.d.ts +13 -0
  6. package/dist/build_tools/BundledFile.js +157 -0
  7. package/dist/build_tools/bundle.d.ts +1 -0
  8. package/dist/build_tools/bundle.js +5 -0
  9. package/dist/bundle.js +1 -0
  10. package/dist/src/Display.js +4 -1
  11. package/dist/src/Editor.d.ts +9 -2
  12. package/dist/src/Editor.js +36 -7
  13. package/dist/src/EditorImage.d.ts +4 -2
  14. package/dist/src/EditorImage.js +29 -5
  15. package/dist/src/Pointer.js +1 -1
  16. package/dist/src/SVGLoader.js +3 -1
  17. package/dist/src/Viewport.d.ts +2 -2
  18. package/dist/src/bundle/bundled.d.ts +4 -0
  19. package/dist/src/bundle/bundled.js +5 -0
  20. package/dist/src/components/AbstractComponent.d.ts +1 -1
  21. package/dist/src/components/Stroke.d.ts +1 -1
  22. package/dist/src/components/Stroke.js +1 -1
  23. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  24. package/dist/src/components/builders/ArrowBuilder.d.ts +17 -0
  25. package/dist/src/components/builders/ArrowBuilder.js +83 -0
  26. package/dist/src/{StrokeBuilder.d.ts → components/builders/FreehandLineBuilder.d.ts} +9 -13
  27. package/dist/src/{StrokeBuilder.js → components/builders/FreehandLineBuilder.js} +42 -12
  28. package/dist/src/components/builders/LineBuilder.d.ts +16 -0
  29. package/dist/src/components/builders/LineBuilder.js +57 -0
  30. package/dist/src/components/builders/RectangleBuilder.d.ts +18 -0
  31. package/dist/src/components/builders/RectangleBuilder.js +41 -0
  32. package/dist/src/components/builders/types.d.ts +12 -0
  33. package/dist/src/components/builders/types.js +1 -0
  34. package/dist/src/geometry/Path.d.ts +1 -0
  35. package/dist/src/geometry/Path.js +43 -0
  36. package/dist/src/geometry/Vec3.d.ts +2 -0
  37. package/dist/src/geometry/Vec3.js +13 -0
  38. package/dist/src/localization.d.ts +1 -1
  39. package/dist/src/localization.js +2 -2
  40. package/dist/src/rendering/AbstractRenderer.js +3 -25
  41. package/dist/src/toolbar/HTMLToolbar.d.ts +5 -3
  42. package/dist/src/toolbar/HTMLToolbar.js +139 -30
  43. package/dist/src/toolbar/localization.d.ts +20 -0
  44. package/dist/src/toolbar/localization.js +19 -0
  45. package/dist/src/toolbar/types.d.ts +0 -13
  46. package/dist/src/tools/Pen.d.ts +13 -3
  47. package/dist/src/tools/Pen.js +37 -28
  48. package/dist/src/tools/SelectionTool.js +1 -1
  49. package/dist/src/tools/ToolController.js +3 -3
  50. package/dist/src/types.d.ts +14 -2
  51. package/dist/src/types.js +1 -0
  52. package/dist-test/test-dist-bundle.html +35 -0
  53. package/package.json +15 -5
  54. package/src/Display.ts +3 -1
  55. package/src/Editor.css +0 -1
  56. package/src/Editor.ts +62 -13
  57. package/src/EditorImage.test.ts +5 -3
  58. package/src/EditorImage.ts +31 -3
  59. package/src/Pointer.ts +1 -1
  60. package/src/SVGLoader.ts +5 -1
  61. package/src/Viewport.ts +1 -1
  62. package/src/bundle/bundled.ts +7 -0
  63. package/src/components/AbstractComponent.ts +1 -1
  64. package/src/components/Stroke.ts +2 -2
  65. package/src/components/UnknownSVGObject.ts +1 -1
  66. package/src/components/builders/ArrowBuilder.ts +104 -0
  67. package/src/{StrokeBuilder.ts → components/builders/FreehandLineBuilder.ts} +59 -22
  68. package/src/components/builders/LineBuilder.ts +75 -0
  69. package/src/components/builders/RectangleBuilder.ts +59 -0
  70. package/src/components/builders/types.ts +15 -0
  71. package/src/geometry/Path.fromString.test.ts +11 -24
  72. package/src/geometry/Path.ts +56 -0
  73. package/src/geometry/Vec2.test.ts +1 -0
  74. package/src/geometry/Vec3.test.ts +14 -0
  75. package/src/geometry/Vec3.ts +16 -0
  76. package/src/localization.ts +2 -3
  77. package/src/rendering/AbstractRenderer.ts +3 -32
  78. package/src/{editorStyles.js → styles.js} +0 -0
  79. package/src/toolbar/HTMLToolbar.ts +167 -39
  80. package/src/toolbar/localization.ts +44 -0
  81. package/src/toolbar/toolbar.css +12 -0
  82. package/src/toolbar/types.ts +0 -16
  83. package/src/tools/Pen.ts +56 -34
  84. package/src/tools/SelectionTool.test.ts +1 -1
  85. package/src/tools/SelectionTool.ts +1 -1
  86. package/src/tools/ToolController.ts +3 -3
  87. package/src/types.ts +16 -1
@@ -1,6 +1,6 @@
1
1
  import Editor from '../Editor';
2
2
  import { ToolType } from '../tools/ToolController';
3
- import { EditorEventType } from '../types';
3
+ import { EditorEventType, StrokeDataPoint } from '../types';
4
4
 
5
5
  import { coloris, init as colorisInit } from '@melloware/coloris';
6
6
  import Color4 from '../Color4';
@@ -8,7 +8,16 @@ import Pen from '../tools/Pen';
8
8
  import Eraser from '../tools/Eraser';
9
9
  import BaseTool from '../tools/BaseTool';
10
10
  import SelectionTool from '../tools/SelectionTool';
11
- import { ToolbarLocalization } from './types';
11
+ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
12
+ import { Vec2 } from '../geometry/Vec2';
13
+ import SVGRenderer from '../rendering/SVGRenderer';
14
+ import Viewport from '../Viewport';
15
+ import EventDispatcher from '../EventDispatcher';
16
+ import { ComponentBuilderFactory } from '../components/builders/types';
17
+ import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
18
+ import { makeLineBuilder } from '../components/builders/LineBuilder';
19
+ import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
20
+ import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
12
21
 
13
22
  const primaryForegroundFill = `
14
23
  style='fill: var(--primary-foreground-color);'
@@ -18,6 +27,8 @@ const primaryForegroundStrokeFill = `
18
27
  `;
19
28
 
20
29
  const toolbarCSSPrefix = 'toolbar-';
30
+ const svgNamespace = 'http://www.w3.org/2000/svg';
31
+
21
32
  abstract class ToolbarWidget {
22
33
  protected readonly container: HTMLElement;
23
34
  private button: HTMLElement;
@@ -163,7 +174,7 @@ abstract class ToolbarWidget {
163
174
  }
164
175
 
165
176
  private createDropdownIcon(): Element {
166
- const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
177
+ const icon = document.createElementNS(svgNamespace, 'svg');
167
178
  icon.innerHTML = `
168
179
  <g>
169
180
  <path
@@ -183,9 +194,7 @@ class EraserWidget extends ToolbarWidget {
183
194
  return this.localizationTable.eraser;
184
195
  }
185
196
  protected createIcon(): Element {
186
- const icon = document.createElementNS(
187
- 'http://www.w3.org/2000/svg', 'svg'
188
- );
197
+ const icon = document.createElementNS(svgNamespace, 'svg');
189
198
 
190
199
  // Draw an eraser-like shape
191
200
  icon.innerHTML = `
@@ -220,7 +229,7 @@ class SelectionWidget extends ToolbarWidget {
220
229
  }
221
230
 
222
231
  protected createIcon(): Element {
223
- const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
232
+ const icon = document.createElementNS(svgNamespace, 'svg');
224
233
 
225
234
  // Draw a cursor-like shape
226
235
  icon.innerHTML = `
@@ -270,7 +279,7 @@ class TouchDrawingWidget extends ToolbarWidget {
270
279
  }
271
280
 
272
281
  protected createIcon(): Element {
273
- const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
282
+ const icon = document.createElementNS(svgNamespace, 'svg');
274
283
 
275
284
  // Draw a cursor-like shape
276
285
  icon.innerHTML = `
@@ -307,7 +316,7 @@ class PenWidget extends ToolbarWidget {
307
316
  private updateInputs: ()=> void = () => {};
308
317
 
309
318
  public constructor(
310
- editor: Editor, private tool: Pen, localization: ToolbarLocalization
319
+ editor: Editor, private tool: Pen, localization: ToolbarLocalization, private penTypes: PenTypeRecord[]
311
320
  ) {
312
321
  super(editor, tool, localization);
313
322
 
@@ -328,13 +337,7 @@ class PenWidget extends ToolbarWidget {
328
337
  return this.targetTool.description;
329
338
  }
330
339
 
331
- protected createIcon(): Element {
332
- // We need to use createElementNS to embed an SVG element in HTML.
333
- // See http://zhangwenli.com/blog/2017/07/26/createelementns/
334
- const icon = document.createElementNS(
335
- 'http://www.w3.org/2000/svg', 'svg'
336
- );
337
-
340
+ private makePenIcon(elem: SVGSVGElement) {
338
341
  // Use a square-root scale to prevent the pen's tip from overflowing.
339
342
  const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
340
343
  const color = this.tool.getColor();
@@ -342,7 +345,7 @@ class PenWidget extends ToolbarWidget {
342
345
  // Draw a pen-like shape
343
346
  const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
344
347
  const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
345
- icon.innerHTML = `
348
+ elem.innerHTML = `
346
349
  <defs>
347
350
  <pattern
348
351
  id='checkerboard'
@@ -375,8 +378,50 @@ class PenWidget extends ToolbarWidget {
375
378
  />
376
379
  </g>
377
380
  `;
381
+ }
382
+
383
+ // Draws an icon with the pen.
384
+ private makeDrawnIcon(icon: SVGSVGElement) {
385
+ const strokeFactory = this.tool.getStrokeFactory();
386
+
387
+ const toolThickness = this.tool.getThickness();
388
+
389
+ const nowTime = (new Date()).getTime();
390
+ const startPoint: StrokeDataPoint = {
391
+ pos: Vec2.of(10, 10),
392
+ width: toolThickness / 5,
393
+ color: this.tool.getColor(),
394
+ time: nowTime - 100,
395
+ };
396
+ const endPoint: StrokeDataPoint = {
397
+ pos: Vec2.of(90, 90),
398
+ width: toolThickness / 5,
399
+ color: this.tool.getColor(),
400
+ time: nowTime,
401
+ };
402
+
403
+ const builder = strokeFactory(startPoint, this.editor.viewport);
404
+ builder.addPoint(endPoint);
405
+
406
+ const viewport = new Viewport(new EventDispatcher());
407
+ viewport.updateScreenSize(Vec2.of(100, 100));
408
+ const renderer = new SVGRenderer(icon, viewport);
409
+ builder.preview(renderer);
410
+ }
411
+
412
+ protected createIcon(): Element {
413
+ // We need to use createElementNS to embed an SVG element in HTML.
414
+ // See http://zhangwenli.com/blog/2017/07/26/createelementns/
415
+ const icon = document.createElementNS(svgNamespace, 'svg');
378
416
  icon.setAttribute('viewBox', '0 0 100 100');
379
417
 
418
+ const strokeFactory = this.tool.getStrokeFactory();
419
+ if (strokeFactory === makeFreehandLineBuilder) {
420
+ this.makePenIcon(icon);
421
+ } else {
422
+ this.makeDrawnIcon(icon);
423
+ }
424
+
380
425
  return icon;
381
426
  }
382
427
 
@@ -384,15 +429,23 @@ class PenWidget extends ToolbarWidget {
384
429
  protected fillDropdown(dropdown: HTMLElement): boolean {
385
430
  const container = document.createElement('div');
386
431
 
387
- // Thickness: Value of the input is squared to allow for finer control/larger values.
388
432
  const thicknessRow = document.createElement('div');
433
+ const objectTypeRow = document.createElement('div');
434
+
435
+ // Thickness: Value of the input is squared to allow for finer control/larger values.
389
436
  const thicknessLabel = document.createElement('label');
390
437
  const thicknessInput = document.createElement('input');
438
+ const objectSelectLabel = document.createElement('label');
439
+ const objectTypeSelect = document.createElement('select');
391
440
 
441
+ // Give inputs IDs so we can label them with a <label for=...>Label text</label>
392
442
  thicknessInput.id = `${toolbarCSSPrefix}thicknessInput${PenWidget.idCounter++}`;
443
+ objectTypeSelect.id = `${toolbarCSSPrefix}builderSelect${PenWidget.idCounter++}`;
393
444
 
394
445
  thicknessLabel.innerText = this.localizationTable.thicknessLabel;
395
446
  thicknessLabel.setAttribute('for', thicknessInput.id);
447
+ objectSelectLabel.innerText = this.localizationTable.selectObjectType;
448
+ objectSelectLabel.setAttribute('for', objectTypeSelect.id);
396
449
 
397
450
  thicknessInput.type = 'range';
398
451
  thicknessInput.min = '1';
@@ -404,6 +457,18 @@ class PenWidget extends ToolbarWidget {
404
457
  thicknessRow.appendChild(thicknessLabel);
405
458
  thicknessRow.appendChild(thicknessInput);
406
459
 
460
+ objectTypeSelect.oninput = () => {
461
+ const penTypeIdx = parseInt(objectTypeSelect.value);
462
+ if (penTypeIdx < 0 || penTypeIdx >= this.penTypes.length) {
463
+ console.error('Invalid pen type index', penTypeIdx);
464
+ return;
465
+ }
466
+
467
+ this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
468
+ };
469
+ objectTypeRow.appendChild(objectSelectLabel);
470
+ objectTypeRow.appendChild(objectTypeSelect);
471
+
407
472
  const colorRow = document.createElement('div');
408
473
  const colorLabel = document.createElement('label');
409
474
  const colorInput = document.createElement('input');
@@ -417,6 +482,18 @@ class PenWidget extends ToolbarWidget {
417
482
  colorInput.oninput = () => {
418
483
  this.tool.setColor(Color4.fromHex(colorInput.value));
419
484
  };
485
+ colorInput.addEventListener('open', () => {
486
+ this.editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
487
+ kind: EditorEventType.ColorPickerToggled,
488
+ open: true,
489
+ });
490
+ });
491
+ colorInput.addEventListener('close', () => {
492
+ this.editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
493
+ kind: EditorEventType.ColorPickerToggled,
494
+ open: false,
495
+ });
496
+ });
420
497
 
421
498
  colorRow.appendChild(colorLabel);
422
499
  colorRow.appendChild(colorInput);
@@ -424,45 +501,80 @@ class PenWidget extends ToolbarWidget {
424
501
  this.updateInputs = () => {
425
502
  colorInput.value = this.tool.getColor().toHexString();
426
503
  thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
504
+
505
+ objectTypeSelect.replaceChildren();
506
+ for (let i = 0; i < this.penTypes.length; i ++) {
507
+ const penType = this.penTypes[i];
508
+ const option = document.createElement('option');
509
+ option.value = i.toString();
510
+ option.innerText = penType.name;
511
+
512
+ objectTypeSelect.appendChild(option);
513
+
514
+ if (penType.factory === this.tool.getStrokeFactory()) {
515
+ objectTypeSelect.value = i.toString();
516
+ }
517
+ }
427
518
  };
428
519
  this.updateInputs();
429
520
 
430
- container.replaceChildren(colorRow, thicknessRow);
521
+ container.replaceChildren(colorRow, thicknessRow, objectTypeRow);
431
522
  dropdown.replaceChildren(container);
432
523
  return true;
433
524
  }
434
525
  }
435
526
 
527
+ interface PenTypeRecord {
528
+ name: string;
529
+ factory: ComponentBuilderFactory;
530
+ }
531
+
436
532
  export default class HTMLToolbar {
437
533
  private container: HTMLElement;
438
-
439
- public static defaultLocalization: ToolbarLocalization = {
440
- pen: 'Pen',
441
- eraser: 'Eraser',
442
- select: 'Select',
443
- touchDrawing: 'Touch Drawing',
444
- thicknessLabel: 'Thickness: ',
445
- colorLabel: 'Color: ',
446
- resizeImageToSelection: 'Resize image to selection',
447
- undo: 'Undo',
448
- redo: 'Redo',
449
-
450
- dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
451
- dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
452
- };
534
+ private penTypes: PenTypeRecord[];
453
535
 
454
536
  public constructor(
455
537
  private editor: Editor, parent: HTMLElement,
456
- private localizationTable: ToolbarLocalization = HTMLToolbar.defaultLocalization,
538
+ private localizationTable: ToolbarLocalization = defaultToolbarLocalization,
457
539
  ) {
458
540
  this.container = document.createElement('div');
459
541
  this.container.classList.add(`${toolbarCSSPrefix}root`);
460
542
  this.container.setAttribute('role', 'toolbar');
461
- this.addElements();
462
543
  parent.appendChild(this.container);
463
544
 
464
- // Initialize color choosers
465
545
  colorisInit();
546
+ this.setupColorPickers();
547
+
548
+ // Default pen types
549
+ this.penTypes = [
550
+ {
551
+ name: localizationTable.freehandPen,
552
+ factory: makeFreehandLineBuilder,
553
+ },
554
+ {
555
+ name: localizationTable.arrowPen,
556
+ factory: makeArrowBuilder,
557
+ },
558
+ {
559
+ name: localizationTable.linePen,
560
+ factory: makeLineBuilder,
561
+ },
562
+ {
563
+ name: localizationTable.filledRectanglePen,
564
+ factory: makeFilledRectangleBuilder,
565
+ },
566
+ {
567
+ name: localizationTable.outlinedRectanglePen,
568
+ factory: makeOutlinedRectangleBuilder,
569
+ },
570
+ ];
571
+ }
572
+
573
+ public setupColorPickers() {
574
+ const closePickerOverlay = document.createElement('div');
575
+ closePickerOverlay.className = `${toolbarCSSPrefix}closeColorPickerOverlay`;
576
+ this.editor.createHTMLOverlay(closePickerOverlay);
577
+
466
578
  coloris({
467
579
  el: '.coloris_input',
468
580
  format: 'hex',
@@ -479,6 +591,16 @@ export default class HTMLToolbar {
479
591
  Color4.white.toHexString(),
480
592
  ],
481
593
  });
594
+
595
+ this.editor.notifier.on(EditorEventType.ColorPickerToggled, event => {
596
+ if (event.kind !== EditorEventType.ColorPickerToggled) {
597
+ return;
598
+ }
599
+
600
+ // Show/hide the overlay. Making the overlay visible gives users a surface to click
601
+ // on that shows/hides the color picker.
602
+ closePickerOverlay.style.display = event.open ? 'block' : 'none';
603
+ });
482
604
  }
483
605
 
484
606
  public addActionButton(text: string, command: ()=> void, parent?: Element) {
@@ -515,14 +637,16 @@ export default class HTMLToolbar {
515
637
  });
516
638
  }
517
639
 
518
- private addElements() {
640
+ public addDefaultToolWidgets() {
519
641
  const toolController = this.editor.toolController;
520
642
  for (const tool of toolController.getMatchingTools(ToolType.Pen)) {
521
643
  if (!(tool instanceof Pen)) {
522
644
  throw new Error('All `Pen` tools must have kind === ToolType.Pen');
523
645
  }
524
646
 
525
- const widget = new PenWidget(this.editor, tool, this.localizationTable);
647
+ const widget = new PenWidget(
648
+ this.editor, tool, this.localizationTable, this.penTypes,
649
+ );
526
650
  widget.addTo(this.container);
527
651
  }
528
652
 
@@ -546,6 +670,10 @@ export default class HTMLToolbar {
546
670
  (new TouchDrawingWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
547
671
  }
548
672
 
673
+ this.setupColorPickers();
674
+ }
675
+
676
+ public addDefaultActionButtons() {
549
677
  this.addUndoRedoButtons();
550
678
  }
551
679
  }
@@ -0,0 +1,44 @@
1
+
2
+
3
+ export interface ToolbarLocalization {
4
+ outlinedRectanglePen: string;
5
+ filledRectanglePen: string;
6
+ linePen: string;
7
+ arrowPen: string;
8
+ freehandPen: string;
9
+ selectObjectType: string;
10
+ colorLabel: string;
11
+ pen: string;
12
+ eraser: string;
13
+ select: string;
14
+ touchDrawing: string;
15
+ thicknessLabel: string;
16
+ resizeImageToSelection: string;
17
+ undo: string;
18
+ redo: string;
19
+
20
+ dropdownShown: (toolName: string)=>string;
21
+ dropdownHidden: (toolName: string)=>string;
22
+ }
23
+
24
+ export const defaultToolbarLocalization: ToolbarLocalization = {
25
+ pen: 'Pen',
26
+ eraser: 'Eraser',
27
+ select: 'Select',
28
+ touchDrawing: 'Touch Drawing',
29
+ thicknessLabel: 'Thickness: ',
30
+ colorLabel: 'Color: ',
31
+ resizeImageToSelection: 'Resize image to selection',
32
+ undo: 'Undo',
33
+ redo: 'Redo',
34
+ selectObjectType: 'Object type: ',
35
+
36
+ freehandPen: 'Freehand',
37
+ arrowPen: 'Arrow',
38
+ linePen: 'Line',
39
+ outlinedRectanglePen: 'Outlined rectangle',
40
+ filledRectanglePen: 'Filled rectangle',
41
+
42
+ dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
43
+ dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
44
+ };
@@ -100,6 +100,18 @@
100
100
  flex-direction: row;
101
101
  }
102
102
 
103
+ .toolbar-closeColorPickerOverlay {
104
+ display: none;
105
+ position: fixed;
106
+ top: 0;
107
+ left: 0;
108
+ bottom: 0;
109
+ right: 0;
110
+
111
+ background-color: var(--primary-background-color);
112
+ opacity: 0.3;
113
+ }
114
+
103
115
  /* Make color selection buttons fill their containing label */
104
116
  .toolbar-dropdown .clr-field button {
105
117
  width: 100%;
@@ -2,19 +2,3 @@ export enum ToolbarButtonType {
2
2
  ToggleButton,
3
3
  ActionButton,
4
4
  }
5
-
6
-
7
- export interface ToolbarLocalization {
8
- colorLabel: string;
9
- pen: string;
10
- eraser: string;
11
- select: string;
12
- touchDrawing: string;
13
- thicknessLabel: string;
14
- resizeImageToSelection: string;
15
- undo: string;
16
- redo: string;
17
-
18
- dropdownShown: (toolName: string)=>string;
19
- dropdownHidden: (toolName: string)=>string;
20
- }
package/src/tools/Pen.ts CHANGED
@@ -1,49 +1,60 @@
1
1
  import Color4 from '../Color4';
2
2
  import Editor from '../Editor';
3
3
  import EditorImage from '../EditorImage';
4
- import { Vec2 } from '../geometry/Vec2';
5
4
  import Pointer, { PointerDevice } from '../Pointer';
6
- import StrokeBuilder from '../StrokeBuilder';
7
- import { EditorEventType, PointerEvt } from '../types';
5
+ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
6
+ import { EditorEventType, PointerEvt, StrokeDataPoint } from '../types';
8
7
  import BaseTool from './BaseTool';
9
8
  import { ToolType } from './ToolController';
9
+ import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
10
+
11
+ interface PenStyle {
12
+ color: Color4;
13
+ thickness: number;
14
+ }
10
15
 
11
16
  export default class Pen extends BaseTool {
12
- private builder: StrokeBuilder|null = null;
17
+ private builder: ComponentBuilder|null = null;
18
+ private builderFactory: ComponentBuilderFactory = makeFreehandLineBuilder;
19
+ private lastPoint: StrokeDataPoint|null = null;
20
+
13
21
  public readonly kind: ToolType = ToolType.Pen;
14
22
 
15
23
  public constructor(
16
24
  private editor: Editor,
17
25
  description: string,
18
- private color: Color4 = Color4.purple,
19
- private thickness: number = 16.0,
26
+ private style: PenStyle,
20
27
  ) {
21
28
  super(editor.notifier, description);
22
29
  }
23
30
 
24
31
  private getPressureMultiplier() {
25
- return 1 / this.editor.viewport.getScaleFactor() * this.thickness;
32
+ return 1 / this.editor.viewport.getScaleFactor() * this.style.thickness;
26
33
  }
27
34
 
28
- private getStrokePoint(pointer: Pointer) {
35
+ private getStrokePoint(pointer: Pointer): StrokeDataPoint {
29
36
  const minPressure = 0.3;
30
37
  const pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
31
38
  return {
32
39
  pos: pointer.canvasPos,
33
40
  width: pressure * this.getPressureMultiplier(),
34
- color: this.color,
41
+ color: this.style.color,
35
42
  time: pointer.timeStamp,
36
43
  };
37
44
  }
38
45
 
39
- private addPointToStroke(pointer: Pointer) {
46
+ private previewStroke() {
47
+ this.editor.clearWetInk();
48
+ this.builder?.preview(this.editor.display.getWetInkRenderer());
49
+ }
50
+
51
+ private addPointToStroke(point: StrokeDataPoint) {
40
52
  if (!this.builder) {
41
53
  throw new Error('No stroke is currently being generated.');
42
54
  }
43
- this.builder.addPoint(this.getStrokePoint(pointer));
44
-
45
- this.editor.clearWetInk();
46
- this.editor.drawWetInk(...this.builder.preview());
55
+ this.builder.addPoint(point);
56
+ this.lastPoint = point;
57
+ this.previewStroke();
47
58
  }
48
59
 
49
60
  public onPointerDown({ current, allPointers }: PointerEvt): boolean {
@@ -52,15 +63,7 @@ export default class Pen extends BaseTool {
52
63
  }
53
64
 
54
65
  if (allPointers.length === 1 || current.device === PointerDevice.Pen) {
55
- // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
56
- // less than ± 2 px from the curve.
57
- const canvasTransform = this.editor.viewport.screenToCanvasTransform;
58
- const maxSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 7;
59
- const minSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 2;
60
-
61
- this.builder = new StrokeBuilder(
62
- this.getStrokePoint(current), minSmoothingDist, maxSmoothingDist
63
- );
66
+ this.builder = this.builderFactory(this.getStrokePoint(current), this.editor.viewport);
64
67
  return true;
65
68
  }
66
69
 
@@ -68,7 +71,7 @@ export default class Pen extends BaseTool {
68
71
  }
69
72
 
70
73
  public onPointerMove({ current }: PointerEvt): void {
71
- this.addPointToStroke(current);
74
+ this.addPointToStroke(this.getStrokePoint(current));
72
75
  }
73
76
 
74
77
  public onPointerUp({ current }: PointerEvt): void {
@@ -76,12 +79,17 @@ export default class Pen extends BaseTool {
76
79
  return;
77
80
  }
78
81
 
79
- this.addPointToStroke(current);
82
+ // onPointerUp events can have zero pressure. Use the last pressure instead.
83
+ const currentPoint = this.getStrokePoint(current);
84
+ const strokePoint = {
85
+ ...currentPoint,
86
+ width: this.lastPoint?.width ?? currentPoint.width,
87
+ };
88
+
89
+ this.addPointToStroke(strokePoint);
80
90
  if (this.builder && current.isPrimary) {
81
91
  const stroke = this.builder.build();
82
-
83
- this.editor.clearWetInk();
84
- this.editor.drawWetInk(...this.builder.preview());
92
+ this.previewStroke();
85
93
 
86
94
  const canFlatten = true;
87
95
  const action = new EditorImage.AddElementCommand(stroke, canFlatten);
@@ -103,19 +111,33 @@ export default class Pen extends BaseTool {
103
111
  }
104
112
 
105
113
  public setColor(color: Color4): void {
106
- if (color.toHexString() !== this.color.toHexString()) {
107
- this.color = color;
114
+ if (color.toHexString() !== this.style.color.toHexString()) {
115
+ this.style = {
116
+ ...this.style,
117
+ color,
118
+ };
108
119
  this.noteUpdated();
109
120
  }
110
121
  }
111
122
 
112
123
  public setThickness(thickness: number) {
113
- if (thickness !== this.thickness) {
114
- this.thickness = thickness;
124
+ if (thickness !== this.style.thickness) {
125
+ this.style = {
126
+ ...this.style,
127
+ thickness,
128
+ };
129
+ this.noteUpdated();
130
+ }
131
+ }
132
+
133
+ public setStrokeFactory(factory: ComponentBuilderFactory) {
134
+ if (factory !== this.builderFactory) {
135
+ this.builderFactory = factory;
115
136
  this.noteUpdated();
116
137
  }
117
138
  }
118
139
 
119
- public getThickness() { return this.thickness; }
120
- public getColor() { return this.color; }
140
+ public getThickness() { return this.style.thickness; }
141
+ public getColor() { return this.style.color; }
142
+ public getStrokeFactory() { return this.builderFactory; }
121
143
  }
@@ -15,7 +15,7 @@ const getSelectionTool = (editor: Editor): SelectionTool => {
15
15
  return editor.toolController.getMatchingTools(ToolType.Selection)[0] as SelectionTool;
16
16
  };
17
17
 
18
- const createEditor = () => new Editor(document.body, RenderingMode.DummyRenderer);
18
+ const createEditor = () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
19
19
 
20
20
  const createSquareStroke = () => {
21
21
  const testStroke = new Stroke([
@@ -16,7 +16,7 @@ const styles = `
16
16
  }
17
17
 
18
18
  .handleOverlay > .selectionBox {
19
- position: fixed;
19
+ position: absolute;
20
20
  z-index: 0;
21
21
  transform-origin: center;
22
22
  }
@@ -24,17 +24,17 @@ export default class ToolController {
24
24
  public constructor(editor: Editor, localization: ToolLocalization) {
25
25
  const primaryToolEnabledGroup = new ToolEnabledGroup();
26
26
  const touchPanZoom = new PanZoom(editor, PanZoomMode.OneFingerGestures, localization.touchPanTool);
27
- const primaryPenTool = new Pen(editor, localization.penTool(1));
27
+ const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
28
28
  const primaryTools = [
29
29
  new SelectionTool(editor, localization.selectionTool),
30
30
  new Eraser(editor, localization.eraserTool),
31
31
 
32
32
  // Three pens
33
33
  primaryPenTool,
34
- new Pen(editor, localization.penTool(2), Color4.clay, 8),
34
+ new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 8 }),
35
35
 
36
36
  // Highlighter-like pen with width=64
37
- new Pen(editor, localization.penTool(3), Color4.ofRGBA(1, 1, 0, 0.5), 64),
37
+ new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
38
38
  ];
39
39
  this.tools = [
40
40
  touchPanZoom,