js-draw 0.0.10 → 0.1.2
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 +11 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -2
- package/dist/src/Editor.js +17 -7
- package/dist/src/EditorImage.d.ts +15 -7
- package/dist/src/EditorImage.js +46 -37
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +6 -2
- package/dist/src/SVGLoader.js +20 -8
- package/dist/src/Viewport.d.ts +4 -0
- package/dist/src/Viewport.js +51 -0
- package/dist/src/components/AbstractComponent.d.ts +9 -2
- package/dist/src/components/AbstractComponent.js +14 -0
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
- package/dist/src/components/Stroke.d.ts +1 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/UnknownSVGObject.d.ts +1 -1
- package/dist/src/components/UnknownSVGObject.js +1 -1
- package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/builders/LineBuilder.d.ts +1 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
- package/dist/src/components/builders/types.d.ts +1 -1
- package/dist/src/geometry/Mat33.js +3 -0
- package/dist/src/geometry/Path.d.ts +1 -1
- package/dist/src/geometry/Path.js +102 -69
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +47 -9
- package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
- package/dist/src/{Display.js → rendering/Display.js} +34 -4
- package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
- package/dist/src/rendering/caching/CacheRecord.js +52 -0
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
- package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
- package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
- package/dist/src/rendering/caching/RenderingCache.js +42 -0
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
- package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
- package/dist/src/rendering/caching/testUtils.d.ts +9 -0
- package/dist/src/rendering/caching/testUtils.js +20 -0
- package/dist/src/rendering/caching/types.d.ts +21 -0
- package/dist/src/rendering/caching/types.js +1 -0
- package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +20 -9
- package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -3
- package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +10 -5
- package/dist/src/rendering/{CanvasRenderer.js → renderers/CanvasRenderer.js} +60 -20
- package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
- package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
- package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +7 -5
- package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +35 -18
- package/dist/src/testing/createEditor.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +165 -154
- package/dist/src/toolbar/icons.d.ts +10 -0
- package/dist/src/toolbar/icons.js +180 -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 +9 -24
- package/dist/src/tools/ToolController.d.ts +5 -6
- package/dist/src/tools/ToolController.js +8 -10
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/Editor.ts +19 -8
- package/src/EditorImage.test.ts +2 -2
- package/src/EditorImage.ts +58 -42
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +36 -10
- package/src/Viewport.ts +68 -0
- package/src/components/AbstractComponent.ts +21 -2
- package/src/components/SVGGlobalAttributesObject.ts +2 -2
- package/src/components/Stroke.ts +2 -2
- package/src/components/UnknownSVGObject.ts +2 -2
- package/src/components/builders/ArrowBuilder.ts +1 -1
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/builders/LineBuilder.ts +1 -1
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/builders/types.ts +1 -1
- package/src/geometry/Mat33.ts +3 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +12 -2
- package/src/geometry/Path.ts +107 -71
- package/src/geometry/Rect2.test.ts +47 -8
- package/src/geometry/Rect2.ts +57 -9
- package/src/{Display.ts → rendering/Display.ts} +39 -6
- package/src/rendering/caching/CacheRecord.test.ts +49 -0
- package/src/rendering/caching/CacheRecord.ts +73 -0
- package/src/rendering/caching/CacheRecordManager.ts +45 -0
- package/src/rendering/caching/RenderingCache.test.ts +44 -0
- package/src/rendering/caching/RenderingCache.ts +63 -0
- package/src/rendering/caching/RenderingCacheNode.ts +378 -0
- package/src/rendering/caching/testUtils.ts +35 -0
- package/src/rendering/caching/types.ts +39 -0
- package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -9
- package/src/rendering/{CanvasRenderer.ts → renderers/CanvasRenderer.ts} +74 -25
- package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
- package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
- package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +39 -23
- package/src/testing/createEditor.ts +1 -1
- package/src/toolbar/HTMLToolbar.ts +199 -170
- package/src/toolbar/icons.ts +203 -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.test.ts +1 -1
- package/src/tools/SelectionTool.ts +12 -33
- package/src/tools/ToolController.ts +3 -5
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +10 -3
- package/tsconfig.json +1 -0
- package/dist/__mocks__/coloris.d.ts +0 -2
- package/dist/__mocks__/coloris.js +0 -5
@@ -6,22 +6,15 @@ 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/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
|
-
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
|
22
|
-
`;
|
13
|
+
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon } from './icons';
|
14
|
+
import PanZoom, { PanZoomMode } from '../tools/PanZoom';
|
15
|
+
import Mat33 from '../geometry/Mat33';
|
16
|
+
import Viewport from '../Viewport';
|
23
17
|
const toolbarCSSPrefix = 'toolbar-';
|
24
|
-
const svgNamespace = 'http://www.w3.org/2000/svg';
|
25
18
|
class ToolbarWidget {
|
26
19
|
constructor(editor, targetTool, localizationTable) {
|
27
20
|
this.editor = editor;
|
@@ -39,9 +32,6 @@ class ToolbarWidget {
|
|
39
32
|
this.label = document.createElement('label');
|
40
33
|
this.button.setAttribute('role', 'button');
|
41
34
|
this.button.tabIndex = 0;
|
42
|
-
this.button.onclick = () => {
|
43
|
-
this.handleClick();
|
44
|
-
};
|
45
35
|
editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
|
46
36
|
if (toolEvt.kind !== EditorEventType.ToolEnabled) {
|
47
37
|
throw new Error('Incorrect event type! (Expected ToolEnabled)');
|
@@ -60,6 +50,11 @@ class ToolbarWidget {
|
|
60
50
|
}
|
61
51
|
});
|
62
52
|
}
|
53
|
+
setupActionBtnClickListener(button) {
|
54
|
+
button.onclick = () => {
|
55
|
+
this.handleClick();
|
56
|
+
};
|
57
|
+
}
|
63
58
|
handleClick() {
|
64
59
|
if (this.hasDropdown) {
|
65
60
|
if (!this.targetTool.isEnabled()) {
|
@@ -76,6 +71,7 @@ class ToolbarWidget {
|
|
76
71
|
// Adds this to [parent]. This can only be called once for each ToolbarWidget.
|
77
72
|
addTo(parent) {
|
78
73
|
this.label.innerText = this.getTitle();
|
74
|
+
this.setupActionBtnClickListener(this.button);
|
79
75
|
this.icon = null;
|
80
76
|
this.updateIcon();
|
81
77
|
this.updateSelected(this.targetTool.isEnabled());
|
@@ -126,22 +122,26 @@ class ToolbarWidget {
|
|
126
122
|
this.container.classList.remove('dropdownVisible');
|
127
123
|
this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.targetTool.description));
|
128
124
|
}
|
125
|
+
this.repositionDropdown();
|
126
|
+
}
|
127
|
+
repositionDropdown() {
|
128
|
+
const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
|
129
|
+
const screenWidth = document.body.clientWidth;
|
130
|
+
if (dropdownBBox.left > screenWidth / 2) {
|
131
|
+
this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
|
132
|
+
this.dropdownContainer.style.transform = 'translate(-100%, 0)';
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
this.dropdownContainer.style.marginLeft = '';
|
136
|
+
this.dropdownContainer.style.transform = '';
|
137
|
+
}
|
129
138
|
}
|
130
139
|
isDropdownVisible() {
|
131
140
|
return !this.dropdownContainer.classList.contains('hidden');
|
132
141
|
}
|
133
142
|
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
|
-
`;
|
143
|
+
const icon = makeDropdownIcon();
|
143
144
|
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
144
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
145
145
|
return icon;
|
146
146
|
}
|
147
147
|
}
|
@@ -150,19 +150,7 @@ class EraserWidget extends ToolbarWidget {
|
|
150
150
|
return this.localizationTable.eraser;
|
151
151
|
}
|
152
152
|
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;
|
153
|
+
return makeEraserIcon();
|
166
154
|
}
|
167
155
|
fillDropdown(_dropdown) {
|
168
156
|
// No dropdown associated with the eraser
|
@@ -178,16 +166,7 @@ class SelectionWidget extends ToolbarWidget {
|
|
178
166
|
return this.localizationTable.select;
|
179
167
|
}
|
180
168
|
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;
|
169
|
+
return makeSelectionIcon();
|
191
170
|
}
|
192
171
|
fillDropdown(dropdown) {
|
193
172
|
const container = document.createElement('div');
|
@@ -223,40 +202,118 @@ class SelectionWidget extends ToolbarWidget {
|
|
223
202
|
return true;
|
224
203
|
}
|
225
204
|
}
|
226
|
-
|
205
|
+
const makeZoomControl = (localizationTable, editor) => {
|
206
|
+
const zoomLevelRow = document.createElement('div');
|
207
|
+
const increaseButton = document.createElement('button');
|
208
|
+
const decreaseButton = document.createElement('button');
|
209
|
+
const zoomLevelDisplay = document.createElement('span');
|
210
|
+
increaseButton.innerText = '+';
|
211
|
+
decreaseButton.innerText = '-';
|
212
|
+
zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
|
213
|
+
zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
|
214
|
+
zoomLevelDisplay.classList.add('zoomDisplay');
|
215
|
+
let lastZoom;
|
216
|
+
const updateZoomDisplay = () => {
|
217
|
+
let zoomLevel = editor.viewport.getScaleFactor() * 100;
|
218
|
+
if (zoomLevel > 0.1) {
|
219
|
+
zoomLevel = Math.round(zoomLevel * 10) / 10;
|
220
|
+
}
|
221
|
+
else {
|
222
|
+
zoomLevel = Math.round(zoomLevel * 1000) / 1000;
|
223
|
+
}
|
224
|
+
if (zoomLevel !== lastZoom) {
|
225
|
+
zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
|
226
|
+
lastZoom = zoomLevel;
|
227
|
+
}
|
228
|
+
};
|
229
|
+
updateZoomDisplay();
|
230
|
+
editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
|
231
|
+
if (event.kind === EditorEventType.ViewportChanged) {
|
232
|
+
updateZoomDisplay();
|
233
|
+
}
|
234
|
+
});
|
235
|
+
const zoomBy = (factor) => {
|
236
|
+
const screenCenter = editor.viewport.visibleRect.center;
|
237
|
+
const transformUpdate = Mat33.scaling2D(factor, screenCenter);
|
238
|
+
editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
|
239
|
+
};
|
240
|
+
increaseButton.onclick = () => {
|
241
|
+
zoomBy(5.0 / 4);
|
242
|
+
};
|
243
|
+
decreaseButton.onclick = () => {
|
244
|
+
zoomBy(4.0 / 5);
|
245
|
+
};
|
246
|
+
return zoomLevelRow;
|
247
|
+
};
|
248
|
+
class HandToolWidget extends ToolbarWidget {
|
249
|
+
constructor(editor, tool, localizationTable) {
|
250
|
+
super(editor, tool, localizationTable);
|
251
|
+
this.tool = tool;
|
252
|
+
this.container.classList.add('dropdownShowable');
|
253
|
+
}
|
227
254
|
getTitle() {
|
228
|
-
return this.localizationTable.
|
255
|
+
return this.localizationTable.handTool;
|
229
256
|
}
|
230
257
|
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;
|
258
|
+
return makeHandToolIcon();
|
248
259
|
}
|
249
|
-
fillDropdown(
|
250
|
-
|
251
|
-
|
260
|
+
fillDropdown(dropdown) {
|
261
|
+
let idCounter = 0;
|
262
|
+
const addCheckbox = (label, onToggle) => {
|
263
|
+
const rowContainer = document.createElement('div');
|
264
|
+
const labelElem = document.createElement('label');
|
265
|
+
const checkboxElem = document.createElement('input');
|
266
|
+
checkboxElem.type = 'checkbox';
|
267
|
+
checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
|
268
|
+
labelElem.setAttribute('for', checkboxElem.id);
|
269
|
+
checkboxElem.oninput = () => {
|
270
|
+
onToggle(checkboxElem.checked);
|
271
|
+
};
|
272
|
+
labelElem.innerText = label;
|
273
|
+
rowContainer.replaceChildren(checkboxElem, labelElem);
|
274
|
+
dropdown.appendChild(rowContainer);
|
275
|
+
return checkboxElem;
|
276
|
+
};
|
277
|
+
const setModeFlag = (enabled, flag) => {
|
278
|
+
const mode = this.tool.getMode();
|
279
|
+
if (enabled) {
|
280
|
+
this.tool.setMode(mode | flag);
|
281
|
+
}
|
282
|
+
else {
|
283
|
+
this.tool.setMode(mode & ~flag);
|
284
|
+
}
|
285
|
+
};
|
286
|
+
const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
|
287
|
+
setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
|
288
|
+
});
|
289
|
+
const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
|
290
|
+
setModeFlag(checked, PanZoomMode.SinglePointerGestures);
|
291
|
+
});
|
292
|
+
dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
|
293
|
+
const updateInputs = () => {
|
294
|
+
const mode = this.tool.getMode();
|
295
|
+
anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
|
296
|
+
if (anyDevicePanningCheckbox.checked) {
|
297
|
+
touchPanningCheckbox.checked = true;
|
298
|
+
touchPanningCheckbox.disabled = true;
|
299
|
+
}
|
300
|
+
else {
|
301
|
+
touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
|
302
|
+
touchPanningCheckbox.disabled = false;
|
303
|
+
}
|
304
|
+
};
|
305
|
+
updateInputs();
|
306
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
|
307
|
+
if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
|
308
|
+
updateInputs();
|
309
|
+
}
|
310
|
+
});
|
311
|
+
return true;
|
252
312
|
}
|
253
|
-
updateSelected(
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
else {
|
258
|
-
this.container.classList.add('selected');
|
259
|
-
}
|
313
|
+
updateSelected(_active) {
|
314
|
+
}
|
315
|
+
handleClick() {
|
316
|
+
this.setDropdownVisible(!this.isDropdownVisible());
|
260
317
|
}
|
261
318
|
}
|
262
319
|
class PenWidget extends ToolbarWidget {
|
@@ -279,84 +336,18 @@ class PenWidget extends ToolbarWidget {
|
|
279
336
|
getTitle() {
|
280
337
|
return this.targetTool.description;
|
281
338
|
}
|
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
339
|
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
340
|
const strokeFactory = this.tool.getStrokeFactory();
|
353
341
|
if (strokeFactory === makeFreehandLineBuilder) {
|
354
|
-
|
342
|
+
// Use a square-root scale to prevent the pen's tip from overflowing.
|
343
|
+
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
344
|
+
const color = this.tool.getColor();
|
345
|
+
return makePenIcon(scale, color.toHexString());
|
355
346
|
}
|
356
347
|
else {
|
357
|
-
this.
|
348
|
+
const strokeFactory = this.tool.getStrokeFactory();
|
349
|
+
return makeIconFromFactory(this.tool, strokeFactory);
|
358
350
|
}
|
359
|
-
return icon;
|
360
351
|
}
|
361
352
|
fillDropdown(dropdown) {
|
362
353
|
const container = document.createElement('div');
|
@@ -502,10 +493,21 @@ export default class HTMLToolbar {
|
|
502
493
|
closePickerOverlay.style.display = event.open ? 'block' : 'none';
|
503
494
|
});
|
504
495
|
}
|
505
|
-
addActionButton(
|
496
|
+
addActionButton(title, command, parent) {
|
506
497
|
const button = document.createElement('button');
|
507
|
-
button.innerText = text;
|
508
498
|
button.classList.add(`${toolbarCSSPrefix}toolButton`);
|
499
|
+
if (typeof title === 'string') {
|
500
|
+
button.innerText = title;
|
501
|
+
}
|
502
|
+
else {
|
503
|
+
const iconElem = title.icon.cloneNode(true);
|
504
|
+
const labelElem = document.createElement('label');
|
505
|
+
// Use the label to describe the icon -- no additional description should be necessary.
|
506
|
+
iconElem.setAttribute('alt', '');
|
507
|
+
labelElem.innerText = title.label;
|
508
|
+
iconElem.classList.add('toolbar-icon');
|
509
|
+
button.replaceChildren(iconElem, labelElem);
|
510
|
+
}
|
509
511
|
button.onclick = command;
|
510
512
|
(parent !== null && parent !== void 0 ? parent : this.container).appendChild(button);
|
511
513
|
return button;
|
@@ -513,10 +515,16 @@ export default class HTMLToolbar {
|
|
513
515
|
addUndoRedoButtons() {
|
514
516
|
const undoRedoGroup = document.createElement('div');
|
515
517
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
516
|
-
const undoButton = this.addActionButton(
|
518
|
+
const undoButton = this.addActionButton({
|
519
|
+
label: 'Undo',
|
520
|
+
icon: makeUndoIcon()
|
521
|
+
}, () => {
|
517
522
|
this.editor.history.undo();
|
518
523
|
}, undoRedoGroup);
|
519
|
-
const redoButton = this.addActionButton(
|
524
|
+
const redoButton = this.addActionButton({
|
525
|
+
label: 'Redo',
|
526
|
+
icon: makeRedoIcon(),
|
527
|
+
}, () => {
|
520
528
|
this.editor.history.redo();
|
521
529
|
}, undoRedoGroup);
|
522
530
|
this.container.appendChild(undoRedoGroup);
|
@@ -551,8 +559,11 @@ export default class HTMLToolbar {
|
|
551
559
|
}
|
552
560
|
(new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
553
561
|
}
|
554
|
-
for (const tool of toolController.getMatchingTools(ToolType.
|
555
|
-
|
562
|
+
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
|
563
|
+
if (!(tool instanceof PanZoom)) {
|
564
|
+
throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
|
565
|
+
}
|
566
|
+
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
556
567
|
}
|
557
568
|
this.setupColorPickers();
|
558
569
|
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { ComponentBuilderFactory } from '../components/builders/types';
|
2
|
+
import Pen from '../tools/Pen';
|
3
|
+
export declare const makeUndoIcon: () => SVGSVGElement;
|
4
|
+
export declare const makeRedoIcon: (mirror?: boolean) => SVGSVGElement;
|
5
|
+
export declare const makeDropdownIcon: () => SVGSVGElement;
|
6
|
+
export declare const makeEraserIcon: () => SVGSVGElement;
|
7
|
+
export declare const makeSelectionIcon: () => SVGSVGElement;
|
8
|
+
export declare const makeHandToolIcon: () => SVGSVGElement;
|
9
|
+
export declare const makePenIcon: (tipThickness: number, color: string) => SVGSVGElement;
|
10
|
+
export declare const makeIconFromFactory: (pen: Pen, factory: ComponentBuilderFactory) => SVGSVGElement;
|
@@ -0,0 +1,180 @@
|
|
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 makePenIcon = (tipThickness, color) => {
|
115
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
116
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
117
|
+
const halfThickness = tipThickness / 2;
|
118
|
+
// Draw a pen-like shape
|
119
|
+
const primaryStrokeTipPath = `M14,63 L${50 - halfThickness},95 L${50 + halfThickness},90 L88,60 Z`;
|
120
|
+
const backgroundStrokeTipPath = `M14,63 L${50 - halfThickness},85 L${50 + halfThickness},83 L88,60 Z`;
|
121
|
+
icon.innerHTML = `
|
122
|
+
<defs>
|
123
|
+
<pattern
|
124
|
+
id='checkerboard'
|
125
|
+
viewBox='0,0,10,10'
|
126
|
+
width='20%'
|
127
|
+
height='20%'
|
128
|
+
patternUnits='userSpaceOnUse'
|
129
|
+
>
|
130
|
+
<rect x=0 y=0 width=10 height=10 fill='white'/>
|
131
|
+
<rect x=0 y=0 width=5 height=5 fill='gray'/>
|
132
|
+
<rect x=5 y=5 width=5 height=5 fill='gray'/>
|
133
|
+
</pattern>
|
134
|
+
</defs>
|
135
|
+
<g>
|
136
|
+
<!-- Pen grip -->
|
137
|
+
<path
|
138
|
+
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
139
|
+
${primaryForegroundStrokeFill}
|
140
|
+
/>
|
141
|
+
</g>
|
142
|
+
<g>
|
143
|
+
<!-- Checkerboard background for slightly transparent pens -->
|
144
|
+
<path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
|
145
|
+
|
146
|
+
<!-- Actual pen tip -->
|
147
|
+
<path
|
148
|
+
d='${primaryStrokeTipPath}'
|
149
|
+
fill='${color}'
|
150
|
+
stroke='${color}'
|
151
|
+
/>
|
152
|
+
</g>
|
153
|
+
`;
|
154
|
+
return icon;
|
155
|
+
};
|
156
|
+
export const makeIconFromFactory = (pen, factory) => {
|
157
|
+
const toolThickness = pen.getThickness();
|
158
|
+
const nowTime = (new Date()).getTime();
|
159
|
+
const startPoint = {
|
160
|
+
pos: Vec2.of(10, 10),
|
161
|
+
width: toolThickness / 5,
|
162
|
+
color: pen.getColor(),
|
163
|
+
time: nowTime - 100,
|
164
|
+
};
|
165
|
+
const endPoint = {
|
166
|
+
pos: Vec2.of(90, 90),
|
167
|
+
width: toolThickness / 5,
|
168
|
+
color: pen.getColor(),
|
169
|
+
time: nowTime,
|
170
|
+
};
|
171
|
+
const viewport = new Viewport(new EventDispatcher());
|
172
|
+
const builder = factory(startPoint, viewport);
|
173
|
+
builder.addPoint(endPoint);
|
174
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
175
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
176
|
+
viewport.updateScreenSize(Vec2.of(100, 100));
|
177
|
+
const renderer = new SVGRenderer(icon, viewport);
|
178
|
+
builder.preview(renderer);
|
179
|
+
return icon;
|
180
|
+
};
|
@@ -1,4 +1,6 @@
|
|
1
1
|
export interface ToolbarLocalization {
|
2
|
+
anyDevicePanning: string;
|
3
|
+
touchPanning: string;
|
2
4
|
outlinedRectanglePen: string;
|
3
5
|
filledRectanglePen: string;
|
4
6
|
linePen: string;
|
@@ -9,7 +11,7 @@ export interface ToolbarLocalization {
|
|
9
11
|
pen: string;
|
10
12
|
eraser: string;
|
11
13
|
select: string;
|
12
|
-
|
14
|
+
handTool: string;
|
13
15
|
thicknessLabel: string;
|
14
16
|
resizeImageToSelection: string;
|
15
17
|
deleteSelection: string;
|
@@ -17,5 +19,6 @@ export interface ToolbarLocalization {
|
|
17
19
|
redo: string;
|
18
20
|
dropdownShown: (toolName: string) => string;
|
19
21
|
dropdownHidden: (toolName: string) => string;
|
22
|
+
zoomLevel: (zoomPercentage: number) => string;
|
20
23
|
}
|
21
24
|
export declare const defaultToolbarLocalization: ToolbarLocalization;
|
@@ -2,7 +2,7 @@ export const defaultToolbarLocalization = {
|
|
2
2
|
pen: 'Pen',
|
3
3
|
eraser: 'Eraser',
|
4
4
|
select: 'Select',
|
5
|
-
|
5
|
+
handTool: 'Pan',
|
6
6
|
thicknessLabel: 'Thickness: ',
|
7
7
|
colorLabel: 'Color: ',
|
8
8
|
resizeImageToSelection: 'Resize image to selection',
|
@@ -10,6 +10,8 @@ export const defaultToolbarLocalization = {
|
|
10
10
|
undo: 'Undo',
|
11
11
|
redo: 'Redo',
|
12
12
|
selectObjectType: 'Object type: ',
|
13
|
+
touchPanning: 'Touchscreen panning',
|
14
|
+
anyDevicePanning: 'Any device panning',
|
13
15
|
freehandPen: 'Freehand',
|
14
16
|
arrowPen: 'Arrow',
|
15
17
|
linePen: 'Line',
|
@@ -17,4 +19,5 @@ export const defaultToolbarLocalization = {
|
|
17
19
|
filledRectanglePen: 'Filled rectangle',
|
18
20
|
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
19
21
|
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
22
|
+
zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
|
20
23
|
};
|