js-draw 0.1.0 → 0.1.3
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 +12 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +6 -3
- package/dist/src/EditorImage.d.ts +1 -1
- package/dist/src/EditorImage.js +6 -3
- 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 +109 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- 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 +25 -8
- package/dist/src/rendering/Display.js +4 -3
- package/dist/src/rendering/caching/CacheRecord.js +2 -1
- package/dist/src/rendering/caching/CacheRecordManager.js +2 -10
- package/dist/src/rendering/caching/RenderingCache.js +10 -4
- package/dist/src/rendering/caching/RenderingCacheNode.js +10 -3
- package/dist/src/rendering/caching/testUtils.js +1 -1
- package/dist/src/rendering/caching/types.d.ts +1 -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 +216 -154
- package/dist/src/toolbar/icons.d.ts +12 -0
- package/dist/src/toolbar/icons.js +197 -0
- package/dist/src/toolbar/localization.d.ts +4 -1
- package/dist/src/toolbar/localization.js +4 -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 +29 -0
- package/dist/src/tools/TextTool.js +154 -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/package.json +1 -1
- package/src/Editor.ts +7 -3
- package/src/EditorImage.ts +7 -3
- 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 +136 -0
- package/src/components/builders/FreehandLineBuilder.ts +1 -1
- 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.test.ts +9 -0
- package/src/geometry/Rect2.ts +33 -8
- package/src/rendering/Display.ts +4 -3
- package/src/rendering/caching/CacheRecord.ts +2 -1
- package/src/rendering/caching/CacheRecordManager.ts +2 -12
- package/src/rendering/caching/RenderingCache.test.ts +1 -1
- package/src/rendering/caching/RenderingCache.ts +11 -4
- package/src/rendering/caching/RenderingCacheNode.ts +16 -3
- package/src/rendering/caching/testUtils.ts +1 -0
- package/src/rendering/caching/types.ts +4 -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 +262 -170
- package/src/toolbar/icons.ts +226 -0
- package/src/toolbar/localization.ts +9 -2
- package/src/toolbar/toolbar.css +21 -8
- 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 +206 -0
- package/src/tools/ToolController.ts +7 -5
- package/src/tools/localization.ts +7 -0
@@ -1,3 +1,6 @@
|
|
1
|
+
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
2
|
+
import { TextStyle } from '../../components/Text';
|
3
|
+
import Mat33 from '../../geometry/Mat33';
|
1
4
|
import Rect2 from '../../geometry/Rect2';
|
2
5
|
import { Point2, Vec2 } from '../../geometry/Vec2';
|
3
6
|
import Viewport from '../../Viewport';
|
@@ -9,7 +12,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
9
12
|
private lastPathStyle;
|
10
13
|
private lastPath;
|
11
14
|
private lastPathStart;
|
12
|
-
private
|
15
|
+
private objectElems;
|
13
16
|
private overwrittenAttrs;
|
14
17
|
constructor(elem: SVGSVGElement, viewport: Viewport);
|
15
18
|
setRootSVGAttribute(name: string, value: string | null): void;
|
@@ -18,8 +21,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
18
21
|
protected beginPath(startPoint: Point2): void;
|
19
22
|
protected endPath(style: RenderingStyle): void;
|
20
23
|
private addPathToSVG;
|
24
|
+
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
21
25
|
startObject(boundingBox: Rect2): void;
|
22
|
-
endObject(): void;
|
26
|
+
endObject(loaderData?: LoadSaveDataTable): void;
|
23
27
|
protected lineTo(point: Point2): void;
|
24
28
|
protected moveTo(point: Point2): void;
|
25
29
|
protected traceCubicBezierCurve(controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2): void;
|
@@ -1,11 +1,13 @@
|
|
1
1
|
import Path, { PathCommandType } from '../../geometry/Path';
|
2
2
|
import { Vec2 } from '../../geometry/Vec2';
|
3
|
+
import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
|
3
4
|
import AbstractRenderer from './AbstractRenderer';
|
4
5
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
5
6
|
export default class SVGRenderer extends AbstractRenderer {
|
6
7
|
constructor(elem, viewport) {
|
7
8
|
super(viewport);
|
8
9
|
this.elem = elem;
|
10
|
+
this.objectElems = null;
|
9
11
|
this.overwrittenAttrs = {};
|
10
12
|
this.clear();
|
11
13
|
}
|
@@ -26,7 +28,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
26
28
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
27
29
|
}
|
28
30
|
clear() {
|
29
|
-
this.mainGroup = document.createElementNS(svgNameSpace, 'g');
|
30
31
|
// Restore all alltributes
|
31
32
|
for (const attrName in this.overwrittenAttrs) {
|
32
33
|
const value = this.overwrittenAttrs[attrName];
|
@@ -38,8 +39,6 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
38
39
|
}
|
39
40
|
}
|
40
41
|
this.overwrittenAttrs = {};
|
41
|
-
// Remove all children
|
42
|
-
this.elem.replaceChildren(this.mainGroup);
|
43
42
|
}
|
44
43
|
beginPath(startPoint) {
|
45
44
|
var _a;
|
@@ -72,6 +71,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
72
71
|
}
|
73
72
|
// Push [this.fullPath] to the SVG
|
74
73
|
addPathToSVG() {
|
74
|
+
var _a;
|
75
75
|
if (!this.lastPathStyle || !this.lastPath) {
|
76
76
|
return;
|
77
77
|
}
|
@@ -83,7 +83,31 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
83
83
|
pathElem.setAttribute('stroke', style.stroke.color.toHexString());
|
84
84
|
pathElem.setAttribute('stroke-width', style.stroke.width.toString());
|
85
85
|
}
|
86
|
-
this.
|
86
|
+
this.elem.appendChild(pathElem);
|
87
|
+
(_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
|
88
|
+
}
|
89
|
+
drawText(text, transform, style) {
|
90
|
+
var _a, _b, _c;
|
91
|
+
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
92
|
+
const textElem = document.createElementNS(svgNameSpace, 'text');
|
93
|
+
textElem.appendChild(document.createTextNode(text));
|
94
|
+
textElem.style.transform = `matrix(
|
95
|
+
${transform.a1}, ${transform.b1},
|
96
|
+
${transform.a2}, ${transform.b2},
|
97
|
+
${transform.a3}, ${transform.b3}
|
98
|
+
)`;
|
99
|
+
textElem.style.fontFamily = style.fontFamily;
|
100
|
+
textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
|
101
|
+
textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
|
102
|
+
textElem.style.fontSize = style.size + 'px';
|
103
|
+
textElem.style.fill = style.renderingStyle.fill.toHexString();
|
104
|
+
if (style.renderingStyle.stroke) {
|
105
|
+
const strokeStyle = style.renderingStyle.stroke;
|
106
|
+
textElem.style.stroke = strokeStyle.color.toHexString();
|
107
|
+
textElem.style.strokeWidth = strokeStyle.width + 'px';
|
108
|
+
}
|
109
|
+
this.elem.appendChild(textElem);
|
110
|
+
(_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
|
87
111
|
}
|
88
112
|
startObject(boundingBox) {
|
89
113
|
super.startObject(boundingBox);
|
@@ -91,11 +115,30 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
91
115
|
this.lastPath = null;
|
92
116
|
this.lastPathStart = null;
|
93
117
|
this.lastPathStyle = null;
|
118
|
+
this.objectElems = [];
|
94
119
|
}
|
95
|
-
endObject() {
|
96
|
-
|
120
|
+
endObject(loaderData) {
|
121
|
+
var _a;
|
122
|
+
super.endObject(loaderData);
|
97
123
|
// Don't extend paths across objects
|
98
124
|
this.addPathToSVG();
|
125
|
+
if (loaderData) {
|
126
|
+
// Restore any attributes unsupported by the app.
|
127
|
+
for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
|
128
|
+
const attrs = loaderData[svgAttributesDataKey];
|
129
|
+
const styleAttrs = loaderData[svgStyleAttributesDataKey];
|
130
|
+
if (attrs) {
|
131
|
+
for (const [attr, value] of attrs) {
|
132
|
+
elem.setAttribute(attr, value);
|
133
|
+
}
|
134
|
+
}
|
135
|
+
if (styleAttrs) {
|
136
|
+
for (const attr of styleAttrs) {
|
137
|
+
elem.style.setProperty(attr.key, attr.value, attr.priority);
|
138
|
+
}
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
99
142
|
}
|
100
143
|
lineTo(point) {
|
101
144
|
point = this.canvasToScreen(point);
|
@@ -137,7 +180,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
137
180
|
elem.setAttribute('cx', `${point.x}`);
|
138
181
|
elem.setAttribute('cy', `${point.y}`);
|
139
182
|
elem.setAttribute('r', '15');
|
140
|
-
this.
|
183
|
+
this.elem.appendChild(elem);
|
141
184
|
});
|
142
185
|
}
|
143
186
|
// Renders a **copy** of the given element.
|
@@ -15,10 +15,7 @@ export const loadExpectExtensions = () => {
|
|
15
15
|
return {
|
16
16
|
pass,
|
17
17
|
message: () => {
|
18
|
-
|
19
|
-
return `Expected ${expected} not to .eq ${actual}. Options(${eqArgs})`;
|
20
|
-
}
|
21
|
-
return `Expected ${expected} to .eq ${actual}. Options(${eqArgs})`;
|
18
|
+
return `Expected ${pass ? '!' : ''}(${actual}).eq(${expected}). Options(${eqArgs})`;
|
22
19
|
},
|
23
20
|
};
|
24
21
|
},
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import { ToolbarLocalization } from './localization';
|
3
|
+
import { ActionButtonIcon } from './types';
|
3
4
|
export default class HTMLToolbar {
|
4
5
|
private editor;
|
5
6
|
private localizationTable;
|
@@ -7,7 +8,7 @@ export default class HTMLToolbar {
|
|
7
8
|
private penTypes;
|
8
9
|
constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
|
9
10
|
setupColorPickers(): void;
|
10
|
-
addActionButton(
|
11
|
+
addActionButton(title: string | ActionButtonIcon, command: () => void, parent?: Element): HTMLButtonElement;
|
11
12
|
private addUndoRedoButtons;
|
12
13
|
addDefaultToolWidgets(): void;
|
13
14
|
addDefaultActionButtons(): void;
|
@@ -6,22 +6,16 @@ import Pen from '../tools/Pen';
|
|
6
6
|
import Eraser from '../tools/Eraser';
|
7
7
|
import SelectionTool from '../tools/SelectionTool';
|
8
8
|
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
9
|
-
import { Vec2 } from '../geometry/Vec2';
|
10
|
-
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
11
|
-
import Viewport from '../Viewport';
|
12
|
-
import EventDispatcher from '../EventDispatcher';
|
13
9
|
import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
|
14
10
|
import { makeLineBuilder } from '../components/builders/LineBuilder';
|
15
11
|
import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
|
16
12
|
import { defaultToolbarLocalization } from './localization';
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
`;
|
13
|
+
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
|
14
|
+
import PanZoom, { PanZoomMode } from '../tools/PanZoom';
|
15
|
+
import Mat33 from '../geometry/Mat33';
|
16
|
+
import Viewport from '../Viewport';
|
17
|
+
import TextTool from '../tools/TextTool';
|
23
18
|
const toolbarCSSPrefix = 'toolbar-';
|
24
|
-
const svgNamespace = 'http://www.w3.org/2000/svg';
|
25
19
|
class ToolbarWidget {
|
26
20
|
constructor(editor, targetTool, localizationTable) {
|
27
21
|
this.editor = editor;
|
@@ -39,9 +33,6 @@ class ToolbarWidget {
|
|
39
33
|
this.label = document.createElement('label');
|
40
34
|
this.button.setAttribute('role', 'button');
|
41
35
|
this.button.tabIndex = 0;
|
42
|
-
this.button.onclick = () => {
|
43
|
-
this.handleClick();
|
44
|
-
};
|
45
36
|
editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
|
46
37
|
if (toolEvt.kind !== EditorEventType.ToolEnabled) {
|
47
38
|
throw new Error('Incorrect event type! (Expected ToolEnabled)');
|
@@ -60,6 +51,11 @@ class ToolbarWidget {
|
|
60
51
|
}
|
61
52
|
});
|
62
53
|
}
|
54
|
+
setupActionBtnClickListener(button) {
|
55
|
+
button.onclick = () => {
|
56
|
+
this.handleClick();
|
57
|
+
};
|
58
|
+
}
|
63
59
|
handleClick() {
|
64
60
|
if (this.hasDropdown) {
|
65
61
|
if (!this.targetTool.isEnabled()) {
|
@@ -76,6 +72,7 @@ class ToolbarWidget {
|
|
76
72
|
// Adds this to [parent]. This can only be called once for each ToolbarWidget.
|
77
73
|
addTo(parent) {
|
78
74
|
this.label.innerText = this.getTitle();
|
75
|
+
this.setupActionBtnClickListener(this.button);
|
79
76
|
this.icon = null;
|
80
77
|
this.updateIcon();
|
81
78
|
this.updateSelected(this.targetTool.isEnabled());
|
@@ -126,22 +123,26 @@ class ToolbarWidget {
|
|
126
123
|
this.container.classList.remove('dropdownVisible');
|
127
124
|
this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.targetTool.description));
|
128
125
|
}
|
126
|
+
this.repositionDropdown();
|
127
|
+
}
|
128
|
+
repositionDropdown() {
|
129
|
+
const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
|
130
|
+
const screenWidth = document.body.clientWidth;
|
131
|
+
if (dropdownBBox.left > screenWidth / 2) {
|
132
|
+
this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
|
133
|
+
this.dropdownContainer.style.transform = 'translate(-100%, 0)';
|
134
|
+
}
|
135
|
+
else {
|
136
|
+
this.dropdownContainer.style.marginLeft = '';
|
137
|
+
this.dropdownContainer.style.transform = '';
|
138
|
+
}
|
129
139
|
}
|
130
140
|
isDropdownVisible() {
|
131
141
|
return !this.dropdownContainer.classList.contains('hidden');
|
132
142
|
}
|
133
143
|
createDropdownIcon() {
|
134
|
-
const icon =
|
135
|
-
icon.innerHTML = `
|
136
|
-
<g>
|
137
|
-
<path
|
138
|
-
d='M5,10 L50,90 L95,10 Z'
|
139
|
-
${primaryForegroundFill}
|
140
|
-
/>
|
141
|
-
</g>
|
142
|
-
`;
|
144
|
+
const icon = makeDropdownIcon();
|
143
145
|
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
144
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
145
146
|
return icon;
|
146
147
|
}
|
147
148
|
}
|
@@ -150,19 +151,7 @@ class EraserWidget extends ToolbarWidget {
|
|
150
151
|
return this.localizationTable.eraser;
|
151
152
|
}
|
152
153
|
createIcon() {
|
153
|
-
|
154
|
-
// Draw an eraser-like shape
|
155
|
-
icon.innerHTML = `
|
156
|
-
<g>
|
157
|
-
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
158
|
-
<rect
|
159
|
-
x=10 y=10 width=80 height=50
|
160
|
-
${primaryForegroundFill}
|
161
|
-
/>
|
162
|
-
</g>
|
163
|
-
`;
|
164
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
165
|
-
return icon;
|
154
|
+
return makeEraserIcon();
|
166
155
|
}
|
167
156
|
fillDropdown(_dropdown) {
|
168
157
|
// No dropdown associated with the eraser
|
@@ -178,16 +167,7 @@ class SelectionWidget extends ToolbarWidget {
|
|
178
167
|
return this.localizationTable.select;
|
179
168
|
}
|
180
169
|
createIcon() {
|
181
|
-
|
182
|
-
// Draw a cursor-like shape
|
183
|
-
icon.innerHTML = `
|
184
|
-
<g>
|
185
|
-
<rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
|
186
|
-
<rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
|
187
|
-
</g>
|
188
|
-
`;
|
189
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
190
|
-
return icon;
|
170
|
+
return makeSelectionIcon();
|
191
171
|
}
|
192
172
|
fillDropdown(dropdown) {
|
193
173
|
const container = document.createElement('div');
|
@@ -223,42 +203,164 @@ class SelectionWidget extends ToolbarWidget {
|
|
223
203
|
return true;
|
224
204
|
}
|
225
205
|
}
|
226
|
-
|
206
|
+
const makeZoomControl = (localizationTable, editor) => {
|
207
|
+
const zoomLevelRow = document.createElement('div');
|
208
|
+
const increaseButton = document.createElement('button');
|
209
|
+
const decreaseButton = document.createElement('button');
|
210
|
+
const zoomLevelDisplay = document.createElement('span');
|
211
|
+
increaseButton.innerText = '+';
|
212
|
+
decreaseButton.innerText = '-';
|
213
|
+
zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
|
214
|
+
zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
|
215
|
+
zoomLevelDisplay.classList.add('zoomDisplay');
|
216
|
+
let lastZoom;
|
217
|
+
const updateZoomDisplay = () => {
|
218
|
+
let zoomLevel = editor.viewport.getScaleFactor() * 100;
|
219
|
+
if (zoomLevel > 0.1) {
|
220
|
+
zoomLevel = Math.round(zoomLevel * 10) / 10;
|
221
|
+
}
|
222
|
+
else {
|
223
|
+
zoomLevel = Math.round(zoomLevel * 1000) / 1000;
|
224
|
+
}
|
225
|
+
if (zoomLevel !== lastZoom) {
|
226
|
+
zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
|
227
|
+
lastZoom = zoomLevel;
|
228
|
+
}
|
229
|
+
};
|
230
|
+
updateZoomDisplay();
|
231
|
+
editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
|
232
|
+
if (event.kind === EditorEventType.ViewportChanged) {
|
233
|
+
updateZoomDisplay();
|
234
|
+
}
|
235
|
+
});
|
236
|
+
const zoomBy = (factor) => {
|
237
|
+
const screenCenter = editor.viewport.visibleRect.center;
|
238
|
+
const transformUpdate = Mat33.scaling2D(factor, screenCenter);
|
239
|
+
editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
|
240
|
+
};
|
241
|
+
increaseButton.onclick = () => {
|
242
|
+
zoomBy(5.0 / 4);
|
243
|
+
};
|
244
|
+
decreaseButton.onclick = () => {
|
245
|
+
zoomBy(4.0 / 5);
|
246
|
+
};
|
247
|
+
return zoomLevelRow;
|
248
|
+
};
|
249
|
+
class HandToolWidget extends ToolbarWidget {
|
250
|
+
constructor(editor, tool, localizationTable) {
|
251
|
+
super(editor, tool, localizationTable);
|
252
|
+
this.tool = tool;
|
253
|
+
this.container.classList.add('dropdownShowable');
|
254
|
+
}
|
227
255
|
getTitle() {
|
228
|
-
return this.localizationTable.
|
256
|
+
return this.localizationTable.handTool;
|
229
257
|
}
|
230
258
|
createIcon() {
|
231
|
-
|
232
|
-
// Draw a cursor-like shape
|
233
|
-
icon.innerHTML = `
|
234
|
-
<g>
|
235
|
-
<path d='M11,-30 Q0,10 20,20 Q40,20 40,-30 Z' fill='blue' stroke='black'/>
|
236
|
-
<path d='
|
237
|
-
M0,90 L0,50 Q5,40 10,50
|
238
|
-
L10,20 Q20,15 30,20
|
239
|
-
L30,50 Q50,40 80,50
|
240
|
-
L80,90 L10,90 Z'
|
241
|
-
|
242
|
-
${primaryForegroundStrokeFill}
|
243
|
-
/>
|
244
|
-
</g>
|
245
|
-
`;
|
246
|
-
icon.setAttribute('viewBox', '-10 -30 100 100');
|
247
|
-
return icon;
|
259
|
+
return makeHandToolIcon();
|
248
260
|
}
|
249
|
-
fillDropdown(
|
250
|
-
|
251
|
-
|
261
|
+
fillDropdown(dropdown) {
|
262
|
+
let idCounter = 0;
|
263
|
+
const addCheckbox = (label, onToggle) => {
|
264
|
+
const rowContainer = document.createElement('div');
|
265
|
+
const labelElem = document.createElement('label');
|
266
|
+
const checkboxElem = document.createElement('input');
|
267
|
+
checkboxElem.type = 'checkbox';
|
268
|
+
checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
|
269
|
+
labelElem.setAttribute('for', checkboxElem.id);
|
270
|
+
checkboxElem.oninput = () => {
|
271
|
+
onToggle(checkboxElem.checked);
|
272
|
+
};
|
273
|
+
labelElem.innerText = label;
|
274
|
+
rowContainer.replaceChildren(checkboxElem, labelElem);
|
275
|
+
dropdown.appendChild(rowContainer);
|
276
|
+
return checkboxElem;
|
277
|
+
};
|
278
|
+
const setModeFlag = (enabled, flag) => {
|
279
|
+
const mode = this.tool.getMode();
|
280
|
+
if (enabled) {
|
281
|
+
this.tool.setMode(mode | flag);
|
282
|
+
}
|
283
|
+
else {
|
284
|
+
this.tool.setMode(mode & ~flag);
|
285
|
+
}
|
286
|
+
};
|
287
|
+
const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
|
288
|
+
setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
|
289
|
+
});
|
290
|
+
const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
|
291
|
+
setModeFlag(checked, PanZoomMode.SinglePointerGestures);
|
292
|
+
});
|
293
|
+
dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
|
294
|
+
const updateInputs = () => {
|
295
|
+
const mode = this.tool.getMode();
|
296
|
+
anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
|
297
|
+
if (anyDevicePanningCheckbox.checked) {
|
298
|
+
touchPanningCheckbox.checked = true;
|
299
|
+
touchPanningCheckbox.disabled = true;
|
300
|
+
}
|
301
|
+
else {
|
302
|
+
touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
|
303
|
+
touchPanningCheckbox.disabled = false;
|
304
|
+
}
|
305
|
+
};
|
306
|
+
updateInputs();
|
307
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
|
308
|
+
if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
|
309
|
+
updateInputs();
|
310
|
+
}
|
311
|
+
});
|
312
|
+
return true;
|
252
313
|
}
|
253
|
-
updateSelected(
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
else {
|
258
|
-
this.container.classList.add('selected');
|
259
|
-
}
|
314
|
+
updateSelected(_active) {
|
315
|
+
}
|
316
|
+
handleClick() {
|
317
|
+
this.setDropdownVisible(!this.isDropdownVisible());
|
260
318
|
}
|
261
319
|
}
|
320
|
+
class TextToolWidget extends ToolbarWidget {
|
321
|
+
constructor(editor, tool, localization) {
|
322
|
+
super(editor, tool, localization);
|
323
|
+
this.tool = tool;
|
324
|
+
this.updateDropdownInputs = null;
|
325
|
+
editor.notifier.on(EditorEventType.ToolUpdated, evt => {
|
326
|
+
var _a;
|
327
|
+
if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
|
328
|
+
this.updateIcon();
|
329
|
+
(_a = this.updateDropdownInputs) === null || _a === void 0 ? void 0 : _a.call(this);
|
330
|
+
}
|
331
|
+
});
|
332
|
+
}
|
333
|
+
getTitle() {
|
334
|
+
return this.targetTool.description;
|
335
|
+
}
|
336
|
+
createIcon() {
|
337
|
+
const textStyle = this.tool.getTextStyle();
|
338
|
+
return makeTextIcon(textStyle);
|
339
|
+
}
|
340
|
+
fillDropdown(dropdown) {
|
341
|
+
const colorRow = document.createElement('div');
|
342
|
+
const colorInput = document.createElement('input');
|
343
|
+
const colorLabel = document.createElement('label');
|
344
|
+
colorLabel.innerText = this.localizationTable.colorLabel;
|
345
|
+
colorInput.classList.add('coloris_input');
|
346
|
+
colorInput.type = 'button';
|
347
|
+
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
|
348
|
+
colorLabel.setAttribute('for', colorInput.id);
|
349
|
+
colorInput.oninput = () => {
|
350
|
+
this.tool.setColor(Color4.fromString(colorInput.value));
|
351
|
+
};
|
352
|
+
colorRow.appendChild(colorLabel);
|
353
|
+
colorRow.appendChild(colorInput);
|
354
|
+
this.updateDropdownInputs = () => {
|
355
|
+
const style = this.tool.getTextStyle();
|
356
|
+
colorInput.value = style.renderingStyle.fill.toHexString();
|
357
|
+
};
|
358
|
+
this.updateDropdownInputs();
|
359
|
+
dropdown.appendChild(colorRow);
|
360
|
+
return true;
|
361
|
+
}
|
362
|
+
}
|
363
|
+
TextToolWidget.idCounter = 0;
|
262
364
|
class PenWidget extends ToolbarWidget {
|
263
365
|
constructor(editor, tool, localization, penTypes) {
|
264
366
|
super(editor, tool, localization);
|
@@ -279,84 +381,18 @@ class PenWidget extends ToolbarWidget {
|
|
279
381
|
getTitle() {
|
280
382
|
return this.targetTool.description;
|
281
383
|
}
|
282
|
-
makePenIcon(elem) {
|
283
|
-
// Use a square-root scale to prevent the pen's tip from overflowing.
|
284
|
-
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
|
285
|
-
const color = this.tool.getColor();
|
286
|
-
// Draw a pen-like shape
|
287
|
-
const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
|
288
|
-
const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
|
289
|
-
elem.innerHTML = `
|
290
|
-
<defs>
|
291
|
-
<pattern
|
292
|
-
id='checkerboard'
|
293
|
-
viewBox='0,0,10,10'
|
294
|
-
width='20%'
|
295
|
-
height='20%'
|
296
|
-
patternUnits='userSpaceOnUse'
|
297
|
-
>
|
298
|
-
<rect x=0 y=0 width=10 height=10 fill='white'/>
|
299
|
-
<rect x=0 y=0 width=5 height=5 fill='gray'/>
|
300
|
-
<rect x=5 y=5 width=5 height=5 fill='gray'/>
|
301
|
-
</pattern>
|
302
|
-
</defs>
|
303
|
-
<g>
|
304
|
-
<!-- Pen grip -->
|
305
|
-
<path
|
306
|
-
d='M10,10 L90,10 L90,60 L${50 + scale},80 L${50 - scale},80 L10,60 Z'
|
307
|
-
${primaryForegroundStrokeFill}
|
308
|
-
/>
|
309
|
-
</g>
|
310
|
-
<g>
|
311
|
-
<!-- Checkerboard background for slightly transparent pens -->
|
312
|
-
<path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
|
313
|
-
|
314
|
-
<!-- Actual pen tip -->
|
315
|
-
<path
|
316
|
-
d='${primaryStrokeTipPath}'
|
317
|
-
fill='${color.toHexString()}'
|
318
|
-
stroke='${color.toHexString()}'
|
319
|
-
/>
|
320
|
-
</g>
|
321
|
-
`;
|
322
|
-
}
|
323
|
-
// Draws an icon with the pen.
|
324
|
-
makeDrawnIcon(icon) {
|
325
|
-
const strokeFactory = this.tool.getStrokeFactory();
|
326
|
-
const toolThickness = this.tool.getThickness();
|
327
|
-
const nowTime = (new Date()).getTime();
|
328
|
-
const startPoint = {
|
329
|
-
pos: Vec2.of(10, 10),
|
330
|
-
width: toolThickness / 5,
|
331
|
-
color: this.tool.getColor(),
|
332
|
-
time: nowTime - 100,
|
333
|
-
};
|
334
|
-
const endPoint = {
|
335
|
-
pos: Vec2.of(90, 90),
|
336
|
-
width: toolThickness / 5,
|
337
|
-
color: this.tool.getColor(),
|
338
|
-
time: nowTime,
|
339
|
-
};
|
340
|
-
const builder = strokeFactory(startPoint, this.editor.viewport);
|
341
|
-
builder.addPoint(endPoint);
|
342
|
-
const viewport = new Viewport(new EventDispatcher());
|
343
|
-
viewport.updateScreenSize(Vec2.of(100, 100));
|
344
|
-
const renderer = new SVGRenderer(icon, viewport);
|
345
|
-
builder.preview(renderer);
|
346
|
-
}
|
347
384
|
createIcon() {
|
348
|
-
// We need to use createElementNS to embed an SVG element in HTML.
|
349
|
-
// See http://zhangwenli.com/blog/2017/07/26/createelementns/
|
350
|
-
const icon = document.createElementNS(svgNamespace, 'svg');
|
351
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
352
385
|
const strokeFactory = this.tool.getStrokeFactory();
|
353
386
|
if (strokeFactory === makeFreehandLineBuilder) {
|
354
|
-
|
387
|
+
// Use a square-root scale to prevent the pen's tip from overflowing.
|
388
|
+
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
389
|
+
const color = this.tool.getColor();
|
390
|
+
return makePenIcon(scale, color.toHexString());
|
355
391
|
}
|
356
392
|
else {
|
357
|
-
this.
|
393
|
+
const strokeFactory = this.tool.getStrokeFactory();
|
394
|
+
return makeIconFromFactory(this.tool, strokeFactory);
|
358
395
|
}
|
359
|
-
return icon;
|
360
396
|
}
|
361
397
|
fillDropdown(dropdown) {
|
362
398
|
const container = document.createElement('div');
|
@@ -502,10 +538,21 @@ export default class HTMLToolbar {
|
|
502
538
|
closePickerOverlay.style.display = event.open ? 'block' : 'none';
|
503
539
|
});
|
504
540
|
}
|
505
|
-
addActionButton(
|
541
|
+
addActionButton(title, command, parent) {
|
506
542
|
const button = document.createElement('button');
|
507
|
-
button.innerText = text;
|
508
543
|
button.classList.add(`${toolbarCSSPrefix}toolButton`);
|
544
|
+
if (typeof title === 'string') {
|
545
|
+
button.innerText = title;
|
546
|
+
}
|
547
|
+
else {
|
548
|
+
const iconElem = title.icon.cloneNode(true);
|
549
|
+
const labelElem = document.createElement('label');
|
550
|
+
// Use the label to describe the icon -- no additional description should be necessary.
|
551
|
+
iconElem.setAttribute('alt', '');
|
552
|
+
labelElem.innerText = title.label;
|
553
|
+
iconElem.classList.add('toolbar-icon');
|
554
|
+
button.replaceChildren(iconElem, labelElem);
|
555
|
+
}
|
509
556
|
button.onclick = command;
|
510
557
|
(parent !== null && parent !== void 0 ? parent : this.container).appendChild(button);
|
511
558
|
return button;
|
@@ -513,10 +560,16 @@ export default class HTMLToolbar {
|
|
513
560
|
addUndoRedoButtons() {
|
514
561
|
const undoRedoGroup = document.createElement('div');
|
515
562
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
516
|
-
const undoButton = this.addActionButton(
|
563
|
+
const undoButton = this.addActionButton({
|
564
|
+
label: 'Undo',
|
565
|
+
icon: makeUndoIcon()
|
566
|
+
}, () => {
|
517
567
|
this.editor.history.undo();
|
518
568
|
}, undoRedoGroup);
|
519
|
-
const redoButton = this.addActionButton(
|
569
|
+
const redoButton = this.addActionButton({
|
570
|
+
label: 'Redo',
|
571
|
+
icon: makeRedoIcon(),
|
572
|
+
}, () => {
|
520
573
|
this.editor.history.redo();
|
521
574
|
}, undoRedoGroup);
|
522
575
|
this.container.appendChild(undoRedoGroup);
|
@@ -551,8 +604,17 @@ export default class HTMLToolbar {
|
|
551
604
|
}
|
552
605
|
(new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
553
606
|
}
|
554
|
-
for (const tool of toolController.getMatchingTools(ToolType.
|
555
|
-
|
607
|
+
for (const tool of toolController.getMatchingTools(ToolType.Text)) {
|
608
|
+
if (!(tool instanceof TextTool)) {
|
609
|
+
throw new Error('All text tools must have kind === ToolType.Text');
|
610
|
+
}
|
611
|
+
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
612
|
+
}
|
613
|
+
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
|
614
|
+
if (!(tool instanceof PanZoom)) {
|
615
|
+
throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
|
616
|
+
}
|
617
|
+
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
556
618
|
}
|
557
619
|
this.setupColorPickers();
|
558
620
|
}
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { ComponentBuilderFactory } from '../components/builders/types';
|
2
|
+
import { TextStyle } from '../components/Text';
|
3
|
+
import Pen from '../tools/Pen';
|
4
|
+
export declare const makeUndoIcon: () => SVGSVGElement;
|
5
|
+
export declare const makeRedoIcon: (mirror?: boolean) => SVGSVGElement;
|
6
|
+
export declare const makeDropdownIcon: () => SVGSVGElement;
|
7
|
+
export declare const makeEraserIcon: () => SVGSVGElement;
|
8
|
+
export declare const makeSelectionIcon: () => SVGSVGElement;
|
9
|
+
export declare const makeHandToolIcon: () => SVGSVGElement;
|
10
|
+
export declare const makeTextIcon: (textStyle: TextStyle) => SVGSVGElement;
|
11
|
+
export declare const makePenIcon: (tipThickness: number, color: string) => SVGSVGElement;
|
12
|
+
export declare const makeIconFromFactory: (pen: Pen, factory: ComponentBuilderFactory) => SVGSVGElement;
|