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
@@ -0,0 +1,198 @@
1
+ import EventDispatcher from '../EventDispatcher';
2
+ import { Vec2 } from '../geometry/Vec2';
3
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
4
+ import Viewport from '../Viewport';
5
+ const svgNamespace = 'http://www.w3.org/2000/svg';
6
+ const primaryForegroundFill = `
7
+ style='fill: var(--primary-foreground-color);'
8
+ `;
9
+ const primaryForegroundStrokeFill = `
10
+ style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
11
+ `;
12
+ export const makeUndoIcon = () => {
13
+ return makeRedoIcon(true);
14
+ };
15
+ export const makeRedoIcon = (mirror = false) => {
16
+ const icon = document.createElementNS(svgNamespace, 'svg');
17
+ icon.innerHTML = `
18
+ <style>
19
+ .toolbar-svg-undo-redo-icon {
20
+ stroke: var(--primary-foreground-color);
21
+ stroke-width: 12;
22
+ stroke-linejoin: round;
23
+ stroke-linecap: round;
24
+ fill: none;
25
+
26
+ transform-origin: center;
27
+ }
28
+ </style>
29
+ <path
30
+ d='M20,20 A15,15 0 0 1 70,80 L80,90 L60,70 L65,90 L87,90 L65,80'
31
+ class='toolbar-svg-undo-redo-icon'
32
+ style='${mirror ? 'transform: scale(-1, 1);' : ''}'/>
33
+ `;
34
+ icon.setAttribute('viewBox', '0 0 100 100');
35
+ return icon;
36
+ };
37
+ export const makeDropdownIcon = () => {
38
+ const icon = document.createElementNS(svgNamespace, 'svg');
39
+ icon.innerHTML = `
40
+ <g>
41
+ <path
42
+ d='M5,10 L50,90 L95,10 Z'
43
+ ${primaryForegroundFill}
44
+ />
45
+ </g>
46
+ `;
47
+ icon.setAttribute('viewBox', '0 0 100 100');
48
+ return icon;
49
+ };
50
+ export const makeEraserIcon = () => {
51
+ const icon = document.createElementNS(svgNamespace, 'svg');
52
+ // Draw an eraser-like shape
53
+ icon.innerHTML = `
54
+ <g>
55
+ <rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
56
+ <rect
57
+ x=10 y=10 width=80 height=50
58
+ ${primaryForegroundFill}
59
+ />
60
+ </g>
61
+ `;
62
+ icon.setAttribute('viewBox', '0 0 100 100');
63
+ return icon;
64
+ };
65
+ export const makeSelectionIcon = () => {
66
+ const icon = document.createElementNS(svgNamespace, 'svg');
67
+ // Draw a cursor-like shape
68
+ icon.innerHTML = `
69
+ <g>
70
+ <rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
71
+ <rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
72
+ </g>
73
+ `;
74
+ icon.setAttribute('viewBox', '0 0 100 100');
75
+ return icon;
76
+ };
77
+ export const makeHandToolIcon = () => {
78
+ const icon = document.createElementNS(svgNamespace, 'svg');
79
+ // Draw a cursor-like shape
80
+ icon.innerHTML = `
81
+ <g>
82
+ <path d='
83
+ m 10,60
84
+ 5,30
85
+ H 90
86
+ V 30
87
+ C 90,20 75,20 75,30
88
+ V 60
89
+ 20
90
+ C 75,10 60,10 60,20
91
+ V 60
92
+ 15
93
+ C 60,5 45,5 45,15
94
+ V 60
95
+ 25
96
+ C 45,15 30,15 30,25
97
+ V 60
98
+ 75
99
+ L 25,60
100
+ C 20,45 10,50 10,60
101
+ Z'
102
+
103
+ fill='none'
104
+ style='
105
+ stroke: var(--primary-foreground-color);
106
+ stroke-width: 2;
107
+ '
108
+ />
109
+ </g>
110
+ `;
111
+ icon.setAttribute('viewBox', '0 0 100 100');
112
+ return icon;
113
+ };
114
+ export const makeTextIcon = (textStyle) => {
115
+ var _a, _b;
116
+ const icon = document.createElementNS(svgNamespace, 'svg');
117
+ icon.setAttribute('viewBox', '0 0 100 100');
118
+ const textNode = document.createElementNS(svgNamespace, 'text');
119
+ textNode.appendChild(document.createTextNode('T'));
120
+ textNode.style.fontFamily = textStyle.fontFamily;
121
+ textNode.style.fontWeight = (_a = textStyle.fontWeight) !== null && _a !== void 0 ? _a : '';
122
+ textNode.style.fontVariant = (_b = textStyle.fontVariant) !== null && _b !== void 0 ? _b : '';
123
+ textNode.style.fill = textStyle.renderingStyle.fill.toHexString();
124
+ textNode.style.textAnchor = 'middle';
125
+ textNode.setAttribute('x', '50');
126
+ textNode.setAttribute('y', '75');
127
+ textNode.style.fontSize = '65px';
128
+ textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
129
+ icon.appendChild(textNode);
130
+ return icon;
131
+ };
132
+ export const makePenIcon = (tipThickness, color) => {
133
+ const icon = document.createElementNS(svgNamespace, 'svg');
134
+ icon.setAttribute('viewBox', '0 0 100 100');
135
+ const halfThickness = tipThickness / 2;
136
+ // Draw a pen-like shape
137
+ const primaryStrokeTipPath = `M14,63 L${50 - halfThickness},95 L${50 + halfThickness},90 L88,60 Z`;
138
+ const backgroundStrokeTipPath = `M14,63 L${50 - halfThickness},85 L${50 + halfThickness},83 L88,60 Z`;
139
+ icon.innerHTML = `
140
+ <defs>
141
+ <pattern
142
+ id='checkerboard'
143
+ viewBox='0,0,10,10'
144
+ width='20%'
145
+ height='20%'
146
+ patternUnits='userSpaceOnUse'
147
+ >
148
+ <rect x=0 y=0 width=10 height=10 fill='white'/>
149
+ <rect x=0 y=0 width=5 height=5 fill='gray'/>
150
+ <rect x=5 y=5 width=5 height=5 fill='gray'/>
151
+ </pattern>
152
+ </defs>
153
+ <g>
154
+ <!-- Pen grip -->
155
+ <path
156
+ d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
157
+ ${primaryForegroundStrokeFill}
158
+ />
159
+ </g>
160
+ <g>
161
+ <!-- Checkerboard background for slightly transparent pens -->
162
+ <path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
163
+
164
+ <!-- Actual pen tip -->
165
+ <path
166
+ d='${primaryStrokeTipPath}'
167
+ fill='${color}'
168
+ stroke='${color}'
169
+ />
170
+ </g>
171
+ `;
172
+ return icon;
173
+ };
174
+ export const makeIconFromFactory = (pen, factory) => {
175
+ const toolThickness = pen.getThickness();
176
+ const nowTime = (new Date()).getTime();
177
+ const startPoint = {
178
+ pos: Vec2.of(10, 10),
179
+ width: toolThickness / 5,
180
+ color: pen.getColor(),
181
+ time: nowTime - 100,
182
+ };
183
+ const endPoint = {
184
+ pos: Vec2.of(90, 90),
185
+ width: toolThickness / 5,
186
+ color: pen.getColor(),
187
+ time: nowTime,
188
+ };
189
+ const viewport = new Viewport(new EventDispatcher());
190
+ const builder = factory(startPoint, viewport);
191
+ builder.addPoint(endPoint);
192
+ const icon = document.createElementNS(svgNamespace, 'svg');
193
+ icon.setAttribute('viewBox', '0 0 100 100');
194
+ viewport.updateScreenSize(Vec2.of(100, 100));
195
+ const renderer = new SVGRenderer(icon, viewport);
196
+ builder.preview(renderer);
197
+ return icon;
198
+ };
@@ -1,4 +1,7 @@
1
1
  export interface ToolbarLocalization {
2
+ fontLabel: string;
3
+ anyDevicePanning: string;
4
+ touchPanning: string;
2
5
  outlinedRectanglePen: string;
3
6
  filledRectanglePen: string;
4
7
  linePen: string;
@@ -9,7 +12,7 @@ export interface ToolbarLocalization {
9
12
  pen: string;
10
13
  eraser: string;
11
14
  select: string;
12
- touchDrawing: string;
15
+ handTool: string;
13
16
  thicknessLabel: string;
14
17
  resizeImageToSelection: string;
15
18
  deleteSelection: string;
@@ -17,5 +20,6 @@ export interface ToolbarLocalization {
17
20
  redo: string;
18
21
  dropdownShown: (toolName: string) => string;
19
22
  dropdownHidden: (toolName: string) => string;
23
+ zoomLevel: (zoomPercentage: number) => string;
20
24
  }
21
25
  export declare const defaultToolbarLocalization: ToolbarLocalization;
@@ -2,14 +2,17 @@ export const defaultToolbarLocalization = {
2
2
  pen: 'Pen',
3
3
  eraser: 'Eraser',
4
4
  select: 'Select',
5
- touchDrawing: 'Touch Drawing',
5
+ handTool: 'Pan',
6
6
  thicknessLabel: 'Thickness: ',
7
7
  colorLabel: 'Color: ',
8
+ fontLabel: 'Font: ',
8
9
  resizeImageToSelection: 'Resize image to selection',
9
10
  deleteSelection: 'Delete selection',
10
11
  undo: 'Undo',
11
12
  redo: 'Redo',
12
13
  selectObjectType: 'Object type: ',
14
+ touchPanning: 'Touchscreen panning',
15
+ anyDevicePanning: 'Any device panning',
13
16
  freehandPen: 'Freehand',
14
17
  arrowPen: 'Arrow',
15
18
  linePen: 'Line',
@@ -17,4 +20,5 @@ export const defaultToolbarLocalization = {
17
20
  filledRectanglePen: 'Filled rectangle',
18
21
  dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
19
22
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
23
+ zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
20
24
  };
@@ -2,3 +2,7 @@ export declare enum ToolbarButtonType {
2
2
  ToggleButton = 0,
3
3
  ActionButton = 1
4
4
  }
5
+ export interface ActionButtonIcon {
6
+ icon: Element;
7
+ label: string;
8
+ }
@@ -11,22 +11,23 @@ interface PinchData {
11
11
  dist: number;
12
12
  }
13
13
  export declare enum PanZoomMode {
14
- OneFingerGestures = 1,
15
- TwoFingerGestures = 2,
16
- AnyDevice = 4
14
+ OneFingerTouchGestures = 1,
15
+ TwoFingerTouchGestures = 2,
16
+ RightClickDrags = 4,
17
+ SinglePointerGestures = 8
17
18
  }
18
19
  export default class PanZoom extends BaseTool {
19
20
  private editor;
20
21
  private mode;
21
- readonly kind: ToolType.PanZoom | ToolType.TouchPanZoom;
22
+ readonly kind: ToolType.PanZoom;
22
23
  private transform;
23
24
  private lastAngle;
24
25
  private lastDist;
25
26
  private lastScreenCenter;
26
27
  constructor(editor: Editor, mode: PanZoomMode, description: string);
27
28
  computePinchData(p1: Pointer, p2: Pointer): PinchData;
28
- private pointersHaveCorrectDeviceType;
29
- onPointerDown({ allPointers }: PointerEvt): boolean;
29
+ private allPointersAreOfType;
30
+ onPointerDown({ allPointers: pointers }: PointerEvt): boolean;
30
31
  private getCenterDelta;
31
32
  private handleTwoFingerMove;
32
33
  private handleOneFingerMove;
@@ -36,5 +37,7 @@ export default class PanZoom extends BaseTool {
36
37
  private updateTransform;
37
38
  onWheel({ delta, screenPos }: WheelEvt): boolean;
38
39
  onKeyPress({ key }: KeyPressEvent): boolean;
40
+ setMode(mode: PanZoomMode): void;
41
+ getMode(): PanZoomMode;
39
42
  }
40
43
  export {};
@@ -2,17 +2,16 @@ import Mat33 from '../geometry/Mat33';
2
2
  import { Vec2 } from '../geometry/Vec2';
3
3
  import Vec3 from '../geometry/Vec3';
4
4
  import { PointerDevice } from '../Pointer';
5
+ import { EditorEventType } from '../types';
5
6
  import { Viewport } from '../Viewport';
6
7
  import BaseTool from './BaseTool';
7
8
  import { ToolType } from './ToolController';
8
9
  export var PanZoomMode;
9
10
  (function (PanZoomMode) {
10
- // Handle one-pointer gestures (touchscreen only unless AnyDevice is set)
11
- PanZoomMode[PanZoomMode["OneFingerGestures"] = 1] = "OneFingerGestures";
12
- // Handle two-pointer gestures (touchscreen only unless AnyDevice is set)
13
- PanZoomMode[PanZoomMode["TwoFingerGestures"] = 2] = "TwoFingerGestures";
14
- // / Handle gestures from any device, rather than just touch
15
- PanZoomMode[PanZoomMode["AnyDevice"] = 4] = "AnyDevice";
11
+ PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
12
+ PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
13
+ PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
14
+ PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
16
15
  })(PanZoomMode || (PanZoomMode = {}));
17
16
  export default class PanZoom extends BaseTool {
18
17
  constructor(editor, mode, description) {
@@ -21,9 +20,6 @@ export default class PanZoom extends BaseTool {
21
20
  this.mode = mode;
22
21
  this.kind = ToolType.PanZoom;
23
22
  this.transform = null;
24
- if (mode === PanZoomMode.OneFingerGestures) {
25
- this.kind = ToolType.TouchPanZoom;
26
- }
27
23
  }
28
24
  // Returns information about the pointers in a gesture
29
25
  computePinchData(p1, p2) {
@@ -34,24 +30,25 @@ export default class PanZoom extends BaseTool {
34
30
  const screenCenter = p2.screenPos.plus(p1.screenPos).times(0.5);
35
31
  return { canvasCenter, screenCenter, angle, dist };
36
32
  }
37
- pointersHaveCorrectDeviceType(pointers) {
38
- return this.mode & PanZoomMode.AnyDevice || pointers.every(pointer => pointer.device === PointerDevice.Touch);
33
+ allPointersAreOfType(pointers, kind) {
34
+ return pointers.every(pointer => pointer.device === kind);
39
35
  }
40
- onPointerDown({ allPointers }) {
36
+ onPointerDown({ allPointers: pointers }) {
41
37
  var _a;
42
38
  let handlingGesture = false;
43
- if (!this.pointersHaveCorrectDeviceType(allPointers)) {
44
- handlingGesture = false;
45
- }
46
- else if (allPointers.length === 2 && this.mode & PanZoomMode.TwoFingerGestures) {
47
- const { screenCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
39
+ const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
40
+ const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
41
+ if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
42
+ const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
48
43
  this.lastAngle = angle;
49
44
  this.lastDist = dist;
50
45
  this.lastScreenCenter = screenCenter;
51
46
  handlingGesture = true;
52
47
  }
53
- else if (allPointers.length === 1 && this.mode & PanZoomMode.OneFingerGestures) {
54
- this.lastScreenCenter = allPointers[0].screenPos;
48
+ else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
49
+ || (isRightClick && this.mode & PanZoomMode.RightClickDrags)
50
+ || (this.mode & PanZoomMode.SinglePointerGestures))) {
51
+ this.lastScreenCenter = pointers[0].screenPos;
55
52
  handlingGesture = true;
56
53
  }
57
54
  if (handlingGesture) {
@@ -87,10 +84,10 @@ export default class PanZoom extends BaseTool {
87
84
  var _a;
88
85
  (_a = this.transform) !== null && _a !== void 0 ? _a : (this.transform = new Viewport.ViewportTransform(Mat33.identity));
89
86
  const lastTransform = this.transform;
90
- if (allPointers.length === 2 && this.mode & PanZoomMode.TwoFingerGestures) {
87
+ if (allPointers.length === 2) {
91
88
  this.handleTwoFingerMove(allPointers);
92
89
  }
93
- else if (allPointers.length === 1 && this.mode & PanZoomMode.OneFingerGestures) {
90
+ else if (allPointers.length === 1) {
94
91
  this.handleOneFingerMove(allPointers[0]);
95
92
  }
96
93
  lastTransform.unapply(this.editor);
@@ -191,4 +188,16 @@ export default class PanZoom extends BaseTool {
191
188
  this.updateTransform(transformUpdate);
192
189
  return true;
193
190
  }
191
+ setMode(mode) {
192
+ if (mode !== this.mode) {
193
+ this.mode = mode;
194
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
195
+ kind: EditorEventType.ToolUpdated,
196
+ tool: this,
197
+ });
198
+ }
199
+ }
200
+ getMode() {
201
+ return this.mode;
202
+ }
194
203
  }
@@ -66,9 +66,14 @@ export default class Pen extends BaseTool {
66
66
  if (this.builder && current.isPrimary) {
67
67
  const stroke = this.builder.build();
68
68
  this.previewStroke();
69
- const canFlatten = true;
70
- const action = new EditorImage.AddElementCommand(stroke, canFlatten);
71
- this.editor.dispatch(action);
69
+ if (stroke.getBBox().area > 0) {
70
+ const canFlatten = true;
71
+ const action = new EditorImage.AddElementCommand(stroke, canFlatten);
72
+ this.editor.dispatch(action);
73
+ }
74
+ else {
75
+ console.warn('Pen: Not adding empty stroke', stroke, 'to the canvas.');
76
+ }
72
77
  }
73
78
  this.builder = null;
74
79
  this.editor.clearWetInk();
@@ -396,7 +396,7 @@ export default class SelectionTool extends BaseTool {
396
396
  if (hasSelection) {
397
397
  this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
398
398
  const selectionRect = this.selectionBox.region;
399
- this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
399
+ this.editor.viewport.zoomTo(selectionRect, false).apply(this.editor);
400
400
  }
401
401
  }
402
402
  onPointerUp(event) {
@@ -0,0 +1,30 @@
1
+ import Color4 from '../Color4';
2
+ import { TextStyle } from '../components/Text';
3
+ import Editor from '../Editor';
4
+ import { PointerEvt } from '../types';
5
+ import BaseTool from './BaseTool';
6
+ import { ToolLocalization } from './localization';
7
+ import { ToolType } from './ToolController';
8
+ export default class TextTool extends BaseTool {
9
+ private editor;
10
+ private localizationTable;
11
+ kind: ToolType;
12
+ private textStyle;
13
+ private textEditOverlay;
14
+ private textInputElem;
15
+ private textTargetPosition;
16
+ private textMeasuringCtx;
17
+ constructor(editor: Editor, description: string, localizationTable: ToolLocalization);
18
+ private getTextAscent;
19
+ private flushInput;
20
+ private updateTextInput;
21
+ private startTextInput;
22
+ setEnabled(enabled: boolean): void;
23
+ onPointerDown({ current, allPointers }: PointerEvt): boolean;
24
+ onGestureCancel(): void;
25
+ private dispatchUpdateEvent;
26
+ setFontFamily(fontFamily: string): void;
27
+ setColor(color: Color4): void;
28
+ setFontSize(size: number): void;
29
+ getTextStyle(): TextStyle;
30
+ }
@@ -0,0 +1,173 @@
1
+ import Color4 from '../Color4';
2
+ import Text from '../components/Text';
3
+ import EditorImage from '../EditorImage';
4
+ import Mat33 from '../geometry/Mat33';
5
+ import { PointerDevice } from '../Pointer';
6
+ import { EditorEventType } from '../types';
7
+ import BaseTool from './BaseTool';
8
+ import { ToolType } from './ToolController';
9
+ const overlayCssClass = 'textEditorOverlay';
10
+ export default class TextTool extends BaseTool {
11
+ constructor(editor, description, localizationTable) {
12
+ super(editor.notifier, description);
13
+ this.editor = editor;
14
+ this.localizationTable = localizationTable;
15
+ this.kind = ToolType.Text;
16
+ this.textInputElem = null;
17
+ this.textTargetPosition = null;
18
+ this.textMeasuringCtx = null;
19
+ this.textStyle = {
20
+ size: 32,
21
+ fontFamily: 'sans-serif',
22
+ renderingStyle: {
23
+ fill: Color4.purple,
24
+ },
25
+ };
26
+ this.textEditOverlay = document.createElement('div');
27
+ this.textEditOverlay.classList.add(overlayCssClass);
28
+ this.editor.addStyleSheet(`
29
+ .${overlayCssClass} {
30
+ height: 0;
31
+ overflow: visible;
32
+ }
33
+
34
+ .${overlayCssClass} input {
35
+ background-color: rgba(0, 0, 0, 0);
36
+ border: none;
37
+ padding: 0;
38
+ }
39
+ `);
40
+ this.editor.createHTMLOverlay(this.textEditOverlay);
41
+ this.editor.notifier.on(EditorEventType.ViewportChanged, () => this.updateTextInput());
42
+ }
43
+ getTextAscent(text, style) {
44
+ var _a;
45
+ (_a = this.textMeasuringCtx) !== null && _a !== void 0 ? _a : (this.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
46
+ if (this.textMeasuringCtx) {
47
+ Text.applyTextStyles(this.textMeasuringCtx, style);
48
+ return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
49
+ }
50
+ // Estimate
51
+ return style.size * 2 / 3;
52
+ }
53
+ flushInput() {
54
+ if (this.textInputElem && this.textTargetPosition) {
55
+ const content = this.textInputElem.value;
56
+ this.textInputElem.remove();
57
+ this.textInputElem = null;
58
+ if (content === '') {
59
+ return;
60
+ }
61
+ const textTransform = Mat33.translation(this.textTargetPosition).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas()));
62
+ const textComponent = new Text([content], textTransform, this.textStyle);
63
+ const action = new EditorImage.AddElementCommand(textComponent);
64
+ this.editor.dispatch(action);
65
+ }
66
+ }
67
+ updateTextInput() {
68
+ var _a, _b, _c;
69
+ if (!this.textInputElem || !this.textTargetPosition) {
70
+ (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
71
+ return;
72
+ }
73
+ const viewport = this.editor.viewport;
74
+ const textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
75
+ this.textInputElem.type = 'text';
76
+ this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
77
+ this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
78
+ this.textInputElem.style.fontVariant = (_b = this.textStyle.fontVariant) !== null && _b !== void 0 ? _b : '';
79
+ this.textInputElem.style.fontWeight = (_c = this.textStyle.fontWeight) !== null && _c !== void 0 ? _c : '';
80
+ this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
81
+ this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
82
+ this.textInputElem.style.position = 'relative';
83
+ this.textInputElem.style.left = `${textScreenPos.x}px`;
84
+ this.textInputElem.style.top = `${textScreenPos.y}px`;
85
+ this.textInputElem.style.margin = '0';
86
+ const rotation = viewport.getRotationAngle();
87
+ const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
88
+ this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
89
+ this.textInputElem.style.transformOrigin = 'top left';
90
+ }
91
+ startTextInput(textCanvasPos, initialText) {
92
+ this.flushInput();
93
+ this.textInputElem = document.createElement('input');
94
+ this.textInputElem.value = initialText;
95
+ this.textTargetPosition = textCanvasPos;
96
+ this.updateTextInput();
97
+ this.textInputElem.oninput = () => {
98
+ var _a;
99
+ if (this.textInputElem) {
100
+ this.textInputElem.size = ((_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.value.length) || 10;
101
+ }
102
+ };
103
+ this.textInputElem.onblur = () => {
104
+ // Don't remove the input within the context of a blur event handler.
105
+ // Doing so causes errors.
106
+ setTimeout(() => this.flushInput(), 0);
107
+ };
108
+ this.textInputElem.onkeyup = (evt) => {
109
+ var _a;
110
+ if (evt.key === 'Enter') {
111
+ this.flushInput();
112
+ this.editor.focus();
113
+ }
114
+ else if (evt.key === 'Escape') {
115
+ // Cancel input.
116
+ (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
117
+ this.textInputElem = null;
118
+ this.editor.focus();
119
+ }
120
+ };
121
+ this.textEditOverlay.replaceChildren(this.textInputElem);
122
+ setTimeout(() => { var _a; return (_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
123
+ }
124
+ setEnabled(enabled) {
125
+ super.setEnabled(enabled);
126
+ if (!enabled) {
127
+ this.flushInput();
128
+ }
129
+ this.textEditOverlay.style.display = enabled ? 'block' : 'none';
130
+ }
131
+ onPointerDown({ current, allPointers }) {
132
+ if (current.device === PointerDevice.Eraser) {
133
+ return false;
134
+ }
135
+ if (allPointers.length === 1) {
136
+ this.startTextInput(current.canvasPos, '');
137
+ return true;
138
+ }
139
+ return false;
140
+ }
141
+ onGestureCancel() {
142
+ this.flushInput();
143
+ this.editor.focus();
144
+ }
145
+ dispatchUpdateEvent() {
146
+ this.updateTextInput();
147
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
148
+ kind: EditorEventType.ToolUpdated,
149
+ tool: this,
150
+ });
151
+ }
152
+ setFontFamily(fontFamily) {
153
+ if (fontFamily !== this.textStyle.fontFamily) {
154
+ this.textStyle = Object.assign(Object.assign({}, this.textStyle), { fontFamily: fontFamily });
155
+ this.dispatchUpdateEvent();
156
+ }
157
+ }
158
+ setColor(color) {
159
+ if (!color.eq(this.textStyle.renderingStyle.fill)) {
160
+ this.textStyle = Object.assign(Object.assign({}, this.textStyle), { renderingStyle: Object.assign(Object.assign({}, this.textStyle.renderingStyle), { fill: color }) });
161
+ this.dispatchUpdateEvent();
162
+ }
163
+ }
164
+ setFontSize(size) {
165
+ if (size !== this.textStyle.size) {
166
+ this.textStyle = Object.assign(Object.assign({}, this.textStyle), { size });
167
+ this.dispatchUpdateEvent();
168
+ }
169
+ }
170
+ getTextStyle() {
171
+ return this.textStyle;
172
+ }
173
+ }
@@ -3,11 +3,11 @@ import Editor from '../Editor';
3
3
  import BaseTool from './BaseTool';
4
4
  import { ToolLocalization } from './localization';
5
5
  export declare enum ToolType {
6
- TouchPanZoom = 0,
7
- Pen = 1,
8
- Selection = 2,
9
- Eraser = 3,
10
- PanZoom = 4,
6
+ Pen = 0,
7
+ Selection = 1,
8
+ Eraser = 2,
9
+ PanZoom = 3,
10
+ Text = 4,
11
11
  UndoRedoShortcut = 5
12
12
  }
13
13
  export default class ToolController {