js-draw 0.1.1 → 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 +3 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.js +4 -0
- package/dist/src/EditorImage.js +3 -0
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +3 -0
- package/dist/src/SVGLoader.js +11 -1
- package/dist/src/Viewport.js +10 -0
- package/dist/src/components/AbstractComponent.d.ts +6 -0
- package/dist/src/components/AbstractComponent.js +11 -0
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/geometry/Path.js +97 -66
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +21 -7
- 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/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/package.json +1 -1
- package/src/Editor.ts +4 -0
- package/src/EditorImage.ts +4 -0
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +25 -2
- package/src/Viewport.ts +13 -1
- package/src/components/AbstractComponent.ts +16 -1
- package/src/components/Stroke.ts +1 -1
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.ts +99 -67
- package/src/rendering/renderers/AbstractRenderer.ts +2 -1
- package/src/rendering/renderers/SVGRenderer.ts +22 -10
- 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/ToolController.ts +3 -5
- package/src/tools/localization.ts +2 -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,19 @@ 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 } from './icons';
|
19
|
+
import PanZoom, { PanZoomMode } from '../tools/PanZoom';
|
20
|
+
import Mat33 from '../geometry/Mat33';
|
21
|
+
import Viewport from '../Viewport';
|
21
22
|
|
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
23
|
|
29
24
|
const toolbarCSSPrefix = 'toolbar-';
|
30
|
-
const svgNamespace = 'http://www.w3.org/2000/svg';
|
31
25
|
|
32
26
|
abstract class ToolbarWidget {
|
33
27
|
protected readonly container: HTMLElement;
|
@@ -57,11 +51,6 @@ abstract class ToolbarWidget {
|
|
57
51
|
this.button.setAttribute('role', 'button');
|
58
52
|
this.button.tabIndex = 0;
|
59
53
|
|
60
|
-
this.button.onclick = () => {
|
61
|
-
this.handleClick();
|
62
|
-
};
|
63
|
-
|
64
|
-
|
65
54
|
editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
|
66
55
|
if (toolEvt.kind !== EditorEventType.ToolEnabled) {
|
67
56
|
throw new Error('Incorrect event type! (Expected ToolEnabled)');
|
@@ -91,6 +80,12 @@ abstract class ToolbarWidget {
|
|
91
80
|
// Returns true if such a menu should be created, false otherwise.
|
92
81
|
protected abstract fillDropdown(dropdown: HTMLElement): boolean;
|
93
82
|
|
83
|
+
protected setupActionBtnClickListener(button: HTMLElement) {
|
84
|
+
button.onclick = () => {
|
85
|
+
this.handleClick();
|
86
|
+
};
|
87
|
+
}
|
88
|
+
|
94
89
|
protected handleClick() {
|
95
90
|
if (this.hasDropdown) {
|
96
91
|
if (!this.targetTool.isEnabled()) {
|
@@ -107,6 +102,8 @@ abstract class ToolbarWidget {
|
|
107
102
|
public addTo(parent: HTMLElement) {
|
108
103
|
this.label.innerText = this.getTitle();
|
109
104
|
|
105
|
+
this.setupActionBtnClickListener(this.button);
|
106
|
+
|
110
107
|
this.icon = null;
|
111
108
|
this.updateIcon();
|
112
109
|
|
@@ -167,6 +164,21 @@ abstract class ToolbarWidget {
|
|
167
164
|
this.localizationTable.dropdownHidden(this.targetTool.description)
|
168
165
|
);
|
169
166
|
}
|
167
|
+
|
168
|
+
this.repositionDropdown();
|
169
|
+
}
|
170
|
+
|
171
|
+
protected repositionDropdown() {
|
172
|
+
const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
|
173
|
+
const screenWidth = document.body.clientWidth;
|
174
|
+
|
175
|
+
if (dropdownBBox.left > screenWidth / 2) {
|
176
|
+
this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
|
177
|
+
this.dropdownContainer.style.transform = 'translate(-100%, 0)';
|
178
|
+
} else {
|
179
|
+
this.dropdownContainer.style.marginLeft = '';
|
180
|
+
this.dropdownContainer.style.transform = '';
|
181
|
+
}
|
170
182
|
}
|
171
183
|
|
172
184
|
protected isDropdownVisible(): boolean {
|
@@ -174,17 +186,8 @@ abstract class ToolbarWidget {
|
|
174
186
|
}
|
175
187
|
|
176
188
|
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
|
-
`;
|
189
|
+
const icon = makeDropdownIcon();
|
186
190
|
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
187
|
-
icon.setAttribute('viewBox', '0 0 100 100');
|
188
191
|
return icon;
|
189
192
|
}
|
190
193
|
}
|
@@ -194,21 +197,7 @@ class EraserWidget extends ToolbarWidget {
|
|
194
197
|
return this.localizationTable.eraser;
|
195
198
|
}
|
196
199
|
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;
|
200
|
+
return makeEraserIcon();
|
212
201
|
}
|
213
202
|
|
214
203
|
protected fillDropdown(_dropdown: HTMLElement): boolean {
|
@@ -229,19 +218,9 @@ class SelectionWidget extends ToolbarWidget {
|
|
229
218
|
}
|
230
219
|
|
231
220
|
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;
|
221
|
+
return makeSelectionIcon();
|
244
222
|
}
|
223
|
+
|
245
224
|
protected fillDropdown(dropdown: HTMLElement): boolean {
|
246
225
|
const container = document.createElement('div');
|
247
226
|
const resizeButton = document.createElement('button');
|
@@ -284,42 +263,143 @@ class SelectionWidget extends ToolbarWidget {
|
|
284
263
|
}
|
285
264
|
}
|
286
265
|
|
287
|
-
|
266
|
+
const makeZoomControl = (localizationTable: ToolbarLocalization, editor: Editor) => {
|
267
|
+
const zoomLevelRow = document.createElement('div');
|
268
|
+
|
269
|
+
const increaseButton = document.createElement('button');
|
270
|
+
const decreaseButton = document.createElement('button');
|
271
|
+
const zoomLevelDisplay = document.createElement('span');
|
272
|
+
increaseButton.innerText = '+';
|
273
|
+
decreaseButton.innerText = '-';
|
274
|
+
zoomLevelRow.replaceChildren(zoomLevelDisplay, increaseButton, decreaseButton);
|
275
|
+
|
276
|
+
zoomLevelRow.classList.add(`${toolbarCSSPrefix}zoomLevelEditor`);
|
277
|
+
zoomLevelDisplay.classList.add('zoomDisplay');
|
278
|
+
|
279
|
+
let lastZoom: number|undefined;
|
280
|
+
const updateZoomDisplay = () => {
|
281
|
+
let zoomLevel = editor.viewport.getScaleFactor() * 100;
|
282
|
+
|
283
|
+
if (zoomLevel > 0.1) {
|
284
|
+
zoomLevel = Math.round(zoomLevel * 10) / 10;
|
285
|
+
} else {
|
286
|
+
zoomLevel = Math.round(zoomLevel * 1000) / 1000;
|
287
|
+
}
|
288
|
+
|
289
|
+
if (zoomLevel !== lastZoom) {
|
290
|
+
zoomLevelDisplay.innerText = localizationTable.zoomLevel(zoomLevel);
|
291
|
+
lastZoom = zoomLevel;
|
292
|
+
}
|
293
|
+
};
|
294
|
+
updateZoomDisplay();
|
295
|
+
|
296
|
+
editor.notifier.on(EditorEventType.ViewportChanged, (event) => {
|
297
|
+
if (event.kind === EditorEventType.ViewportChanged) {
|
298
|
+
updateZoomDisplay();
|
299
|
+
}
|
300
|
+
});
|
301
|
+
|
302
|
+
const zoomBy = (factor: number) => {
|
303
|
+
const screenCenter = editor.viewport.visibleRect.center;
|
304
|
+
const transformUpdate = Mat33.scaling2D(factor, screenCenter);
|
305
|
+
editor.dispatch(new Viewport.ViewportTransform(transformUpdate), false);
|
306
|
+
};
|
307
|
+
|
308
|
+
increaseButton.onclick = () => {
|
309
|
+
zoomBy(5.0/4);
|
310
|
+
};
|
311
|
+
|
312
|
+
decreaseButton.onclick = () => {
|
313
|
+
zoomBy(4.0/5);
|
314
|
+
};
|
315
|
+
|
316
|
+
return zoomLevelRow;
|
317
|
+
};
|
318
|
+
|
319
|
+
class HandToolWidget extends ToolbarWidget {
|
320
|
+
public constructor(
|
321
|
+
editor: Editor, protected tool: PanZoom, localizationTable: ToolbarLocalization
|
322
|
+
) {
|
323
|
+
super(editor, tool, localizationTable);
|
324
|
+
this.container.classList.add('dropdownShowable');
|
325
|
+
}
|
288
326
|
protected getTitle(): string {
|
289
|
-
return this.localizationTable.
|
327
|
+
return this.localizationTable.handTool;
|
290
328
|
}
|
291
329
|
|
292
330
|
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');
|
331
|
+
return makeHandToolIcon();
|
332
|
+
}
|
310
333
|
|
311
|
-
|
334
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
335
|
+
type OnToggle = (checked: boolean)=>void;
|
336
|
+
let idCounter = 0;
|
337
|
+
const addCheckbox = (label: string, onToggle: OnToggle) => {
|
338
|
+
const rowContainer = document.createElement('div');
|
339
|
+
const labelElem = document.createElement('label');
|
340
|
+
const checkboxElem = document.createElement('input');
|
341
|
+
|
342
|
+
checkboxElem.type = 'checkbox';
|
343
|
+
checkboxElem.id = `${toolbarCSSPrefix}hand-tool-option-${idCounter++}`;
|
344
|
+
labelElem.setAttribute('for', checkboxElem.id);
|
345
|
+
|
346
|
+
checkboxElem.oninput = () => {
|
347
|
+
onToggle(checkboxElem.checked);
|
348
|
+
};
|
349
|
+
labelElem.innerText = label;
|
350
|
+
|
351
|
+
rowContainer.replaceChildren(checkboxElem, labelElem);
|
352
|
+
dropdown.appendChild(rowContainer);
|
353
|
+
|
354
|
+
return checkboxElem;
|
355
|
+
};
|
356
|
+
|
357
|
+
const setModeFlag = (enabled: boolean, flag: PanZoomMode) => {
|
358
|
+
const mode = this.tool.getMode();
|
359
|
+
if (enabled) {
|
360
|
+
this.tool.setMode(mode | flag);
|
361
|
+
} else {
|
362
|
+
this.tool.setMode(mode & ~flag);
|
363
|
+
}
|
364
|
+
};
|
365
|
+
|
366
|
+
const touchPanningCheckbox = addCheckbox(this.localizationTable.touchPanning, checked => {
|
367
|
+
setModeFlag(checked, PanZoomMode.OneFingerTouchGestures);
|
368
|
+
});
|
369
|
+
|
370
|
+
const anyDevicePanningCheckbox = addCheckbox(this.localizationTable.anyDevicePanning, checked => {
|
371
|
+
setModeFlag(checked, PanZoomMode.SinglePointerGestures);
|
372
|
+
});
|
373
|
+
|
374
|
+
dropdown.appendChild(makeZoomControl(this.localizationTable, this.editor));
|
375
|
+
|
376
|
+
const updateInputs = () => {
|
377
|
+
const mode = this.tool.getMode();
|
378
|
+
anyDevicePanningCheckbox.checked = !!(mode & PanZoomMode.SinglePointerGestures);
|
379
|
+
if (anyDevicePanningCheckbox.checked) {
|
380
|
+
touchPanningCheckbox.checked = true;
|
381
|
+
touchPanningCheckbox.disabled = true;
|
382
|
+
} else {
|
383
|
+
touchPanningCheckbox.checked = !!(mode & PanZoomMode.OneFingerTouchGestures);
|
384
|
+
touchPanningCheckbox.disabled = false;
|
385
|
+
}
|
386
|
+
};
|
387
|
+
|
388
|
+
updateInputs();
|
389
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, event => {
|
390
|
+
if (event.kind === EditorEventType.ToolUpdated && event.tool === this.tool) {
|
391
|
+
updateInputs();
|
392
|
+
}
|
393
|
+
});
|
394
|
+
|
395
|
+
return true;
|
312
396
|
}
|
313
|
-
|
314
|
-
|
315
|
-
return false;
|
397
|
+
|
398
|
+
protected updateSelected(_active: boolean) {
|
316
399
|
}
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
} else {
|
321
|
-
this.container.classList.add('selected');
|
322
|
-
}
|
400
|
+
|
401
|
+
protected handleClick() {
|
402
|
+
this.setDropdownVisible(!this.isDropdownVisible());
|
323
403
|
}
|
324
404
|
}
|
325
405
|
|
@@ -348,92 +428,17 @@ class PenWidget extends ToolbarWidget {
|
|
348
428
|
return this.targetTool.description;
|
349
429
|
}
|
350
430
|
|
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
431
|
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
432
|
const strokeFactory = this.tool.getStrokeFactory();
|
430
433
|
if (strokeFactory === makeFreehandLineBuilder) {
|
431
|
-
|
434
|
+
// Use a square-root scale to prevent the pen's tip from overflowing.
|
435
|
+
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
436
|
+
const color = this.tool.getColor();
|
437
|
+
return makePenIcon(scale, color.toHexString());
|
432
438
|
} else {
|
433
|
-
this.
|
439
|
+
const strokeFactory = this.tool.getStrokeFactory();
|
440
|
+
return makeIconFromFactory(this.tool, strokeFactory);
|
434
441
|
}
|
435
|
-
|
436
|
-
return icon;
|
437
442
|
}
|
438
443
|
|
439
444
|
private static idCounter: number = 0;
|
@@ -614,10 +619,24 @@ export default class HTMLToolbar {
|
|
614
619
|
});
|
615
620
|
}
|
616
621
|
|
617
|
-
public addActionButton(
|
622
|
+
public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
|
618
623
|
const button = document.createElement('button');
|
619
|
-
button.innerText = text;
|
620
624
|
button.classList.add(`${toolbarCSSPrefix}toolButton`);
|
625
|
+
|
626
|
+
if (typeof title === 'string') {
|
627
|
+
button.innerText = title;
|
628
|
+
} else {
|
629
|
+
const iconElem = title.icon.cloneNode(true) as HTMLElement;
|
630
|
+
const labelElem = document.createElement('label');
|
631
|
+
|
632
|
+
// Use the label to describe the icon -- no additional description should be necessary.
|
633
|
+
iconElem.setAttribute('alt', '');
|
634
|
+
labelElem.innerText = title.label;
|
635
|
+
iconElem.classList.add('toolbar-icon');
|
636
|
+
|
637
|
+
button.replaceChildren(iconElem, labelElem);
|
638
|
+
}
|
639
|
+
|
621
640
|
button.onclick = command;
|
622
641
|
(parent ?? this.container).appendChild(button);
|
623
642
|
|
@@ -628,10 +647,16 @@ export default class HTMLToolbar {
|
|
628
647
|
const undoRedoGroup = document.createElement('div');
|
629
648
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
630
649
|
|
631
|
-
const undoButton = this.addActionButton(
|
650
|
+
const undoButton = this.addActionButton({
|
651
|
+
label: 'Undo',
|
652
|
+
icon: makeUndoIcon()
|
653
|
+
}, () => {
|
632
654
|
this.editor.history.undo();
|
633
655
|
}, undoRedoGroup);
|
634
|
-
const redoButton = this.addActionButton(
|
656
|
+
const redoButton = this.addActionButton({
|
657
|
+
label: 'Redo',
|
658
|
+
icon: makeRedoIcon(),
|
659
|
+
}, () => {
|
635
660
|
this.editor.history.redo();
|
636
661
|
}, undoRedoGroup);
|
637
662
|
this.container.appendChild(undoRedoGroup);
|
@@ -677,8 +702,12 @@ export default class HTMLToolbar {
|
|
677
702
|
(new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
678
703
|
}
|
679
704
|
|
680
|
-
for (const tool of toolController.getMatchingTools(ToolType.
|
681
|
-
|
705
|
+
for (const tool of toolController.getMatchingTools(ToolType.PanZoom)) {
|
706
|
+
if (!(tool instanceof PanZoom)) {
|
707
|
+
throw new Error('All SelectionTools must have kind === ToolType.PanZoom');
|
708
|
+
}
|
709
|
+
|
710
|
+
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
682
711
|
}
|
683
712
|
|
684
713
|
this.setupColorPickers();
|
@@ -0,0 +1,203 @@
|
|
1
|
+
import { ComponentBuilderFactory } from '../components/builders/types';
|
2
|
+
import EventDispatcher from '../EventDispatcher';
|
3
|
+
import { Vec2 } from '../geometry/Vec2';
|
4
|
+
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
5
|
+
import Pen from '../tools/Pen';
|
6
|
+
import { StrokeDataPoint } from '../types';
|
7
|
+
import Viewport from '../Viewport';
|
8
|
+
|
9
|
+
const svgNamespace = 'http://www.w3.org/2000/svg';
|
10
|
+
const primaryForegroundFill = `
|
11
|
+
style='fill: var(--primary-foreground-color);'
|
12
|
+
`;
|
13
|
+
const primaryForegroundStrokeFill = `
|
14
|
+
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
|
15
|
+
`;
|
16
|
+
|
17
|
+
export const makeUndoIcon = () => {
|
18
|
+
return makeRedoIcon(true);
|
19
|
+
};
|
20
|
+
|
21
|
+
export const makeRedoIcon = (mirror: boolean = false) => {
|
22
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
23
|
+
icon.innerHTML = `
|
24
|
+
<style>
|
25
|
+
.toolbar-svg-undo-redo-icon {
|
26
|
+
stroke: var(--primary-foreground-color);
|
27
|
+
stroke-width: 12;
|
28
|
+
stroke-linejoin: round;
|
29
|
+
stroke-linecap: round;
|
30
|
+
fill: none;
|
31
|
+
|
32
|
+
transform-origin: center;
|
33
|
+
}
|
34
|
+
</style>
|
35
|
+
<path
|
36
|
+
d='M20,20 A15,15 0 0 1 70,80 L80,90 L60,70 L65,90 L87,90 L65,80'
|
37
|
+
class='toolbar-svg-undo-redo-icon'
|
38
|
+
style='${mirror ? 'transform: scale(-1, 1);' : ''}'/>
|
39
|
+
`;
|
40
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
41
|
+
return icon;
|
42
|
+
};
|
43
|
+
|
44
|
+
export const makeDropdownIcon = () => {
|
45
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
46
|
+
icon.innerHTML = `
|
47
|
+
<g>
|
48
|
+
<path
|
49
|
+
d='M5,10 L50,90 L95,10 Z'
|
50
|
+
${primaryForegroundFill}
|
51
|
+
/>
|
52
|
+
</g>
|
53
|
+
`;
|
54
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
55
|
+
return icon;
|
56
|
+
};
|
57
|
+
|
58
|
+
export const makeEraserIcon = () => {
|
59
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
60
|
+
|
61
|
+
// Draw an eraser-like shape
|
62
|
+
icon.innerHTML = `
|
63
|
+
<g>
|
64
|
+
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
65
|
+
<rect
|
66
|
+
x=10 y=10 width=80 height=50
|
67
|
+
${primaryForegroundFill}
|
68
|
+
/>
|
69
|
+
</g>
|
70
|
+
`;
|
71
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
72
|
+
return icon;
|
73
|
+
};
|
74
|
+
|
75
|
+
export const makeSelectionIcon = () => {
|
76
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
77
|
+
|
78
|
+
// Draw a cursor-like shape
|
79
|
+
icon.innerHTML = `
|
80
|
+
<g>
|
81
|
+
<rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
|
82
|
+
<rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
|
83
|
+
</g>
|
84
|
+
`;
|
85
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
86
|
+
|
87
|
+
return icon;
|
88
|
+
};
|
89
|
+
|
90
|
+
export const makeHandToolIcon = () => {
|
91
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
92
|
+
|
93
|
+
// Draw a cursor-like shape
|
94
|
+
icon.innerHTML = `
|
95
|
+
<g>
|
96
|
+
<path d='
|
97
|
+
m 10,60
|
98
|
+
5,30
|
99
|
+
H 90
|
100
|
+
V 30
|
101
|
+
C 90,20 75,20 75,30
|
102
|
+
V 60
|
103
|
+
20
|
104
|
+
C 75,10 60,10 60,20
|
105
|
+
V 60
|
106
|
+
15
|
107
|
+
C 60,5 45,5 45,15
|
108
|
+
V 60
|
109
|
+
25
|
110
|
+
C 45,15 30,15 30,25
|
111
|
+
V 60
|
112
|
+
75
|
113
|
+
L 25,60
|
114
|
+
C 20,45 10,50 10,60
|
115
|
+
Z'
|
116
|
+
|
117
|
+
fill='none'
|
118
|
+
style='
|
119
|
+
stroke: var(--primary-foreground-color);
|
120
|
+
stroke-width: 2;
|
121
|
+
'
|
122
|
+
/>
|
123
|
+
</g>
|
124
|
+
`;
|
125
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
126
|
+
return icon;
|
127
|
+
};
|
128
|
+
|
129
|
+
export const makePenIcon = (tipThickness: number, color: string) => {
|
130
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
131
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
132
|
+
|
133
|
+
const halfThickness = tipThickness / 2;
|
134
|
+
|
135
|
+
// Draw a pen-like shape
|
136
|
+
const primaryStrokeTipPath = `M14,63 L${50 - halfThickness},95 L${50 + halfThickness},90 L88,60 Z`;
|
137
|
+
const backgroundStrokeTipPath = `M14,63 L${50 - halfThickness},85 L${50 + halfThickness},83 L88,60 Z`;
|
138
|
+
icon.innerHTML = `
|
139
|
+
<defs>
|
140
|
+
<pattern
|
141
|
+
id='checkerboard'
|
142
|
+
viewBox='0,0,10,10'
|
143
|
+
width='20%'
|
144
|
+
height='20%'
|
145
|
+
patternUnits='userSpaceOnUse'
|
146
|
+
>
|
147
|
+
<rect x=0 y=0 width=10 height=10 fill='white'/>
|
148
|
+
<rect x=0 y=0 width=5 height=5 fill='gray'/>
|
149
|
+
<rect x=5 y=5 width=5 height=5 fill='gray'/>
|
150
|
+
</pattern>
|
151
|
+
</defs>
|
152
|
+
<g>
|
153
|
+
<!-- Pen grip -->
|
154
|
+
<path
|
155
|
+
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
156
|
+
${primaryForegroundStrokeFill}
|
157
|
+
/>
|
158
|
+
</g>
|
159
|
+
<g>
|
160
|
+
<!-- Checkerboard background for slightly transparent pens -->
|
161
|
+
<path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
|
162
|
+
|
163
|
+
<!-- Actual pen tip -->
|
164
|
+
<path
|
165
|
+
d='${primaryStrokeTipPath}'
|
166
|
+
fill='${color}'
|
167
|
+
stroke='${color}'
|
168
|
+
/>
|
169
|
+
</g>
|
170
|
+
`;
|
171
|
+
return icon;
|
172
|
+
};
|
173
|
+
|
174
|
+
export const makeIconFromFactory = (pen: Pen, factory: ComponentBuilderFactory) => {
|
175
|
+
const toolThickness = pen.getThickness();
|
176
|
+
|
177
|
+
const nowTime = (new Date()).getTime();
|
178
|
+
const startPoint: StrokeDataPoint = {
|
179
|
+
pos: Vec2.of(10, 10),
|
180
|
+
width: toolThickness / 5,
|
181
|
+
color: pen.getColor(),
|
182
|
+
time: nowTime - 100,
|
183
|
+
};
|
184
|
+
const endPoint: StrokeDataPoint = {
|
185
|
+
pos: Vec2.of(90, 90),
|
186
|
+
width: toolThickness / 5,
|
187
|
+
color: pen.getColor(),
|
188
|
+
time: nowTime,
|
189
|
+
};
|
190
|
+
|
191
|
+
const viewport = new Viewport(new EventDispatcher());
|
192
|
+
const builder = factory(startPoint, viewport);
|
193
|
+
builder.addPoint(endPoint);
|
194
|
+
|
195
|
+
const icon = document.createElementNS(svgNamespace, 'svg');
|
196
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
197
|
+
viewport.updateScreenSize(Vec2.of(100, 100));
|
198
|
+
|
199
|
+
const renderer = new SVGRenderer(icon, viewport);
|
200
|
+
builder.preview(renderer);
|
201
|
+
|
202
|
+
return icon;
|
203
|
+
};
|