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,225 @@
1
+ import Color4 from '../Color4';
2
+ import Text, { TextStyle } from '../components/Text';
3
+ import Editor from '../Editor';
4
+ import EditorImage from '../EditorImage';
5
+ import Mat33 from '../geometry/Mat33';
6
+ import { Vec2 } from '../geometry/Vec2';
7
+ import { PointerDevice } from '../Pointer';
8
+ import { EditorEventType, PointerEvt } from '../types';
9
+ import BaseTool from './BaseTool';
10
+ import { ToolLocalization } from './localization';
11
+ import { ToolType } from './ToolController';
12
+
13
+ const overlayCssClass = 'textEditorOverlay';
14
+ export default class TextTool extends BaseTool {
15
+ public kind: ToolType = ToolType.Text;
16
+ private textStyle: TextStyle;
17
+
18
+ private textEditOverlay: HTMLElement;
19
+ private textInputElem: HTMLInputElement|null = null;
20
+ private textTargetPosition: Vec2|null = null;
21
+ private textMeasuringCtx: CanvasRenderingContext2D|null = null;
22
+
23
+ public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {
24
+ super(editor.notifier, description);
25
+ this.textStyle = {
26
+ size: 32,
27
+ fontFamily: 'sans-serif',
28
+ renderingStyle: {
29
+ fill: Color4.purple,
30
+ },
31
+ };
32
+
33
+ this.textEditOverlay = document.createElement('div');
34
+ this.textEditOverlay.classList.add(overlayCssClass);
35
+ this.editor.addStyleSheet(`
36
+ .${overlayCssClass} {
37
+ height: 0;
38
+ overflow: visible;
39
+ }
40
+
41
+ .${overlayCssClass} input {
42
+ background-color: rgba(0, 0, 0, 0);
43
+ border: none;
44
+ padding: 0;
45
+ }
46
+ `);
47
+ this.editor.createHTMLOverlay(this.textEditOverlay);
48
+ this.editor.notifier.on(EditorEventType.ViewportChanged, () => this.updateTextInput());
49
+ }
50
+
51
+ private getTextAscent(text: string, style: TextStyle): number {
52
+ this.textMeasuringCtx ??= document.createElement('canvas').getContext('2d');
53
+ if (this.textMeasuringCtx) {
54
+ Text.applyTextStyles(this.textMeasuringCtx, style);
55
+ return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
56
+ }
57
+
58
+ // Estimate
59
+ return style.size * 2 / 3;
60
+ }
61
+
62
+ private flushInput() {
63
+ if (this.textInputElem && this.textTargetPosition) {
64
+ const content = this.textInputElem.value;
65
+ this.textInputElem.remove();
66
+ this.textInputElem = null;
67
+
68
+ if (content === '') {
69
+ return;
70
+ }
71
+
72
+ const textTransform = Mat33.translation(
73
+ this.textTargetPosition
74
+ ).rightMul(
75
+ Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
76
+ );
77
+
78
+ const textComponent = new Text(
79
+ [ content ],
80
+ textTransform,
81
+ this.textStyle,
82
+ );
83
+
84
+ const action = new EditorImage.AddElementCommand(textComponent);
85
+ this.editor.dispatch(action);
86
+ }
87
+ }
88
+
89
+ private updateTextInput() {
90
+ if (!this.textInputElem || !this.textTargetPosition) {
91
+ this.textInputElem?.remove();
92
+ return;
93
+ }
94
+
95
+ const viewport = this.editor.viewport;
96
+ const textScreenPos = this.editor.viewport.canvasToScreen(this.textTargetPosition);
97
+ this.textInputElem.type = 'text';
98
+ this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
99
+ this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
100
+ this.textInputElem.style.fontVariant = this.textStyle.fontVariant ?? '';
101
+ this.textInputElem.style.fontWeight = this.textStyle.fontWeight ?? '';
102
+ this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
103
+ this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
104
+
105
+ this.textInputElem.style.position = 'relative';
106
+ this.textInputElem.style.left = `${textScreenPos.x}px`;
107
+ this.textInputElem.style.top = `${textScreenPos.y}px`;
108
+ this.textInputElem.style.margin = '0';
109
+
110
+ const rotation = viewport.getRotationAngle();
111
+ const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
112
+ this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
113
+ this.textInputElem.style.transformOrigin = 'top left';
114
+ }
115
+
116
+ private startTextInput(textCanvasPos: Vec2, initialText: string) {
117
+ this.flushInput();
118
+
119
+ this.textInputElem = document.createElement('input');
120
+ this.textInputElem.value = initialText;
121
+ this.textTargetPosition = textCanvasPos;
122
+ this.updateTextInput();
123
+
124
+ this.textInputElem.oninput = () => {
125
+ if (this.textInputElem) {
126
+ this.textInputElem.size = this.textInputElem?.value.length || 10;
127
+ }
128
+ };
129
+ this.textInputElem.onblur = () => {
130
+ // Don't remove the input within the context of a blur event handler.
131
+ // Doing so causes errors.
132
+ setTimeout(() => this.flushInput(), 0);
133
+ };
134
+ this.textInputElem.onkeyup = (evt) => {
135
+ if (evt.key === 'Enter') {
136
+ this.flushInput();
137
+ this.editor.focus();
138
+ } else if (evt.key === 'Escape') {
139
+ // Cancel input.
140
+ this.textInputElem?.remove();
141
+ this.textInputElem = null;
142
+ this.editor.focus();
143
+ }
144
+ };
145
+
146
+ this.textEditOverlay.replaceChildren(this.textInputElem);
147
+ setTimeout(() => this.textInputElem?.focus(), 0);
148
+ }
149
+
150
+ public setEnabled(enabled: boolean) {
151
+ super.setEnabled(enabled);
152
+
153
+ if (!enabled) {
154
+ this.flushInput();
155
+ }
156
+
157
+ this.textEditOverlay.style.display = enabled ? 'block' : 'none';
158
+ }
159
+
160
+ public onPointerDown({ current, allPointers }: PointerEvt): boolean {
161
+ if (current.device === PointerDevice.Eraser) {
162
+ return false;
163
+ }
164
+
165
+ if (allPointers.length === 1) {
166
+ this.startTextInput(current.canvasPos, '');
167
+ return true;
168
+ }
169
+
170
+ return false;
171
+ }
172
+
173
+ public onGestureCancel(): void {
174
+ this.flushInput();
175
+ this.editor.focus();
176
+ }
177
+
178
+ private dispatchUpdateEvent() {
179
+ this.updateTextInput();
180
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
181
+ kind: EditorEventType.ToolUpdated,
182
+ tool: this,
183
+ });
184
+ }
185
+
186
+ public setFontFamily(fontFamily: string) {
187
+ if (fontFamily !== this.textStyle.fontFamily) {
188
+ this.textStyle = {
189
+ ...this.textStyle,
190
+ fontFamily: fontFamily,
191
+ };
192
+
193
+ this.dispatchUpdateEvent();
194
+ }
195
+ }
196
+
197
+ public setColor(color: Color4) {
198
+ if (!color.eq(this.textStyle.renderingStyle.fill)) {
199
+ this.textStyle = {
200
+ ...this.textStyle,
201
+ renderingStyle: {
202
+ ...this.textStyle.renderingStyle,
203
+ fill: color,
204
+ },
205
+ };
206
+
207
+ this.dispatchUpdateEvent();
208
+ }
209
+ }
210
+
211
+ public setFontSize(size: number) {
212
+ if (size !== this.textStyle.size) {
213
+ this.textStyle = {
214
+ ...this.textStyle,
215
+ size,
216
+ };
217
+
218
+ this.dispatchUpdateEvent();
219
+ }
220
+ }
221
+
222
+ public getTextStyle(): TextStyle {
223
+ return this.textStyle;
224
+ }
225
+ }
@@ -9,13 +9,14 @@ import SelectionTool from './SelectionTool';
9
9
  import Color4 from '../Color4';
