js-draw 0.1.3 → 0.1.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.
- 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 +19 -4
- package/dist/src/SVGLoader.js +6 -2
- package/dist/src/Viewport.d.ts +1 -1
- package/dist/src/Viewport.js +5 -5
- package/dist/src/components/Text.js +3 -1
- package/dist/src/localization.d.ts +2 -1
- package/dist/src/localization.js +2 -1
- package/dist/src/rendering/Display.d.ts +2 -0
- package/dist/src/rendering/Display.js +19 -0
- package/dist/src/rendering/localization.d.ts +5 -0
- package/dist/src/rendering/localization.js +4 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +24 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
- package/dist/src/toolbar/HTMLToolbar.js +27 -1
- package/dist/src/toolbar/icons.js +1 -0
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/tools/TextTool.d.ts +2 -0
- package/dist/src/tools/TextTool.js +25 -5
- package/dist-test/test-dist-bundle.html +8 -1
- package/package.json +1 -1
- package/src/Editor.css +12 -0
- package/src/Editor.ts +20 -5
- package/src/SVGLoader.ts +7 -2
- package/src/Viewport.ts +5 -5
- package/src/components/Text.ts +5 -1
- package/src/localization.ts +3 -1
- package/src/rendering/Display.ts +26 -0
- package/src/rendering/localization.ts +10 -0
- package/src/rendering/renderers/TextOnlyRenderer.ts +51 -0
- package/src/toolbar/HTMLToolbar.ts +34 -2
- package/src/toolbar/icons.ts +1 -0
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +6 -3
- package/src/tools/TextTool.ts +27 -4
package/src/components/Text.ts
CHANGED
@@ -23,12 +23,16 @@ export default class Text extends AbstractComponent {
|
|
23
23
|
}
|
24
24
|
|
25
25
|
public static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle) {
|
26
|
+
// Quote the font family if necessary.
|
27
|
+
const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
|
28
|
+
|
26
29
|
ctx.font = [
|
27
30
|
(style.size ?? 12) + 'px',
|
28
31
|
style.fontWeight ?? '',
|
29
|
-
|
32
|
+
`${fontFamily}`,
|
30
33
|
style.fontWeight
|
31
34
|
].join(' ');
|
35
|
+
|
32
36
|
ctx.textAlign = 'left';
|
33
37
|
}
|
34
38
|
|
package/src/localization.ts
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
import { CommandLocalization, defaultCommandLocalization } from './commands/localization';
|
2
2
|
import { defaultComponentLocalization, ImageComponentLocalization } from './components/localization';
|
3
|
+
import { defaultTextRendererLocalization, TextRendererLocalization } from './rendering/localization';
|
3
4
|
import { defaultToolbarLocalization, ToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { defaultToolLocalization, ToolLocalization } from './tools/localization';
|
5
6
|
|
6
7
|
|
7
|
-
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
|
8
|
+
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
|
8
9
|
undoAnnouncement: (actionDescription: string)=> string;
|
9
10
|
redoAnnouncement: (actionDescription: string)=> string;
|
10
11
|
doneLoading: string;
|
@@ -17,6 +18,7 @@ export const defaultEditorLocalization: EditorLocalization = {
|
|
17
18
|
...defaultToolLocalization,
|
18
19
|
...defaultCommandLocalization,
|
19
20
|
...defaultComponentLocalization,
|
21
|
+
...defaultTextRendererLocalization,
|
20
22
|
loading: (percentage: number) => `Loading ${percentage}%...`,
|
21
23
|
imageEditor: 'Image Editor',
|
22
24
|
doneLoading: 'Done loading',
|
package/src/rendering/Display.ts
CHANGED
@@ -5,6 +5,7 @@ import { EditorEventType } from '../types';
|
|
5
5
|
import DummyRenderer from './renderers/DummyRenderer';
|
6
6
|
import { Vec2 } from '../geometry/Vec2';
|
7
7
|
import RenderingCache from './caching/RenderingCache';
|
8
|
+
import TextOnlyRenderer from './renderers/TextOnlyRenderer';
|
8
9
|
|
9
10
|
export enum RenderingMode {
|
10
11
|
DummyRenderer,
|
@@ -15,6 +16,7 @@ export enum RenderingMode {
|
|
15
16
|
export default class Display {
|
16
17
|
private dryInkRenderer: AbstractRenderer;
|
17
18
|
private wetInkRenderer: AbstractRenderer;
|
19
|
+
private textRenderer: TextOnlyRenderer;
|
18
20
|
private cache: RenderingCache;
|
19
21
|
private resizeSurfacesCallback?: ()=> void;
|
20
22
|
private flattenCallback?: ()=> void;
|
@@ -31,6 +33,9 @@ export default class Display {
|
|
31
33
|
throw new Error(`Unknown rendering mode, ${mode}!`);
|
32
34
|
}
|
33
35
|
|
36
|
+
this.textRenderer = new TextOnlyRenderer(editor.viewport, editor.localization);
|
37
|
+
this.initializeTextRendering();
|
38
|
+
|
34
39
|
const cacheBlockResolution = Vec2.of(600, 600);
|
35
40
|
this.cache = new RenderingCache({
|
36
41
|
createRenderer: () => {
|
@@ -129,6 +134,27 @@ export default class Display {
|
|
129
134
|
};
|
130
135
|
}
|
131
136
|
|
137
|
+
private initializeTextRendering() {
|
138
|
+
const textRendererOutputContainer = document.createElement('div');
|
139
|
+
textRendererOutputContainer.classList.add('textRendererOutputContainer');
|
140
|
+
|
141
|
+
const rerenderButton = document.createElement('button');
|
142
|
+
rerenderButton.classList.add('rerenderButton');
|
143
|
+
rerenderButton.innerText = this.editor.localization.rerenderAsText;
|
144
|
+
|
145
|
+
const rerenderOutput = document.createElement('div');
|
146
|
+
rerenderOutput.ariaLive = 'polite';
|
147
|
+
|
148
|
+
rerenderButton.onclick = () => {
|
149
|
+
this.textRenderer.clear();
|
150
|
+
this.editor.image.render(this.textRenderer, this.editor.viewport);
|
151
|
+
rerenderOutput.innerText = this.textRenderer.getDescription();
|
152
|
+
};
|
153
|
+
|
154
|
+
textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
|
155
|
+
this.editor.createHTMLOverlay(textRendererOutputContainer);
|
156
|
+
}
|
157
|
+
|
132
158
|
// Clears the drawing surfaces and otherwise prepares for a rerender.
|
133
159
|
public startRerender(): AbstractRenderer {
|
134
160
|
this.resizeSurfacesCallback?.();
|
@@ -0,0 +1,10 @@
|
|
1
|
+
|
2
|
+
export interface TextRendererLocalization {
|
3
|
+
textNode(content: string): string;
|
4
|
+
rerenderAsText: string;
|
5
|
+
}
|
6
|
+
|
7
|
+
export const defaultTextRendererLocalization: TextRendererLocalization = {
|
8
|
+
textNode: (content: string) => `Text: ${content}`,
|
9
|
+
rerenderAsText: 'Re-render as text',
|
10
|
+
};
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { TextStyle } from '../../components/Text';
|
2
|
+
import Mat33 from '../../geometry/Mat33';
|
3
|
+
import Rect2 from '../../geometry/Rect2';
|
4
|
+
import { Vec2 } from '../../geometry/Vec2';
|
5
|
+
import Vec3 from '../../geometry/Vec3';
|
6
|
+
import Viewport from '../../Viewport';
|
7
|
+
import { TextRendererLocalization } from '../localization';
|
8
|
+
import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
|
9
|
+
|
10
|
+
// Outputs a description of what was rendered.
|
11
|
+
|
12
|
+
export default class TextOnlyRenderer extends AbstractRenderer {
|
13
|
+
private descriptionBuilder: string[] = [];
|
14
|
+
public constructor(viewport: Viewport, private localizationTable: TextRendererLocalization) {
|
15
|
+
super(viewport);
|
16
|
+
}
|
17
|
+
|
18
|
+
public displaySize(): Vec3 {
|
19
|
+
// We don't have a graphical display, export a reasonable size.
|
20
|
+
return Vec2.of(500, 500);
|
21
|
+
}
|
22
|
+
|
23
|
+
public clear(): void {
|
24
|
+
this.descriptionBuilder = [];
|
25
|
+
}
|
26
|
+
|
27
|
+
public getDescription(): string {
|
28
|
+
return this.descriptionBuilder.join('\n');
|
29
|
+
}
|
30
|
+
|
31
|
+
protected beginPath(_startPoint: Vec3): void {
|
32
|
+
}
|
33
|
+
protected endPath(_style: RenderingStyle): void {
|
34
|
+
}
|
35
|
+
protected lineTo(_point: Vec3): void {
|
36
|
+
}
|
37
|
+
protected moveTo(_point: Vec3): void {
|
38
|
+
}
|
39
|
+
protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void {
|
40
|
+
}
|
41
|
+
protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void {
|
42
|
+
}
|
43
|
+
public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
|
44
|
+
this.descriptionBuilder.push(this.localizationTable.textNode(text));
|
45
|
+
}
|
46
|
+
public isTooSmallToRender(rect: Rect2): boolean {
|
47
|
+
return rect.maxDimension < 10 / this.getSizeOfCanvasPixelOnScreen();
|
48
|
+
}
|
49
|
+
public drawPoints(..._points: Vec3[]): void {
|
50
|
+
}
|
51
|
+
}
|
@@ -428,10 +428,25 @@ class TextToolWidget extends ToolbarWidget {
|
|
428
428
|
|
429
429
|
private static idCounter: number = 0;
|
430
430
|
protected fillDropdown(dropdown: HTMLElement): boolean {
|
431
|
+
const fontRow = document.createElement('div');
|
431
432
|
const colorRow = document.createElement('div');
|
433
|
+
|
434
|
+
const fontInput = document.createElement('select');
|
435
|
+
const fontLabel = document.createElement('label');
|
436
|
+
|
432
437
|
const colorInput = document.createElement('input');
|
433
438
|
const colorLabel = document.createElement('label');
|
434
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;
|
435
450
|
colorLabel.innerText = this.localizationTable.colorLabel;
|
436
451
|
|
437
452
|
colorInput.classList.add('coloris_input');
|
@@ -439,6 +454,16 @@ class TextToolWidget extends ToolbarWidget {
|
|
439
454
|
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
|
440
455
|
colorLabel.setAttribute('for', colorInput.id);
|
441
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
|
+
|
442
467
|
colorInput.oninput = () => {
|
443
468
|
this.tool.setColor(Color4.fromString(colorInput.value));
|
444
469
|
};
|
@@ -446,14 +471,21 @@ class TextToolWidget extends ToolbarWidget {
|
|
446
471
|
colorRow.appendChild(colorLabel);
|
447
472
|
colorRow.appendChild(colorInput);
|
448
473
|
|
474
|
+
fontRow.appendChild(fontLabel);
|
475
|
+
fontRow.appendChild(fontInput);
|
476
|
+
|
449
477
|
this.updateDropdownInputs = () => {
|
450
478
|
const style = this.tool.getTextStyle();
|
451
479
|
colorInput.value = style.renderingStyle.fill.toHexString();
|
480
|
+
|
481
|
+
if (!fontsInInput.has(style.fontFamily)) {
|
482
|
+
addFontToInput(style.fontFamily);
|
483
|
+
}
|
484
|
+
fontInput.value = style.fontFamily;
|
452
485
|
};
|
453
486
|
this.updateDropdownInputs();
|
454
487
|
|
455
|
-
dropdown.
|
456
|
-
|
488
|
+
dropdown.replaceChildren(colorRow, fontRow);
|
457
489
|
return true;
|
458
490
|
}
|
459
491
|
}
|
package/src/toolbar/icons.ts
CHANGED
@@ -143,6 +143,7 @@ export const makeTextIcon = (textStyle: TextStyle) => {
|
|
143
143
|
textNode.setAttribute('x', '50');
|
144
144
|
textNode.setAttribute('y', '75');
|
145
145
|
textNode.style.fontSize = '65px';
|
146
|
+
textNode.style.filter = 'drop-shadow(0px 0px 10px var(--primary-shadow-color))';
|
146
147
|
|
147
148
|
icon.appendChild(textNode);
|
148
149
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
export interface ToolbarLocalization {
|
4
|
+
fontLabel: string;
|
4
5
|
anyDevicePanning: string;
|
5
6
|
touchPanning: string;
|
6
7
|
outlinedRectanglePen: string;
|
@@ -32,6 +33,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
32
33
|
handTool: 'Pan',
|
33
34
|
thicknessLabel: 'Thickness: ',
|
34
35
|
colorLabel: 'Color: ',
|
36
|
+
fontLabel: 'Font: ',
|
35
37
|
resizeImageToSelection: 'Resize image to selection',
|
36
38
|
deleteSelection: 'Delete selection',
|
37
39
|
undo: 'Undo',
|
package/src/toolbar/toolbar.css
CHANGED
@@ -12,6 +12,9 @@
|
|
12
12
|
flex-direction: row;
|
13
13
|
justify-content: center;
|
14
14
|
|
15
|
+
/* Display above selection dialogs, etc. */
|
16
|
+
z-index: 2;
|
17
|
+
|
15
18
|
font-family: system-ui, -apple-system, sans-serif;
|
16
19
|
}
|
17
20
|
|
@@ -41,13 +44,13 @@
|
|
41
44
|
background-color: var(--primary-background-color);
|
42
45
|
color: var(--primary-foreground-color);
|
43
46
|
border: none;
|
44
|
-
box-shadow: 0px 0px 2px var(--primary-
|
47
|
+
box-shadow: 0px 0px 2px var(--primary-shadow-color);
|
45
48
|
|
46
49
|
transition: background-color 0.25s ease, box-shadow 0.25s ease, opacity 0.3s ease;
|
47
50
|
}
|
48
51
|
|
49
52
|
.toolbar-button:hover, .toolbar-root button:not(:disabled):hover {
|
50
|
-
box-shadow: 0px 2px 4px var(--primary-
|
53
|
+
box-shadow: 0px 2px 4px var(--primary-shadow-color);
|
51
54
|
}
|
52
55
|
|
53
56
|
.toolbar-root button:disabled {
|
@@ -90,7 +93,7 @@
|
|
90
93
|
/* Prevent overlap/being displayed under the undo/redo buttons */
|
91
94
|
z-index: 2;
|
92
95
|
background-color: var(--primary-background-color);
|
93
|
-
box-shadow: 0px 3px 3px var(--primary-
|
96
|
+
box-shadow: 0px 3px 3px var(--primary-shadow-color);
|
94
97
|
}
|
95
98
|
|
96
99
|
.toolbar-buttonGroup {
|
package/src/tools/TextTool.ts
CHANGED
@@ -19,6 +19,7 @@ export default class TextTool extends BaseTool {
|
|
19
19
|
private textInputElem: HTMLInputElement|null = null;
|
20
20
|
private textTargetPosition: Vec2|null = null;
|
21
21
|
private textMeasuringCtx: CanvasRenderingContext2D|null = null;
|
22
|
+
private textRotation: number;
|
22
23
|
|
23
24
|
public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {
|
24
25
|
super(editor.notifier, description);
|
@@ -73,6 +74,8 @@ export default class TextTool extends BaseTool {
|
|
73
74
|
this.textTargetPosition
|
74
75
|
).rightMul(
|
75
76
|
Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
|
77
|
+
).rightMul(
|
78
|
+
Mat33.zRotation(this.textRotation)
|
76
79
|
);
|
77
80
|
|
78
81
|
const textComponent = new Text(
|
@@ -92,7 +95,8 @@ export default class TextTool extends BaseTool {
|
|
92
95
|
return;
|
93
96
|
}
|
94
97
|
|
95
|
-
const
|
98
|
+
const viewport = this.editor.viewport;
|
99
|
+
const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
|
96
100
|
this.textInputElem.type = 'text';
|
97
101
|
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
|
98
102
|
this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
|
@@ -103,8 +107,13 @@ export default class TextTool extends BaseTool {
|
|
103
107
|
|
104
108
|
this.textInputElem.style.position = 'relative';
|
105
109
|
this.textInputElem.style.left = `${textScreenPos.x}px`;
|
110
|
+
this.textInputElem.style.top = `${textScreenPos.y}px`;
|
111
|
+
this.textInputElem.style.margin = '0';
|
112
|
+
|
113
|
+
const rotation = this.textRotation + viewport.getRotationAngle();
|
106
114
|
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
107
|
-
this.textInputElem.style.
|
115
|
+
this.textInputElem.style.transform = `rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
|
116
|
+
this.textInputElem.style.transformOrigin = 'top left';
|
108
117
|
}
|
109
118
|
|
110
119
|
private startTextInput(textCanvasPos: Vec2, initialText: string) {
|
@@ -113,6 +122,7 @@ export default class TextTool extends BaseTool {
|
|
113
122
|
this.textInputElem = document.createElement('input');
|
114
123
|
this.textInputElem.value = initialText;
|
115
124
|
this.textTargetPosition = textCanvasPos;
|
125
|
+
this.textRotation = -this.editor.viewport.getRotationAngle();
|
116
126
|
this.updateTextInput();
|
117
127
|
|
118
128
|
this.textInputElem.oninput = () => {
|
@@ -121,16 +131,24 @@ export default class TextTool extends BaseTool {
|
|
121
131
|
}
|
122
132
|
};
|
123
133
|
this.textInputElem.onblur = () => {
|
124
|
-
|
134
|
+
// Don't remove the input within the context of a blur event handler.
|
135
|
+
// Doing so causes errors.
|
136
|
+
setTimeout(() => this.flushInput(), 0);
|
125
137
|
};
|
126
138
|
this.textInputElem.onkeyup = (evt) => {
|
127
139
|
if (evt.key === 'Enter') {
|
128
140
|
this.flushInput();
|
141
|
+
this.editor.focus();
|
142
|
+
} else if (evt.key === 'Escape') {
|
143
|
+
// Cancel input.
|
144
|
+
this.textInputElem?.remove();
|
145
|
+
this.textInputElem = null;
|
146
|
+
this.editor.focus();
|
129
147
|
}
|
130
148
|
};
|
131
149
|
|
132
150
|
this.textEditOverlay.replaceChildren(this.textInputElem);
|
133
|
-
setTimeout(() => this.textInputElem
|
151
|
+
setTimeout(() => this.textInputElem?.focus(), 0);
|
134
152
|
}
|
135
153
|
|
136
154
|
public setEnabled(enabled: boolean) {
|
@@ -156,6 +174,11 @@ export default class TextTool extends BaseTool {
|
|
156
174
|
return false;
|
157
175
|
}
|
158
176
|
|
177
|
+
public onGestureCancel(): void {
|
178
|
+
this.flushInput();
|
179
|
+
this.editor.focus();
|
180
|
+
}
|
181
|
+
|
159
182
|
private dispatchUpdateEvent() {
|
160
183
|
this.updateTextInput();
|
161
184
|
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|