js-draw 0.0.1
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/.eslintrc.js +57 -0
- package/.husky/pre-commit +4 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/__mocks__/coloris.ts +8 -0
- package/__mocks__/styleMock.js +1 -0
- package/dist/__mocks__/coloris.d.ts +2 -0
- package/dist/__mocks__/coloris.js +5 -0
- package/dist/build_tools/BundledFile.d.ts +12 -0
- package/dist/build_tools/BundledFile.js +153 -0
- package/dist/scripts/bundle.d.ts +1 -0
- package/dist/scripts/bundle.js +19 -0
- package/dist/scripts/watchBundle.d.ts +1 -0
- package/dist/scripts/watchBundle.js +9 -0
- package/dist/src/Color4.d.ts +23 -0
- package/dist/src/Color4.js +102 -0
- package/dist/src/Display.d.ts +22 -0
- package/dist/src/Display.js +93 -0
- package/dist/src/Editor.d.ts +55 -0
- package/dist/src/Editor.js +366 -0
- package/dist/src/EditorImage.d.ts +44 -0
- package/dist/src/EditorImage.js +243 -0
- package/dist/src/EventDispatcher.d.ts +11 -0
- package/dist/src/EventDispatcher.js +39 -0
- package/dist/src/Pointer.d.ts +22 -0
- package/dist/src/Pointer.js +57 -0
- package/dist/src/SVGLoader.d.ts +21 -0
- package/dist/src/SVGLoader.js +204 -0
- package/dist/src/StrokeBuilder.d.ts +35 -0
- package/dist/src/StrokeBuilder.js +275 -0
- package/dist/src/UndoRedoHistory.d.ts +17 -0
- package/dist/src/UndoRedoHistory.js +46 -0
- package/dist/src/Viewport.d.ts +39 -0
- package/dist/src/Viewport.js +134 -0
- package/dist/src/commands/Command.d.ts +15 -0
- package/dist/src/commands/Command.js +29 -0
- package/dist/src/commands/Erase.d.ts +11 -0
- package/dist/src/commands/Erase.js +37 -0
- package/dist/src/commands/localization.d.ts +19 -0
- package/dist/src/commands/localization.js +17 -0
- package/dist/src/components/AbstractComponent.d.ts +19 -0
- package/dist/src/components/AbstractComponent.js +46 -0
- package/dist/src/components/Stroke.d.ts +16 -0
- package/dist/src/components/Stroke.js +79 -0
- package/dist/src/components/UnknownSVGObject.d.ts +15 -0
- package/dist/src/components/UnknownSVGObject.js +25 -0
- package/dist/src/components/localization.d.ts +5 -0
- package/dist/src/components/localization.js +4 -0
- package/dist/src/geometry/LineSegment2.d.ts +19 -0
- package/dist/src/geometry/LineSegment2.js +100 -0
- package/dist/src/geometry/Mat33.d.ts +31 -0
- package/dist/src/geometry/Mat33.js +187 -0
- package/dist/src/geometry/Path.d.ts +55 -0
- package/dist/src/geometry/Path.js +364 -0
- package/dist/src/geometry/Rect2.d.ts +47 -0
- package/dist/src/geometry/Rect2.js +148 -0
- package/dist/src/geometry/Vec2.d.ts +13 -0
- package/dist/src/geometry/Vec2.js +13 -0
- package/dist/src/geometry/Vec3.d.ts +32 -0
- package/dist/src/geometry/Vec3.js +98 -0
- package/dist/src/localization.d.ts +12 -0
- package/dist/src/localization.js +5 -0
- package/dist/src/main.d.ts +3 -0
- package/dist/src/main.js +4 -0
- package/dist/src/rendering/AbstractRenderer.d.ts +38 -0
- package/dist/src/rendering/AbstractRenderer.js +108 -0
- package/dist/src/rendering/CanvasRenderer.d.ts +23 -0
- package/dist/src/rendering/CanvasRenderer.js +108 -0
- package/dist/src/rendering/DummyRenderer.d.ts +25 -0
- package/dist/src/rendering/DummyRenderer.js +65 -0
- package/dist/src/rendering/SVGRenderer.d.ts +27 -0
- package/dist/src/rendering/SVGRenderer.js +122 -0
- package/dist/src/testing/loadExpectExtensions.d.ts +17 -0
- package/dist/src/testing/loadExpectExtensions.js +27 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +12 -0
- package/dist/src/toolbar/HTMLToolbar.js +444 -0
- package/dist/src/toolbar/types.d.ts +17 -0
- package/dist/src/toolbar/types.js +5 -0
- package/dist/src/tools/BaseTool.d.ts +20 -0
- package/dist/src/tools/BaseTool.js +44 -0
- package/dist/src/tools/Eraser.d.ts +16 -0
- package/dist/src/tools/Eraser.js +53 -0
- package/dist/src/tools/PanZoom.d.ts +40 -0
- package/dist/src/tools/PanZoom.js +191 -0
- package/dist/src/tools/Pen.d.ts +25 -0
- package/dist/src/tools/Pen.js +97 -0
- package/dist/src/tools/SelectionTool.d.ts +49 -0
- package/dist/src/tools/SelectionTool.js +437 -0
- package/dist/src/tools/ToolController.d.ts +18 -0
- package/dist/src/tools/ToolController.js +110 -0
- package/dist/src/tools/ToolEnabledGroup.d.ts +6 -0
- package/dist/src/tools/ToolEnabledGroup.js +11 -0
- package/dist/src/tools/localization.d.ts +10 -0
- package/dist/src/tools/localization.js +9 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +20 -0
- package/jest.config.js +22 -0
- package/lint-staged.config.js +6 -0
- package/package.json +82 -0
- package/src/Color4.test.ts +12 -0
- package/src/Color4.ts +122 -0
- package/src/Display.ts +118 -0
- package/src/Editor.css +58 -0
- package/src/Editor.ts +469 -0
- package/src/EditorImage.test.ts +90 -0
- package/src/EditorImage.ts +297 -0
- package/src/EventDispatcher.test.ts +123 -0
- package/src/EventDispatcher.ts +53 -0
- package/src/Pointer.ts +93 -0
- package/src/SVGLoader.ts +230 -0
- package/src/StrokeBuilder.ts +362 -0
- package/src/UndoRedoHistory.ts +61 -0
- package/src/Viewport.ts +168 -0
- package/src/commands/Command.ts +43 -0
- package/src/commands/Erase.ts +52 -0
- package/src/commands/localization.ts +38 -0
- package/src/components/AbstractComponent.ts +73 -0
- package/src/components/Stroke.test.ts +18 -0
- package/src/components/Stroke.ts +102 -0
- package/src/components/UnknownSVGObject.ts +36 -0
- package/src/components/localization.ts +9 -0
- package/src/editorStyles.js +3 -0
- package/src/geometry/LineSegment2.test.ts +77 -0
- package/src/geometry/LineSegment2.ts +127 -0
- package/src/geometry/Mat33.test.ts +144 -0
- package/src/geometry/Mat33.ts +268 -0
- package/src/geometry/Path.fromString.test.ts +146 -0
- package/src/geometry/Path.test.ts +96 -0
- package/src/geometry/Path.toString.test.ts +31 -0
- package/src/geometry/Path.ts +456 -0
- package/src/geometry/Rect2.test.ts +121 -0
- package/src/geometry/Rect2.ts +215 -0
- package/src/geometry/Vec2.test.ts +32 -0
- package/src/geometry/Vec2.ts +18 -0
- package/src/geometry/Vec3.test.ts +29 -0
- package/src/geometry/Vec3.ts +133 -0
- package/src/localization.ts +27 -0
- package/src/rendering/AbstractRenderer.ts +164 -0
- package/src/rendering/CanvasRenderer.ts +141 -0
- package/src/rendering/DummyRenderer.ts +80 -0
- package/src/rendering/SVGRenderer.ts +159 -0
- package/src/testing/loadExpectExtensions.ts +43 -0
- package/src/toolbar/HTMLToolbar.ts +551 -0
- package/src/toolbar/toolbar.css +110 -0
- package/src/toolbar/types.ts +20 -0
- package/src/tools/BaseTool.ts +58 -0
- package/src/tools/Eraser.ts +67 -0
- package/src/tools/PanZoom.ts +253 -0
- package/src/tools/Pen.ts +121 -0
- package/src/tools/SelectionTool.test.ts +85 -0
- package/src/tools/SelectionTool.ts +545 -0
- package/src/tools/ToolController.ts +126 -0
- package/src/tools/ToolEnabledGroup.ts +14 -0
- package/src/tools/localization.ts +22 -0
- package/src/types.ts +133 -0
- package/tsconfig.json +28 -0
@@ -0,0 +1,551 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { ToolType } from '../tools/ToolController';
|
3
|
+
import { EditorEventType } from '../types';
|
4
|
+
|
5
|
+
import { coloris, init as colorisInit } from '@melloware/coloris';
|
6
|
+
import Color4 from '../Color4';
|
7
|
+
import Pen from '../tools/Pen';
|
8
|
+
import Eraser from '../tools/Eraser';
|
9
|
+
import BaseTool from '../tools/BaseTool';
|
10
|
+
import SelectionTool from '../tools/SelectionTool';
|
11
|
+
import { ToolbarLocalization } from './types';
|
12
|
+
|
13
|
+
const primaryForegroundFill = `
|
14
|
+
style='fill: var(--primary-foreground-color);'
|
15
|
+
`;
|
16
|
+
const primaryForegroundStrokeFill = `
|
17
|
+
style='fill: var(--primary-foreground-color); stroke: var(--primary-foreground-color);'
|
18
|
+
`;
|
19
|
+
|
20
|
+
const toolbarCSSPrefix = 'toolbar-';
|
21
|
+
abstract class ToolbarWidget {
|
22
|
+
protected readonly container: HTMLElement;
|
23
|
+
private button: HTMLElement;
|
24
|
+
private icon: Element|null;
|
25
|
+
private dropdownContainer: HTMLElement;
|
26
|
+
private dropdownIcon: Element;
|
27
|
+
private label: HTMLLabelElement;
|
28
|
+
private hasDropdown: boolean;
|
29
|
+
|
30
|
+
public constructor(
|
31
|
+
protected editor: Editor,
|
32
|
+
protected targetTool: BaseTool,
|
33
|
+
protected localizationTable: ToolbarLocalization,
|
34
|
+
) {
|
35
|
+
this.icon = null;
|
36
|
+
this.container = document.createElement('div');
|
37
|
+
this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
|
38
|
+
this.dropdownContainer = document.createElement('div');
|
39
|
+
this.dropdownContainer.classList.add(`${toolbarCSSPrefix}dropdown`);
|
40
|
+
this.dropdownContainer.classList.add('hidden');
|
41
|
+
this.hasDropdown = false;
|
42
|
+
|
43
|
+
this.button = document.createElement('div');
|
44
|
+
this.button.classList.add(`${toolbarCSSPrefix}button`);
|
45
|
+
this.label = document.createElement('label');
|
46
|
+
this.button.setAttribute('role', 'button');
|
47
|
+
this.button.tabIndex = 0;
|
48
|
+
|
49
|
+
this.button.onclick = () => {
|
50
|
+
this.handleClick();
|
51
|
+
};
|
52
|
+
|
53
|
+
|
54
|
+
editor.notifier.on(EditorEventType.ToolEnabled, toolEvt => {
|
55
|
+
if (toolEvt.kind !== EditorEventType.ToolEnabled) {
|
56
|
+
throw new Error('Incorrect event type! (Expected ToolEnabled)');
|
57
|
+
}
|
58
|
+
|
59
|
+
if (toolEvt.tool === targetTool) {
|
60
|
+
this.updateSelected(true);
|
61
|
+
}
|
62
|
+
});
|
63
|
+
|
64
|
+
editor.notifier.on(EditorEventType.ToolDisabled, toolEvt => {
|
65
|
+
if (toolEvt.kind !== EditorEventType.ToolDisabled) {
|
66
|
+
throw new Error('Incorrect event type! (Expected ToolDisabled)');
|
67
|
+
}
|
68
|
+
|
69
|
+
if (toolEvt.tool === targetTool) {
|
70
|
+
this.updateSelected(false);
|
71
|
+
this.setDropdownVisible(false);
|
72
|
+
}
|
73
|
+
});
|
74
|
+
}
|
75
|
+
|
76
|
+
protected abstract getTitle(): string;
|
77
|
+
protected abstract createIcon(): Element;
|
78
|
+
|
79
|
+
// Add content to the widget's associated dropdown menu.
|
80
|
+
// Returns true if such a menu should be created, false otherwise.
|
81
|
+
protected abstract fillDropdown(dropdown: HTMLElement): boolean;
|
82
|
+
|
83
|
+
protected handleClick() {
|
84
|
+
if (this.hasDropdown) {
|
85
|
+
if (!this.targetTool.isEnabled()) {
|
86
|
+
this.targetTool.setEnabled(true);
|
87
|
+
} else {
|
88
|
+
this.setDropdownVisible(!this.isDropdownVisible());
|
89
|
+
}
|
90
|
+
} else {
|
91
|
+
this.targetTool.setEnabled(!this.targetTool.isEnabled());
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
// Adds this to [parent]. This can only be called once for each ToolbarWidget.
|
96
|
+
public addTo(parent: HTMLElement) {
|
97
|
+
this.label.innerText = this.getTitle();
|
98
|
+
|
99
|
+
this.icon = null;
|
100
|
+
this.updateIcon();
|
101
|
+
|
102
|
+
this.updateSelected(this.targetTool.isEnabled());
|
103
|
+
|
104
|
+
this.button.replaceChildren(this.icon!, this.label);
|
105
|
+
this.container.appendChild(this.button);
|
106
|
+
|
107
|
+
this.hasDropdown = this.fillDropdown(this.dropdownContainer);
|
108
|
+
if (this.hasDropdown) {
|
109
|
+
this.dropdownIcon = this.createDropdownIcon();
|
110
|
+
this.button.appendChild(this.dropdownIcon);
|
111
|
+
this.container.appendChild(this.dropdownContainer);
|
112
|
+
}
|
113
|
+
|
114
|
+
this.setDropdownVisible(false);
|
115
|
+
parent.appendChild(this.container);
|
116
|
+
}
|
117
|
+
|
118
|
+
protected updateIcon() {
|
119
|
+
const newIcon = this.createIcon();
|
120
|
+
this.icon?.replaceWith(newIcon);
|
121
|
+
this.icon = newIcon;
|
122
|
+
this.icon.classList.add(`${toolbarCSSPrefix}icon`);
|
123
|
+
}
|
124
|
+
|
125
|
+
protected updateSelected(selected: boolean) {
|
126
|
+
const currentlySelected = this.container.classList.contains('selected');
|
127
|
+
if (currentlySelected === selected) {
|
128
|
+
return;
|
129
|
+
}
|
130
|
+
|
131
|
+
if (selected) {
|
132
|
+
this.container.classList.add('selected');
|
133
|
+
this.button.ariaSelected = 'true';
|
134
|
+
} else {
|
135
|
+
this.container.classList.remove('selected');
|
136
|
+
this.button.ariaSelected = 'false';
|
137
|
+
}
|
138
|
+
}
|
139
|
+
|
140
|
+
protected setDropdownVisible(visible: boolean) {
|
141
|
+
const currentlyVisible = this.container.classList.contains('dropdownVisible');
|
142
|
+
if (currentlyVisible === visible) {
|
143
|
+
return;
|
144
|
+
}
|
145
|
+
|
146
|
+
if (visible) {
|
147
|
+
this.dropdownContainer.classList.remove('hidden');
|
148
|
+
this.container.classList.add('dropdownVisible');
|
149
|
+
this.editor.announceForAccessibility(
|
150
|
+
this.localizationTable.dropdownShown(this.targetTool.description)
|
151
|
+
);
|
152
|
+
} else {
|
153
|
+
this.dropdownContainer.classList.add('hidden');
|
154
|
+
this.container.classList.remove('dropdownVisible');
|
155
|
+
this.editor.announceForAccessibility(
|
156
|
+
this.localizationTable.dropdownHidden(this.targetTool.description)
|
157
|
+
);
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
protected isDropdownVisible(): boolean {
|
162
|
+
return !this.dropdownContainer.classList.contains('hidden');
|
163
|
+
}
|
164
|
+
|
165
|
+
private createDropdownIcon(): Element {
|
166
|
+
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
167
|
+
icon.innerHTML = `
|
168
|
+
<g>
|
169
|
+
<path
|
170
|
+
d='M5,10 L50,90 L95,10 Z'
|
171
|
+
${primaryForegroundFill}
|
172
|
+
/>
|
173
|
+
</g>
|
174
|
+
`;
|
175
|
+
icon.classList.add(`${toolbarCSSPrefix}showHideDropdownIcon`);
|
176
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
177
|
+
return icon;
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
class EraserWidget extends ToolbarWidget {
|
182
|
+
protected getTitle(): string {
|
183
|
+
return this.localizationTable.eraser;
|
184
|
+
}
|
185
|
+
protected createIcon(): Element {
|
186
|
+
const icon = document.createElementNS(
|
187
|
+
'http://www.w3.org/2000/svg', 'svg'
|
188
|
+
);
|
189
|
+
|
190
|
+
// Draw an eraser-like shape
|
191
|
+
icon.innerHTML = `
|
192
|
+
<g>
|
193
|
+
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
194
|
+
<rect
|
195
|
+
x=10 y=10 width=80 height=50
|
196
|
+
${primaryForegroundFill}
|
197
|
+
/>
|
198
|
+
</g>
|
199
|
+
`;
|
200
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
201
|
+
|
202
|
+
return icon;
|
203
|
+
}
|
204
|
+
|
205
|
+
protected fillDropdown(_dropdown: HTMLElement): boolean {
|
206
|
+
// No dropdown associated with the eraser
|
207
|
+
return false;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
class SelectionWidget extends ToolbarWidget {
|
212
|
+
public constructor(
|
213
|
+
editor: Editor, private tool: SelectionTool, localization: ToolbarLocalization
|
214
|
+
) {
|
215
|
+
super(editor, tool, localization);
|
216
|
+
}
|
217
|
+
|
218
|
+
protected getTitle(): string {
|
219
|
+
return this.localizationTable.select;
|
220
|
+
}
|
221
|
+
|
222
|
+
protected createIcon(): Element {
|
223
|
+
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
224
|
+
|
225
|
+
// Draw a cursor-like shape
|
226
|
+
icon.innerHTML = `
|
227
|
+
<g>
|
228
|
+
<rect x=10 y=10 width=70 height=70 fill='pink' stroke='black'/>
|
229
|
+
<rect x=75 y=75 width=10 height=10 fill='white' stroke='black'/>
|
230
|
+
</g>
|
231
|
+
`;
|
232
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
233
|
+
|
234
|
+
return icon;
|
235
|
+
}
|
236
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
237
|
+
const container = document.createElement('div');
|
238
|
+
const resizeButton = document.createElement('button');
|
239
|
+
|
240
|
+
resizeButton.innerText = this.localizationTable.resizeImageToSelection;
|
241
|
+
resizeButton.disabled = true;
|
242
|
+
|
243
|
+
resizeButton.onclick = () => {
|
244
|
+
const selection = this.tool.getSelection();
|
245
|
+
this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
|
246
|
+
};
|
247
|
+
|
248
|
+
// Enable/disable actions based on whether items are selected
|
249
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
250
|
+
if (toolEvt.kind !== EditorEventType.ToolUpdated) {
|
251
|
+
throw new Error('Invalid event type!');
|
252
|
+
}
|
253
|
+
|
254
|
+
if (toolEvt.tool === this.tool) {
|
255
|
+
const selection = this.tool.getSelection();
|
256
|
+
const hasSelection = selection && selection.region.area > 0;
|
257
|
+
resizeButton.disabled = !hasSelection;
|
258
|
+
}
|
259
|
+
});
|
260
|
+
|
261
|
+
container.replaceChildren(resizeButton);
|
262
|
+
dropdown.appendChild(container);
|
263
|
+
return true;
|
264
|
+
}
|
265
|
+
}
|
266
|
+
|
267
|
+
class TouchDrawingWidget extends ToolbarWidget {
|
268
|
+
protected getTitle(): string {
|
269
|
+
return this.localizationTable.touchDrawing;
|
270
|
+
}
|
271
|
+
|
272
|
+
protected createIcon(): Element {
|
273
|
+
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
274
|
+
|
275
|
+
// Draw a cursor-like shape
|
276
|
+
icon.innerHTML = `
|
277
|
+
<g>
|
278
|
+
<path d='M11,-30 Q0,10 20,20 Q40,20 40,-30 Z' fill='blue' stroke='black'/>
|
279
|
+
<path d='
|
280
|
+
M0,90 L0,50 Q5,40 10,50
|
281
|
+
L10,20 Q20,15 30,20
|
282
|
+
L30,50 Q50,40 80,50
|
283
|
+
L80,90 L10,90 Z'
|
284
|
+
|
285
|
+
${primaryForegroundStrokeFill}
|
286
|
+
/>
|
287
|
+
</g>
|
288
|
+
`;
|
289
|
+
icon.setAttribute('viewBox', '-10 -30 100 100');
|
290
|
+
|
291
|
+
return icon;
|
292
|
+
}
|
293
|
+
protected fillDropdown(_dropdown: HTMLElement): boolean {
|
294
|
+
// No dropdown
|
295
|
+
return false;
|
296
|
+
}
|
297
|
+
protected updateSelected(active: boolean) {
|
298
|
+
if (active) {
|
299
|
+
this.container.classList.remove('selected');
|
300
|
+
} else {
|
301
|
+
this.container.classList.add('selected');
|
302
|
+
}
|
303
|
+
}
|
304
|
+
}
|
305
|
+
|
306
|
+
class PenWidget extends ToolbarWidget {
|
307
|
+
private updateInputs: ()=> void = () => {};
|
308
|
+
|
309
|
+
public constructor(
|
310
|
+
editor: Editor, private tool: Pen, localization: ToolbarLocalization
|
311
|
+
) {
|
312
|
+
super(editor, tool, localization);
|
313
|
+
|
314
|
+
this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
|
315
|
+
if (toolEvt.kind !== EditorEventType.ToolUpdated) {
|
316
|
+
throw new Error('Invalid event type!');
|
317
|
+
}
|
318
|
+
|
319
|
+
// The button icon may depend on tool properties.
|
320
|
+
if (toolEvt.tool === this.tool) {
|
321
|
+
this.updateIcon();
|
322
|
+
this.updateInputs();
|
323
|
+
}
|
324
|
+
});
|
325
|
+
}
|
326
|
+
|
327
|
+
protected getTitle(): string {
|
328
|
+
return this.targetTool.description;
|
329
|
+
}
|
330
|
+
|
331
|
+
protected createIcon(): Element {
|
332
|
+
// We need to use createElementNS to embed an SVG element in HTML.
|
333
|
+
// See http://zhangwenli.com/blog/2017/07/26/createelementns/
|
334
|
+
const icon = document.createElementNS(
|
335
|
+
'http://www.w3.org/2000/svg', 'svg'
|
336
|
+
);
|
337
|
+
|
338
|
+
// Use a square-root scale to prevent the pen's tip from overflowing.
|
339
|
+
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 2);
|
340
|
+
const color = this.tool.getColor();
|
341
|
+
|
342
|
+
// Draw a pen-like shape
|
343
|
+
const primaryStrokeTipPath = `M14,63 L${50 - scale},95 L${50 + scale},90 L88,60 Z`;
|
344
|
+
const backgroundStrokeTipPath = `M14,63 L${50 - scale},85 L${50 + scale},83 L88,60 Z`;
|
345
|
+
icon.innerHTML = `
|
346
|
+
<defs>
|
347
|
+
<pattern
|
348
|
+
id='checkerboard'
|
349
|
+
viewBox='0,0,10,10'
|
350
|
+
width='20%'
|
351
|
+
height='20%'
|
352
|
+
patternUnits='userSpaceOnUse'
|
353
|
+
>
|
354
|
+
<rect x=0 y=0 width=10 height=10 fill='white'/>
|
355
|
+
<rect x=0 y=0 width=5 height=5 fill='gray'/>
|
356
|
+
<rect x=5 y=5 width=5 height=5 fill='gray'/>
|
357
|
+
</pattern>
|
358
|
+
</defs>
|
359
|
+
<g>
|
360
|
+
<!-- Pen grip -->
|
361
|
+
<path
|
362
|
+
d='M10,10 L90,10 L90,60 L${50 + scale},80 L${50 - scale},80 L10,60 Z'
|
363
|
+
${primaryForegroundStrokeFill}
|
364
|
+
/>
|
365
|
+
</g>
|
366
|
+
<g>
|
367
|
+
<!-- Checkerboard background for slightly transparent pens -->
|
368
|
+
<path d='${backgroundStrokeTipPath}' fill='url(#checkerboard)'/>
|
369
|
+
|
370
|
+
<!-- Actual pen tip -->
|
371
|
+
<path
|
372
|
+
d='${primaryStrokeTipPath}'
|
373
|
+
fill='${color.toHexString()}'
|
374
|
+
stroke='${color.toHexString()}'
|
375
|
+
/>
|
376
|
+
</g>
|
377
|
+
`;
|
378
|
+
icon.setAttribute('viewBox', '0 0 100 100');
|
379
|
+
|
380
|
+
return icon;
|
381
|
+
}
|
382
|
+
|
383
|
+
private static idCounter: number = 0;
|
384
|
+
protected fillDropdown(dropdown: HTMLElement): boolean {
|
385
|
+
const container = document.createElement('div');
|
386
|
+
|
387
|
+
// Thickness: Value of the input is squared to allow for finer control/larger values.
|
388
|
+
const thicknessRow = document.createElement('div');
|
389
|
+
const thicknessLabel = document.createElement('label');
|
390
|
+
const thicknessInput = document.createElement('input');
|
391
|
+
|
392
|
+
thicknessInput.id = `${toolbarCSSPrefix}thicknessInput${PenWidget.idCounter++}`;
|
393
|
+
|
394
|
+
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
395
|
+
thicknessLabel.setAttribute('for', thicknessInput.id);
|
396
|
+
|
397
|
+
thicknessInput.type = 'range';
|
398
|
+
thicknessInput.min = '1';
|
399
|
+
thicknessInput.max = '20';
|
400
|
+
thicknessInput.step = '1';
|
401
|
+
thicknessInput.oninput = () => {
|
402
|
+
this.tool.setThickness(parseFloat(thicknessInput.value) ** 2);
|
403
|
+
};
|
404
|
+
thicknessRow.appendChild(thicknessLabel);
|
405
|
+
thicknessRow.appendChild(thicknessInput);
|
406
|
+
|
407
|
+
const colorRow = document.createElement('div');
|
408
|
+
const colorLabel = document.createElement('label');
|
409
|
+
const colorInput = document.createElement('input');
|
410
|
+
|
411
|
+
colorInput.id = `${toolbarCSSPrefix}colorInput${PenWidget.idCounter++}`;
|
412
|
+
colorLabel.innerText = this.localizationTable.colorLabel;
|
413
|
+
colorLabel.setAttribute('for', colorInput.id);
|
414
|
+
|
415
|
+
colorInput.className = 'coloris_input';
|
416
|
+
colorInput.type = 'button';
|
417
|
+
colorInput.oninput = () => {
|
418
|
+
this.tool.setColor(Color4.fromHex(colorInput.value));
|
419
|
+
};
|
420
|
+
|
421
|
+
colorRow.appendChild(colorLabel);
|
422
|
+
colorRow.appendChild(colorInput);
|
423
|
+
|
424
|
+
this.updateInputs = () => {
|
425
|
+
colorInput.value = this.tool.getColor().toHexString();
|
426
|
+
thicknessInput.value = Math.sqrt(this.tool.getThickness()).toString();
|
427
|
+
};
|
428
|
+
this.updateInputs();
|
429
|
+
|
430
|
+
container.replaceChildren(colorRow, thicknessRow);
|
431
|
+
dropdown.replaceChildren(container);
|
432
|
+
return true;
|
433
|
+
}
|
434
|
+
}
|
435
|
+
|
436
|
+
export default class HTMLToolbar {
|
437
|
+
private container: HTMLElement;
|
438
|
+
|
439
|
+
public static defaultLocalization: ToolbarLocalization = {
|
440
|
+
pen: 'Pen',
|
441
|
+
eraser: 'Eraser',
|
442
|
+
select: 'Select',
|
443
|
+
touchDrawing: 'Touch Drawing',
|
444
|
+
thicknessLabel: 'Thickness: ',
|
445
|
+
colorLabel: 'Color: ',
|
446
|
+
resizeImageToSelection: 'Resize image to selection',
|
447
|
+
undo: 'Undo',
|
448
|
+
redo: 'Redo',
|
449
|
+
|
450
|
+
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
451
|
+
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
452
|
+
};
|
453
|
+
|
454
|
+
public constructor(
|
455
|
+
private editor: Editor, parent: HTMLElement,
|
456
|
+
private localizationTable: ToolbarLocalization = HTMLToolbar.defaultLocalization,
|
457
|
+
) {
|
458
|
+
this.container = document.createElement('div');
|
459
|
+
this.container.classList.add(`${toolbarCSSPrefix}root`);
|
460
|
+
this.container.setAttribute('role', 'toolbar');
|
461
|
+
this.addElements();
|
462
|
+
parent.appendChild(this.container);
|
463
|
+
|
464
|
+
// Initialize color choosers
|
465
|
+
colorisInit();
|
466
|
+
coloris({
|
467
|
+
el: '.coloris_input',
|
468
|
+
format: 'hex',
|
469
|
+
selectInput: false,
|
470
|
+
focusInput: false,
|
471
|
+
themeMode: 'auto',
|
472
|
+
|
473
|
+
swatches: [
|
474
|
+
Color4.red.toHexString(),
|
475
|
+
Color4.purple.toHexString(),
|
476
|
+
Color4.blue.toHexString(),
|
477
|
+
Color4.clay.toHexString(),
|
478
|
+
Color4.black.toHexString(),
|
479
|
+
Color4.white.toHexString(),
|
480
|
+
],
|
481
|
+
});
|
482
|
+
}
|
483
|
+
|
484
|
+
public addActionButton(text: string, command: ()=> void, parent?: Element) {
|
485
|
+
const button = document.createElement('button');
|
486
|
+
button.innerText = text;
|
487
|
+
button.classList.add(`${toolbarCSSPrefix}toolButton`);
|
488
|
+
button.onclick = command;
|
489
|
+
(parent ?? this.container).appendChild(button);
|
490
|
+
|
491
|
+
return button;
|
492
|
+
}
|
493
|
+
|
494
|
+
private addUndoRedoButtons() {
|
495
|
+
const undoRedoGroup = document.createElement('div');
|
496
|
+
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
497
|
+
|
498
|
+
const undoButton = this.addActionButton('Undo', () => {
|
499
|
+
this.editor.history.undo();
|
500
|
+
}, undoRedoGroup);
|
501
|
+
const redoButton = this.addActionButton('Redo', () => {
|
502
|
+
this.editor.history.redo();
|
503
|
+
}, undoRedoGroup);
|
504
|
+
this.container.appendChild(undoRedoGroup);
|
505
|
+
|
506
|
+
undoButton.disabled = true;
|
507
|
+
redoButton.disabled = true;
|
508
|
+
this.editor.notifier.on(EditorEventType.UndoRedoStackUpdated, event => {
|
509
|
+
if (event.kind !== EditorEventType.UndoRedoStackUpdated) {
|
510
|
+
throw new Error('Wrong event type!');
|
511
|
+
}
|
512
|
+
|
513
|
+
undoButton.disabled = event.undoStackSize === 0;
|
514
|
+
redoButton.disabled = event.redoStackSize === 0;
|
515
|
+
});
|
516
|
+
}
|
517
|
+
|
518
|
+
private addElements() {
|
519
|
+
const toolController = this.editor.toolController;
|
520
|
+
for (const tool of toolController.getMatchingTools(ToolType.Pen)) {
|
521
|
+
if (!(tool instanceof Pen)) {
|
522
|
+
throw new Error('All `Pen` tools must have kind === ToolType.Pen');
|
523
|
+
}
|
524
|
+
|
525
|
+
const widget = new PenWidget(this.editor, tool, this.localizationTable);
|
526
|
+
widget.addTo(this.container);
|
527
|
+
}
|
528
|
+
|
529
|
+
for (const tool of toolController.getMatchingTools(ToolType.Eraser)) {
|
530
|
+
if (!(tool instanceof Eraser)) {
|
531
|
+
throw new Error('All Erasers must have kind === ToolType.Eraser!');
|
532
|
+
}
|
533
|
+
|
534
|
+
(new EraserWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
535
|
+
}
|
536
|
+
|
537
|
+
for (const tool of toolController.getMatchingTools(ToolType.Selection)) {
|
538
|
+
if (!(tool instanceof SelectionTool)) {
|
539
|
+
throw new Error('All SelectionTools must have kind === ToolType.Selection');
|
540
|
+
}
|
541
|
+
|
542
|
+
(new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
543
|
+
}
|
544
|
+
|
545
|
+
for (const tool of toolController.getMatchingTools(ToolType.TouchPanZoom)) {
|
546
|
+
(new TouchDrawingWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
547
|
+
}
|
548
|
+
|
549
|
+
this.addUndoRedoButtons();
|
550
|
+
}
|
551
|
+
}
|
@@ -0,0 +1,110 @@
|
|
1
|
+
.toolbar-root {
|
2
|
+
background-color: var(--primary-background-color);
|
3
|
+
|
4
|
+
border: 1px solid var(--secondary-background-color);
|
5
|
+
border-radius: 2px;
|
6
|
+
flex-wrap: wrap;
|
7
|
+
|
8
|
+
box-sizing: border-box;
|
9
|
+
width: 100%;
|
10
|
+
|
11
|
+
display: flex;
|
12
|
+
flex-direction: row;
|
13
|
+
justify-content: center;
|
14
|
+
|
15
|
+
font-family: system-ui, -apple-system, sans-serif;
|
16
|
+
}
|
17
|
+
|
18
|
+
.toolbar-button, .toolbar-root button {
|
19
|
+
display: flex;
|
20
|
+
flex-direction: column;
|
21
|
+
align-items: center;
|
22
|
+
justify-content: center;
|
23
|
+
|
24
|
+
text-align: center;
|
25
|
+
border-radius: 6px;
|
26
|
+
cursor: pointer;
|
27
|
+
|
28
|
+
padding-left: 3px;
|
29
|
+
padding-right: 3px;
|
30
|
+
margin-left: 3px;
|
31
|
+
margin-right: 3px;
|
32
|
+
|
33
|
+
min-width: 40px;
|
34
|
+
max-width: 70px;
|
35
|
+
font-size: 11pt;
|
36
|
+
|
37
|
+
cursor: pointer;
|
38
|
+
|
39
|
+
height: min(20vh, 60px);
|
40
|
+
background-color: var(--primary-background-color);
|
41
|
+
color: var(--primary-foreground-color);
|
42
|
+
border: none;
|
43
|
+
box-shadow: 0px 0px 2px var(--primary-foreground-color);
|
44
|
+
|
45
|
+
transition: background-color 0.25s ease, box-shadow 0.25s ease, opacity 0.3s ease;
|
46
|
+
}
|
47
|
+
|
48
|
+
.toolbar-button:hover, .toolbar-root button:not(:disabled):hover {
|
49
|
+
box-shadow: 0px 2px 4px var(--primary-foreground-color);
|
50
|
+
}
|
51
|
+
|
52
|
+
.toolbar-root button {
|
53
|
+
height: auto;
|
54
|
+
}
|
55
|
+
|
56
|
+
.toolbar-root button:disabled {
|
57
|
+
cursor: inherit;
|
58
|
+
filter: opacity(0.5);
|
59
|
+
}
|
60
|
+
|
61
|
+
.toolbar-button .toolbar-icon {
|
62
|
+
flex-shrink: 1;
|
63
|
+
min-width: 30px;
|
64
|
+
}
|
65
|
+
|
66
|
+
.toolbar-toolContainer.selected .toolbar-button {
|
67
|
+
background-color: var(--secondary-background-color);
|
68
|
+
color: var(--secondary-foreground-color);
|
69
|
+
}
|
70
|
+
|
71
|
+
.toolbar-toolContainer:not(.selected) .toolbar-showHideDropdownIcon {
|
72
|
+
display: none;
|
73
|
+
}
|
74
|
+
|
75
|
+
.toolbar-toolContainer.selected .toolbar-showHideDropdownIcon {
|
76
|
+
height: 10px;
|
77
|
+
transition: transform 0.5s ease;
|
78
|
+
}
|
79
|
+
|
80
|
+
.toolbar-toolContainer.dropdownVisible .toolbar-showHideDropdownIcon {
|
81
|
+
transform: rotate(180deg);
|
82
|
+
}
|
83
|
+
|
84
|
+
.toolbar-dropdown.hidden, .toolbar-toolContainer:not(.selected) > .toolbar-dropdown {
|
85
|
+
display: none;
|
86
|
+
}
|
87
|
+
|
88
|
+
.toolbar-dropdown {
|
89
|
+
position: absolute;
|
90
|
+
padding: 15px;
|
91
|
+
padding-top: 5px;
|
92
|
+
/* Prevent overlap/being displayed under the undo/redo buttons */
|
93
|
+
z-index: 2;
|
94
|
+
background-color: var(--primary-background-color);
|
95
|
+
box-shadow: 0px 3px 3px var(--primary-foreground-color);
|
96
|
+
}
|
97
|
+
|
98
|
+
.toolbar-buttonGroup {
|
99
|
+
display: flex;
|
100
|
+
flex-direction: row;
|
101
|
+
}
|
102
|
+
|
103
|
+
/* Make color selection buttons fill their containing label */
|
104
|
+
.toolbar-dropdown .clr-field button {
|
105
|
+
width: 100%;
|
106
|
+
height: 100%;
|
107
|
+
border-radius: 2px;
|
108
|
+
margin-left: 0;
|
109
|
+
margin-right: 0;
|
110
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
export enum ToolbarButtonType {
|
2
|
+
ToggleButton,
|
3
|
+
ActionButton,
|
4
|
+
}
|
5
|
+
|
6
|
+
|
7
|
+
export interface ToolbarLocalization {
|
8
|
+
colorLabel: string;
|
9
|
+
pen: string;
|
10
|
+
eraser: string;
|
11
|
+
select: string;
|
12
|
+
touchDrawing: string;
|
13
|
+
thicknessLabel: string;
|
14
|
+
resizeImageToSelection: string;
|
15
|
+
undo: string;
|
16
|
+
redo: string;
|
17
|
+
|
18
|
+
dropdownShown: (toolName: string)=>string;
|
19
|
+
dropdownHidden: (toolName: string)=>string;
|
20
|
+
}
|