10
10
  import { ToolLocalization } from './localization';
11
11
  import UndoRedoShortcut from './UndoRedoShortcut';
12
+ import TextTool from './TextTool';
12
13
 
13
14
  export enum ToolType {
14
- TouchPanZoom,
15
15
  Pen,
16
16
  Selection,
17
17
  Eraser,
18
18
  PanZoom,
19
+ Text,
19
20
  UndoRedoShortcut,
20
21
  }
21
22
 
@@ -25,7 +26,7 @@ export default class ToolController {
25
26
 
26
27
  public constructor(editor: Editor, localization: ToolLocalization) {
27
28
  const primaryToolEnabledGroup = new ToolEnabledGroup();
28
- const touchPanZoom = new PanZoom(editor, PanZoomMode.OneFingerGestures, localization.touchPanTool);
29
+ const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
29
30
  const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
30
31
  const primaryTools = [
31
32
  new SelectionTool(editor, localization.selectionTool),
@@ -37,15 +38,16 @@ export default class ToolController {
37
38
 
38
39
  // Highlighter-like pen with width=64
39
40
  new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
41
+
42
+ new TextTool(editor, localization.textTool, localization),
40
43
  ];
41
44
  this.tools = [
42
- touchPanZoom,
45
+ panZoomTool,
43
46
  ...primaryTools,
44
- new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
45
47
  new UndoRedoShortcut(editor),
46
48
  ];
47
49
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
48
- touchPanZoom.setEnabled(false);
50
+ panZoomTool.setEnabled(true);
49
51
  primaryPenTool.setEnabled(true);
50
52
 
51
53
  editor.notifier.on(EditorEventType.ToolEnabled, event => {
@@ -1,11 +1,14 @@
1
1
 
2
2
  export interface ToolLocalization {
3
+ rightClickDragPanTool: string;
3
4
  penTool: (penId: number)=>string;
4
5
  selectionTool: string;
5
6
  eraserTool: string;
6
7
  touchPanTool: string;
7
8
  twoFingerPanZoomTool: string;
8
9
  undoRedoTool: string;
10
+ textTool: string;
11
+ enterTextToInsert: string;
9
12
 
10
13
  toolEnabledAnnouncement: (toolName: string) => string;
11
14
  toolDisabledAnnouncement: (toolName: string) => string;
@@ -18,6 +21,10 @@ export const defaultToolLocalization: ToolLocalization = {
18
21
  touchPanTool: 'Touch Panning',
19
22
  twoFingerPanZoomTool: 'Panning and Zooming',
20
23
  undoRedoTool: 'Undo/Redo',
24
+ rightClickDragPanTool: 'Right-click drag',
25
+
26
+ textTool: 'Text',
27
+ enterTextToInsert: 'Text to insert',
21
28
 
22
29
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
23
30
  toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,