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,6 +1,6 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import { ToolType } from '../tools/ToolController';
|
3
|
-
import { EditorEventType
|
3
|
+
import { EditorEventType } from '../types';
|
4
4
|
|
5
5
|
import { coloris, init as colorisInit } from '@melloware/coloris';
|
6
6
|
import Color4 from '../Color4';
|
@@ -9,25 +9,20 @@ import Eraser from '../tools/Eraser';
|
|
9
9
|
import BaseTool from '../tools/BaseTool';
|
10
10
|
import SelectionTool from '../tools/SelectionTool';
|
11
11
|
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
12
|
-
import { Vec2 } from '../geometry/Vec2';
|
13
|
-
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
14
|
-
import Viewport from '../Viewport';
|
15
|
-
import EventDispatcher from '../EventDispatcher';
|
16
12
|
import { ComponentBuilderFactory } from '../components/builders/types';
|
17
13
|
import { makeArrowBuilder } from '../components/builders/ArrowBuilder';
|
18
14
|
import { makeLineBuilder } from '../components/builders/LineBuilder';
|
19
15
|
import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../components/builders/RectangleBuilder';
|
20
16
|
import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
|
17
|
+
import { ActionButtonIcon } from './types';
|
18
|
+
import { makeDropdownIcon, makeEraserIcon, makeIconFromFactory, makePenIcon, makeRedoIcon, makeSelectionIcon, makeHandToolIcon, makeUndoIcon, makeTextIcon } from './icons';
|
19
|
+
import PanZoom, { PanZoomMode } from '../tools/PanZoom';
|
20
|
+
import Mat33 from '../geometry/Mat33';
|
21
|
+
import Viewport from '../Viewport';
|
22
|
+
import TextTool from '../tools/TextTool';
|
21
23
|
|
22
|
-
const primaryForegroundFill = `
|
23
|
-
style='fill: var(--primary-foreground-color);'
|
24
|
-
`;
|
25
|
-
const primaryForegroundStrokeFill = `
|
26
|
-
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
|
27
|
-
`;
|
28
24
|
|
29
25
|
const toolbarCSSPrefix = 'toolbar-';
|
30
|
-
const svgNamespace = 'http://www.w3.org/2000/svg';
|
31
26
|
|
32
27
|
abstract class ToolbarWidget {
|
33
28
|
protected readonly container: HTMLElement;
|
@@ -57,11 +52,6 @@ abstract class ToolbarWidget {
|
|
57
52
|
this.button.setAttribute('role', 'button');
|
58
53
|
this.button.tabIndex = 0;
|
59
54
|
|
60
|
-
this.button.onclick = () => {
|
61
|
-
this.handleClick();
|
62
|
-
};
|
63
|
-
|
64
|
-
|
65
55
|
editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
|
66
56
|
if (toolEvt.kind !== EditorEventType.ToolEnabled) {
|
67
57
|
throw new Error('Incorrect event type! (Expected ToolEnabled)');
|
@@ -91,6 +81,12 @@ abstract class ToolbarWidget {
|
|
91
81
|
// Returns true if such a menu should be created, false otherwise.
|
92
82
|
protected abstract fillDropdown(dropdown: HTMLElement): boolean;
|
93
83
|
|
84
|
+
protected setupActionBtnClickListener(button: HTMLElement) {
|
85
|
+
button.onclick = () => {
|
86
|
+
this.handleClick();
|
87
|
+
};
|
88
|
+
}
|
89
|
+
|
94
90
|
protected handleClick() {
|
95
91
|
if (this.hasDropdown) {
|
96
92
|
if (!this.targetTool.isEnabled()) {
|
@@ -107,6 +103,8 @@ abstract class ToolbarWidget {
|
|
107
103
|
public addTo(parent: HTMLElement) {
|
108
104
|
this.label.innerText = this.getTitle();
|
109
105
|
|
106
|
+
this.setupActionBtnClickListener(this.button);
|
107
|
+
|
110
108
|
this.icon = null;
|
111
109
|
this.updateIcon();
|
112
110
|
|
@@ -167,6 +165,21 @@ abstract class ToolbarWidget {
|
|
167
165
|
this.localizationTable.dropdownHidden(this.targetTool.description)
|
168
166
|
);
|
169
167
|
}
|
168
|
+
|
169
|
+
this.repositionDropdown();
|
170
|
+
}
|
171
|
+
|
172
|
+
protected repositionDropdown() {
|
173
|
+
const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
|
174
|
+
const screenWidth = document.body.clientWidth;
|
175
|
+
|
176
|
+
if (dropdownBBox.left > screenWidth / 2) {
|
177
|
+
this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
|
178
|
+
this.dropdownContainer.style.transform = 'translate(-100%, 0)';
|
179
|
+
} else {
|
180
|
+
this.dropdownContainer.style.marginLeft = '';
|
181
|
+
this.dropdownContainer.style.transform = '';
|
182
|
+
}
|
170
183
|
}
|
171
184
|
|
172
185
|
protected isDropdownVisible(): boolean {
|
@@ -174,17 +187,8 @@ abstract class ToolbarWidget {
|
|
174
187
|
}
|
175
188
|
|
176
189
|
private createDropdownIcon(): Element {
|
177
|
-
const icon =
|
178
|
-
icon.innerHTML = `
|
179
|
-
<g>
|
180
|
-
<path
|
181
|
-
d='M5,10 L50,90 L95,10 Z'
|
182
|
-
${primaryForegroundFill}
|
183
|
-
/>
|
184
|
-
</g>
|
185
|
-
`;
|
190
|
+
const icon = makeDropdownIcon();
|
186
191
|
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
187
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
188
192
|
return icon;
|
189
193
|
}
|
190
194
|
}
|
@@ -194,21 +198,7 @@ class EraserWidget extends ToolbarWidget {
|
|
194
198
|
return this.localizationTable.eraser;
|
195
199
|
}
|
196
200
|
protected createIcon(): Element {
|
197
|
-
|
198
|
-
|
199
|
-
// Draw an eraser-like shape
|
200
|
-
icon.innerHTML = `
|
201
|
-
<g>
|
202
|
-
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
203
|
-
<rect
|
204
|
-
x=10 y=10 width=80 height=50
|
205
|
-
${primaryForegroundFill}
|
206
|
-
/>
|
207
|
-
</g>
|
208
|
-
`;
|
209
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
210
|
-
|
211
|
-
return icon;
|
201
|
+
return makeEraserIcon();
|
212
202
|
}
|
213
203
|
|
214
204
|
protected fillDropdown(_dropdown: HTMLElement): boolean {
|
@@ -229,19 +219,9 @@ class SelectionWidget extends ToolbarWidget {
|
|
229
219
|
}
|
230
220
|
|
231
221
|
protected createIcon(): Element {
|
232
|
-
|
233
|
-
|
234
|
-
// Draw a cursor-like shape
|
235
|
-
icon.innerHTML = `
|
236
|
-
<g>
|
237
|
-
<rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
|
238
|
-
<rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
|
239
|
-
</g>
|
240
|
-
`;
|
241
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
242
|
-
|
243
|
-
return icon;
|
222
|
+
return makeSelectionIcon();
|
244
223
|
}
|
224
|
+
|
245
225
|
protected fillDropdown(dropdown: HTMLElement): boolean {
|
246
226
|
const container = document.createElement('div');
|
247
227
|
const resizeButton = document.createElement('button');
|
@@ -284,42 +264,197 @@ class SelectionWidget extends ToolbarWidget {
|
|
284
264
|
}
|
285
265
|
}
|
286
266
|
|
287
|
-
|
267
|
+
const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
|
268
|
+
const zoomLevelRow = document.createElement('div');
|
269
|
+
|
270
|
+
const increaseButton = document.createElement('button');
|
271
|
+
const decreaseButton = document.createElement('button');
|
272
|
+
const zoomLevelDisplay = document.createElement('span');
|
273
|
+
increaseButton.innerText = '+';
|
274
|
+
decreaseButton.innerText = '-';
|
275
|
+
zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
|
276
|
+
|
277
|
+
zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
|
278
|
+
zoomLevelDisplay.classList.add('zoomDisplay');
|
279
|
+
|
280
|
+
let lastZoom: number|undefined;
|
281
|
+
const updateZoomDisplay = () => {
|
282
|
+
let zoomLevel = editor.viewport.getScaleFactor() * 100;
|
283
|
+
|
284
|
+
if (zoomLevel > 0.1) {
|
285
|
+
zoomLevel = Math.round(zoomLevel * 10) / 10;
|
286
|
+
} else {
|
287
|
+
zoomLevel = Math.round(zoomLevel * 1000) / 1000;
|
288
|
+
}
|
289
|
+
|
290
|
+
if (zoomLevel !== lastZoom) {
|
291
|
+
zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
|
292
|
+
lastZoom = zoomLevel;
|
293
|
+
}
|
294
|
+
};
|
295
|
+
updateZoomDisplay();
|
296
|
+
|
297
|
+
editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
|
298
|
+
if (event.kind === EditorEventType.ViewportChanged) {
|
299
|
+
updateZoomDisplay();
|
300
|
+
}
|
301
|
+
});
|
302
|
+
|
303
|
+
const zoomBy = (factor: number) => {
|
304
|
+
const screenCenter = editor.viewport.visibleRect.center;
|
305
|
+
const transformUpdate = Mat33.scaling2D(factor, screenCenter);
|
306
|
+
editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
|
307
|
+
};
|
308
|
+
|
309
|
+
increaseButton.onclick = () => {
|
310
|
+
zoomBy(5.0/4);
|
311
|
+
};
|
312
|
+
|
313
|
+
decreaseButton.onclick = () => {
|
314
|
+
zoomBy(4.0/5);
|
315
|
+
};
|
316
|
+
|
317
|
+
return zoomLevelRow;
|
318
|
+
};
|
319
|
+
|
320
|
+
class HandToolWidget extends ToolbarWidget {
|
321
|
+
public constructor(
|
322
|
+
editor: Editor, protected tool: PanZoom, localizationTable: ToolbarLocalization
|
323
|
+
) {
|
324
|
+
super(editor, tool, localizationTable);
|
325
|
+
this.container.classList.add('dropdownShowable');
|
326
|
+
}
|
288
327
|
protected getTitle(): string {
|
289
|
-
return this.localizationTable.
|
328
|
+
return this.localizationTable.handTool;
|
290
329
|
}
|
291
330
|
|
292
331
|
protected createIcon(): Element {
|
293
|
-
|
294
|
-
|
295
|
-
// Draw a cursor-like shape
|
296
|
-
icon.innerHTML = `
|
297
|
-
<g>
|
298
|
-
<path d='M11,-30 Q0,10 20,20 Q40,20 40,-30 Z' fill='blue' stroke='black'/>
|
299
|
-
<path d='
|
300
|
-
M0,90 L0,50 Q5,40 10,50
|
301
|
-
L10,20 Q20,15 30,20
|
302
|
-
L30,50 Q50,40 80,50
|
303
|
-
L80,90 L10,90 Z'
|
304
|
-
|
305
|
-
${primaryForegroundStrokeFill}
|
306
|
-
/>
|
307
|
-
</g>
|
308
|
-
`;
|
309
|
-
icon.setAttribute('viewBox', '-10 -30 100 100');
|
332
|
+
return makeHandToolIcon();
|
333
|
+
}
|
310
334
|
|
311
|
-
|
335
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
336
|
+
type OnToggle = (checked: boolean)=>void;
|
337
|
+
let idCounter = 0;
|
338
|
+
const addCheckbox = (label: string, onToggle: OnToggle) => {
|
339
|
+
const rowContainer = document.createElement('div');
|
340
|
+
const labelElem = document.createElement('label');
|
341
|
+
const checkboxElem = document.createElement('input');
|
342
|
+
|
343
|
+
checkboxElem.type = 'checkbox';
|
344
|
+
checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
|
345
|
+
labelElem.setAttribute('for', checkboxElem.id);
|
346
|
+
|
347
|
+
checkboxElem.oninput = () => {
|
348
|
+
onToggle(checkboxElem.checked);
|
349
|
+
};
|
350
|
+
labelElem.innerText = label;
|
351
|
+
|
352
|
+
rowContainer.replaceChildren(checkboxElem, labelElem);
|
353
|
+
dropdown.appendChild(rowContainer);
|
354
|
+
|
355
|
+
return checkboxElem;
|
356
|
+
};
|
357
|
+
|
358
|
+
const setModeFlag = (enabled: boolean, flag: PanZoomMode) => {
|
359
|
+
const mode = this.tool.getMode();
|
360
|
+
if (enabled) {
|
361
|
+
this.tool.setMode(mode | flag);
|
362
|
+
} else {
|
363
|
+
this.tool.setMode(mode & ~flag);
|
364
|
+
}
|
365
|
+
};
|
366
|
+
|
367
|
+
const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
|
368
|
+
setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
|
369
|
+
});
|
370
|
+
|
371
|
+
const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
|
372
|
+
setModeFlag(checked, PanZoomMode.SinglePointerGestures);
|
373
|
+
});
|
374
|
+
|
375
|
+
dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
|
376
|
+
|
377
|
+
const updateInputs = () => {
|
378
|
+
const mode = this.tool.getMode();
|
379
|
+
anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
|
380
|
+
if (anyDevicePanningCheckbox.checked) {
|
381
|
+
touchPanningCheckbox.checked = true;
|
382
|
+
touchPanningCheckbox.disabled = true;
|
383
|
+
} else {
|
384
|
+
touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
|
385
|
+
touchPanningCheckbox.disabled = false;
|
386
|
+
}
|
387
|
+
};
|
388
|
+
|
389
|
+
updateInputs();
|
390
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
|
391
|
+
if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
|
392
|
+
updateInputs();
|
393
|
+
}
|
394
|
+
});
|
395
|
+
|
396
|
+
return true;
|
312
397
|
}
|
313
|
-
|
314
|
-
|
315
|
-
return false;
|
398
|
+
|
399
|
+
protected updateSelected(_active: boolean) {
|
316
400
|
}
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
401
|
+
|
402
|
+
protected handleClick() {
|
403
|
+
this.setDropdownVisible(!this.isDropdownVisible());
|
404
|
+
}
|
405
|
+
}
|
406
|
+
|
407
|
+
class TextToolWidget extends ToolbarWidget {
|
408
|
+
private updateDropdownInputs: (()=>void)|null = null;
|
409
|
+
public constructor(editor: Editor, private tool: TextTool, localization: ToolbarLocalization) {
|
410
|
+
super(editor, tool, localization);
|
411
|
+
|
412
|
+
editor.notifier.on(EditorEventType.ToolUpdated, evt => {
|
413
|
+
if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
|
414
|
+
this.updateIcon();
|
415
|
+
this.updateDropdownInputs?.();
|
416
|
+
}
|
417
|
+
});
|
418
|
+
}
|
419
|
+
|
420
|
+
protected getTitle(): string {
|
421
|
+
return this.targetTool.description;
|
422
|
+
}
|
423
|
+
|
424
|
+
protected createIcon(): Element {
|
425
|
+
const textStyle = this.tool.getTextStyle();
|
426
|
+
return makeTextIcon(textStyle);
|
427
|
+
}
|
428
|
+
|
429
|
+
private static idCounter: number = 0;
|
430
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
431
|
+
const colorRow = document.createElement('div');
|
432
|
+
const colorInput = document.createElement('input');
|
433
|
+
const colorLabel = document.createElement('label');
|
434
|
+
|
435
|
+
colorLabel.innerText = this.localizationTable.colorLabel;
|
436
|
+
|
437
|
+
colorInput.classList.add('coloris_input');
|
438
|
+
colorInput.type = 'button';
|
439
|
+
colorInput.id = `${toolbarCSSPrefix}-text-color-input-${TextToolWidget.idCounter++}`;
|
440
|
+
colorLabel.setAttribute('for', colorInput.id);
|
441
|
+
|
442
|
+
colorInput.oninput = () => {
|
443
|
+
this.tool.setColor(Color4.fromString(colorInput.value));
|
444
|
+
};
|
445
|
+
|
446
|
+
colorRow.appendChild(colorLabel);
|
447
|
+
colorRow.appendChild(colorInput);
|
448
|
+
|
449
|
+
this.updateDropdownInputs = () => {
|
450
|
+
const style = this.tool.getTextStyle();
|
451
|
+
colorInput.value = style.renderingStyle.fill.toHexString();
|
452
|
+
};
|
453
|
+
this.updateDropdownInputs();
|
454
|
+
|
455
|
+
dropdown.appendChild(colorRow);
|
456
|
+
|
457
|
+
return true;
|
323
458
|
}
|
324
459
|
}
|
325
460
|
|
@@ -348,92 +483,17 @@ class PenWidget extends ToolbarWidget {
|
|
348
483
|
return this.targetTool.description;
|
349
484
|
}
|
350
485
|
|
351
|
-
private makePenIcon(elem: SVGSVGElement) {
|
352
|
-
// Use a square-root scale to prevent the pen's tip from overflowing.
|
353
|
-
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
|
354
|
-
const color = this.tool.getColor();
|
355
|
-
|
356
|
-
// Draw a pen-like shape
|
357
|
-
const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
|
358
|
-
const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
|
359
|
-
elem.innerHTML = `
|
360
|
-
<defs>
|
361
|
-
<pattern
|
362
|
-
id='checkerboard'
|
363
|
-
viewBox='0,0,10,10'
|
364
|
-
width='20%'
|
365
|
-
height='20%'
|
366
|
-
patternUnits='userSpaceOnUse'
|
367
|
-
>
|
368
|
-
<rect x=0 y=0 width=10 height=10 fill='white'/>
|
369
|
-
<rect x=0 y=0 width=5 height=5 fill='gray'/>
|
370
|
-
<rect x=5 y=5 width=5 height=5 fill='gray'/>
|
371
|
-
</pattern>
|
372
|
-
</defs>
|
373
|
-
<g>
|
374
|
-
<!-- Pen grip -->
|
375
|
-
<path
|
376
|
-
d='M10,10 L90,10 L90,60 L${50 + scale},80 L${50 - scale},80 L10,60 Z'
|
377
|
-
${primaryForegroundStrokeFill}
|
378
|
-
/>
|
379
|
-
</g>
|
380
|
-
<g>
|
381
|
-
<!-- Checkerboard background for slightly transparent pens -->
|
382
|
-
<path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
|
383
|
-
|
384
|
-
<!-- Actual pen tip -->
|
385
|
-
<path
|
386
|
-
d='${primaryStrokeTipPath}'
|
387
|
-
fill='${color.toHexString()}'
|
388
|
-
stroke='${color.toHexString()}'
|
389
|
-
/>
|
390
|
-
</g>
|
391
|
-
`;
|
392
|
-
}
|
393
|
-
|
394
|
-
// Draws an icon with the pen.
|
395
|
-
private makeDrawnIcon(icon: SVGSVGElement) {
|
396
|
-
const strokeFactory = this.tool.getStrokeFactory();
|
397
|
-
|
398
|
-
const toolThickness = this.tool.getThickness();
|
399
|
-
|
400
|
-
const nowTime = (new Date()).getTime();
|
401
|
-
const startPoint: StrokeDataPoint = {
|
402
|
-
pos: Vec2.of(10, 10),
|
403
|
-
width: toolThickness / 5,
|
404
|
-
color: this.tool.getColor(),
|
405
|
-
time: nowTime - 100,
|
406
|
-
};
|
407
|
-
const endPoint: StrokeDataPoint = {
|
408
|
-
pos: Vec2.of(90, 90),
|
409
|
-
width: toolThickness / 5,
|
410
|
-
color: this.tool.getColor(),
|
411
|
-
time: nowTime,
|
412
|
-
};
|
413
|
-
|
414
|
-
const builder = strokeFactory(startPoint, this.editor.viewport);
|
415
|
-
builder.addPoint(endPoint);
|
416
|
-
|
417
|
-
const viewport = new Viewport(new EventDispatcher());
|
418
|
-
viewport.updateScreenSize(Vec2.of(100, 100));
|
419
|
-
const renderer = new SVGRenderer(icon, viewport);
|
420
|
-
builder.preview(renderer);
|
421
|
-
}
|
422
|
-
|
423
486
|
protected createIcon(): Element {
|
424
|
-
// We need to use createElementNS to embed an SVG element in HTML.
|
425
|
-
// See http://zhangwenli.com/blog/2017/07/26/createelementns/
|
426
|
-
const icon = document.createElementNS(svgNamespace, 'svg');
|
427
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
428
|
-
|
429
487
|
const strokeFactory = this.tool.getStrokeFactory();
|
430
488
|
if (strokeFactory === makeFreehandLineBuilder) {
|
431
|
-
|
489
|
+
// Use a square-root scale to prevent the pen's tip from overflowing.
|
490
|
+
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
491
|
+
const color = this.tool.getColor();
|
492
|
+
return makePenIcon(scale, color.toHexString());
|
432
493
|
} else {
|
433
|
-
this.
|
494
|
+
const strokeFactory = this.tool.getStrokeFactory();
|
495
|
+
return makeIconFromFactory(this.tool, strokeFactory);
|
434
496
|
}
|
435
|
-
|
436
|
-
return icon;
|
437
497
|
}
|
438
498
|
|
439
499
|
private static idCounter: number = 0;
|
@@ -614,10 +674,24 @@ export default class HTMLToolbar {
|
|
614
674
|
});
|
615
675
|
}
|
616
676
|
|
617
|
-
public addActionButton(
|
677
|
+
public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
|
618
678
|
const button = document.createElement('button');
|
619
|
-
button.innerText = text;
|
620
679
|
button.classList.add(`${toolbarCSSPrefix}toolButton`);
|
680
|
+
|
681
|
+
if (typeof title === 'string') {
|
682
|
+
button.innerText = title;
|
683
|
+
} else {
|
684
|
+
const iconElem = title.icon.cloneNode(true) as HTMLElement;
|
685
|
+
const labelElem = document.createElement('label');
|
686
|
+
|
687
|
+
// Use the label to describe the icon -- no additional description should be necessary.
|
688
|
+
iconElem.setAttribute('alt', '');
|
689
|
+
labelElem.innerText = title.label;
|
690
|
+
iconElem.classList.add('toolbar-icon');
|
691
|
+
|
692
|
+
button.replaceChildren(iconElem, labelElem);
|
693
|
+
}
|
694
|
+
|
621
695
|
button.onclick = command;
|
622
696
|
(parent ?? this.container).appendChild(button);
|
623
697
|
|
@@ -628,10 +702,16 @@ export default class HTMLToolbar {
|
|
628
702
|
const undoRedoGroup = document.createElement('div');
|
629
703
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
630
704
|
|
631
|
-
const undoButton = this.addActionButton(
|
705
|
+
const undoButton = this.addActionButton({
|
706
|
+
label: 'Undo',
|
707
|
+
icon: makeUndoIcon()
|
708
|
+
}, () => {
|
632
709
|
this.editor.history.undo();
|
633
710
|
}, undoRedoGroup);
|
634
|
-
const redoButton = this.addActionButton(
|
711
|
+
const redoButton = this.addActionButton({
|
712
|
+
label: 'Redo',
|
713
|
+
icon: makeRedoIcon(),
|
714
|
+
}, () => {
|
635
715
|
this.editor.history.redo();
|
636
716
|
}, undoRedoGroup);
|
637
717
|
this.container.appendChild(undoRedoGroup);
|
@@ -677,8 +757,20 @@ export default class HTMLToolbar {
|
|
677
757
|
(new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
678
758
|
}
|
679
759
|
|
680
|
-
for (const tool of toolController.getMatchingTools(ToolType.
|
681
|
-
|
760
|
+
for (const tool of toolController.getMatchingTools(ToolType.Text)) {
|
761
|
+
if (!(tool instanceof TextTool)) {
|
762
|
+
throw new Error('All text tools must have kind === ToolType.Text');
|
763
|
+
}
|
764
|
+
|
765
|
+
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
766
|
+
}
|
767
|
+
|
768
|
+
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
|
769
|
+
if (!(tool instanceof PanZoom)) {
|
770
|
+
throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
|
771
|
+
}
|
772
|
+
|
773
|
+
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
682
774
|
}
|
683
775
|
|
684
776
|
this.setupColorPickers();
|