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.
- package/CHANGELOG.md +13 -0
- package/README.md +21 -12
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -1
- package/dist/src/Editor.js +24 -6
- package/dist/src/EditorImage.js +3 -0
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +11 -0
- package/dist/src/SVGLoader.js +113 -4
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +12 -2
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/Text.d.ts +30 -0
- package/dist/src/components/Text.js +111 -0
- package/dist/src/components/localization.d.ts +1 -0
- package/dist/src/components/localization.js +1 -0
- package/dist/src/geometry/Mat33.d.ts +1 -0
- package/dist/src/geometry/Mat33.js +30 -0
- package/dist/src/geometry/Path.js +105 -67
- package/dist/src/geometry/Rect2.d.ts +2 -0
- package/dist/src/geometry/Rect2.js +6 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
- package/dist/src/testing/loadExpectExtensions.js +1 -4
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +242 -154
- package/dist/src/toolbar/icons.d.ts +12 -0
- package/dist/src/toolbar/icons.js +198 -0
- package/dist/src/toolbar/localization.d.ts +5 -1
- package/dist/src/toolbar/localization.js +5 -1
- package/dist/src/toolbar/types.d.ts +4 -0
- package/dist/src/tools/PanZoom.d.ts +9 -6
- package/dist/src/tools/PanZoom.js +30 -21
- package/dist/src/tools/Pen.js +8 -3
- package/dist/src/tools/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +30 -0
- package/dist/src/tools/TextTool.js +173 -0
- package/dist/src/tools/ToolController.d.ts +5 -5
- package/dist/src/tools/ToolController.js +10 -9
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist-test/test-dist-bundle.html +8 -1
- package/package.json +1 -1
- package/src/Editor.css +2 -0
- package/src/Editor.ts +26 -7
- package/src/EditorImage.ts +4 -0
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +146 -5
- package/src/Viewport.ts +15 -3
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/SVGGlobalAttributesObject.ts +0 -1
- package/src/components/Stroke.ts +1 -1
- package/src/components/Text.ts +140 -0
- package/src/components/localization.ts +2 -0
- package/src/geometry/Mat33.test.ts +44 -0
- package/src/geometry/Mat33.ts +41 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +7 -3
- package/src/geometry/Path.ts +110 -68
- package/src/geometry/Rect2.ts +8 -0
- package/src/rendering/renderers/AbstractRenderer.ts +18 -1
- package/src/rendering/renderers/CanvasRenderer.ts +34 -10
- package/src/rendering/renderers/DummyRenderer.ts +8 -0
- package/src/rendering/renderers/SVGRenderer.ts +57 -10
- package/src/testing/loadExpectExtensions.ts +1 -4
- package/src/toolbar/HTMLToolbar.ts +294 -170
- package/src/toolbar/icons.ts +227 -0
- package/src/toolbar/localization.ts +11 -2
- package/src/toolbar/toolbar.css +27 -11
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +225 -0
- package/src/tools/ToolController.ts +7 -5
- 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
|
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
|
-
|
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
|
-
|
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`,
|