js-draw 0.11.3 → 0.13.0
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 +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +13 -0
- package/dist/src/Color4.js +17 -0
- package/dist/src/Editor.d.ts +33 -18
- package/dist/src/Editor.js +26 -24
- package/dist/src/EditorImage.d.ts +12 -0
- package/dist/src/EditorImage.js +12 -0
- package/dist/src/Pointer.d.ts +1 -0
- package/dist/src/Pointer.js +8 -0
- package/dist/src/SVGLoader.d.ts +5 -0
- package/dist/src/SVGLoader.js +49 -36
- package/dist/src/Viewport.d.ts +30 -1
- package/dist/src/Viewport.js +39 -9
- package/dist/src/commands/invertCommand.js +1 -1
- package/dist/src/components/AbstractComponent.d.ts +20 -0
- package/dist/src/components/AbstractComponent.js +32 -2
- package/dist/src/lib.d.ts +6 -3
- package/dist/src/lib.js +4 -1
- package/dist/src/math/Mat33.d.ts +1 -1
- package/dist/src/math/Mat33.js +1 -1
- package/dist/src/rendering/Display.d.ts +9 -11
- package/dist/src/rendering/Display.js +12 -14
- package/dist/src/rendering/lib.d.ts +3 -0
- package/dist/src/rendering/lib.js +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +2 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +4 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +51 -0
- package/dist/src/toolbar/HTMLToolbar.js +63 -5
- package/dist/src/toolbar/IconProvider.d.ts +2 -2
- package/dist/src/toolbar/IconProvider.js +123 -35
- package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +8 -1
- package/dist/src/toolbar/widgets/EraserToolWidget.js +45 -4
- package/dist/src/toolbar/widgets/PenToolWidget.js +2 -2
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +12 -3
- package/dist/src/tools/Eraser.d.ts +10 -1
- package/dist/src/tools/Eraser.js +65 -13
- package/dist/src/tools/PanZoom.js +1 -1
- package/dist/src/tools/PasteHandler.d.ts +11 -4
- package/dist/src/tools/PasteHandler.js +12 -5
- package/dist/src/tools/Pen.d.ts +7 -2
- package/dist/src/tools/Pen.js +39 -6
- package/dist/src/tools/SelectionTool/Selection.d.ts +4 -1
- package/dist/src/tools/SelectionTool/Selection.js +64 -27
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/src/tools/SelectionTool/SelectionTool.js +56 -16
- package/dist/src/tools/TextTool.js +10 -6
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
- package/dist/src/tools/UndoRedoShortcut.js +2 -4
- package/dist/src/types.d.ts +2 -2
- package/package.json +2 -2
- package/src/Color4.test.ts +11 -0
- package/src/Color4.ts +23 -0
- package/src/Editor.ts +39 -26
- package/src/EditorImage.ts +12 -0
- package/src/Pointer.ts +19 -0
- package/src/SVGLoader.ts +20 -15
- package/src/Viewport.ts +50 -11
- package/src/commands/invertCommand.ts +1 -1
- package/src/components/AbstractComponent.ts +52 -2
- package/src/lib.ts +6 -3
- package/src/math/Mat33.ts +1 -1
- package/src/rendering/Display.ts +12 -15
- package/src/rendering/RenderingStyle.ts +1 -1
- package/src/rendering/lib.ts +4 -0
- package/src/rendering/renderers/DummyRenderer.ts +2 -3
- package/src/rendering/renderers/SVGRenderer.ts +4 -0
- package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
- package/src/toolbar/HTMLToolbar.ts +81 -5
- package/src/toolbar/IconProvider.ts +132 -37
- package/src/toolbar/widgets/EraserToolWidget.ts +64 -5
- package/src/toolbar/widgets/PenToolWidget.ts +2 -2
- package/src/toolbar/widgets/SelectionToolWidget.ts +2 -2
- package/src/tools/Eraser.test.ts +79 -0
- package/src/tools/Eraser.ts +81 -17
- package/src/tools/PanZoom.ts +1 -1
- package/src/tools/PasteHandler.ts +12 -6
- package/src/tools/Pen.test.ts +44 -1
- package/src/tools/Pen.ts +53 -8
- package/src/tools/SelectionTool/Selection.ts +73 -23
- package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
- package/src/tools/SelectionTool/SelectionTool.test.ts +138 -21
- package/src/tools/SelectionTool/SelectionTool.ts +70 -16
- package/src/tools/TextTool.ts +14 -8
- package/src/tools/ToolSwitcherShortcut.ts +10 -5
- package/src/tools/UndoRedoShortcut.ts +2 -5
- package/src/types.ts +2 -2
- package/typedoc.json +2 -2
@@ -1,6 +1,3 @@
|
|
1
|
-
// Allows users to select/transform portions of the `EditorImage`.
|
2
|
-
// With respect to `extend`ing, `SelectionTool` is not stable.
|
3
|
-
// @packageDocumentation
|
4
1
|
import Mat33 from '../../math/Mat33';
|
5
2
|
import { Vec2 } from '../../math/Vec2';
|
6
3
|
import { EditorEventType } from '../../types';
|
@@ -10,7 +7,8 @@ import SVGRenderer from '../../rendering/renderers/SVGRenderer';
|
|
10
7
|
import Selection from './Selection';
|
11
8
|
import TextComponent from '../../components/TextComponent';
|
12
9
|
export const cssPrefix = 'selection-tool-';
|
13
|
-
//
|
10
|
+
// Allows users to select/transform portions of the `EditorImage`.
|
11
|
+
// With respect to `extend`ing, `SelectionTool` is not stable.
|
14
12
|
export default class SelectionTool extends BaseTool {
|
15
13
|
constructor(editor, description) {
|
16
14
|
super(editor.notifier, description);
|
@@ -18,6 +16,7 @@ export default class SelectionTool extends BaseTool {
|
|
18
16
|
this.lastEvtTarget = null;
|
19
17
|
this.expandingSelectionBox = false;
|
20
18
|
this.shiftKeyPressed = false;
|
19
|
+
this.ctrlKeyPressed = false;
|
21
20
|
this.selectionBoxHandlingEvt = false;
|
22
21
|
this.handleOverlay = document.createElement('div');
|
23
22
|
editor.createHTMLOverlay(this.handleOverlay);
|
@@ -45,17 +44,37 @@ export default class SelectionTool extends BaseTool {
|
|
45
44
|
}
|
46
45
|
this.selectionBox.addTo(this.handleOverlay);
|
47
46
|
}
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
snapSelectionToGrid() {
|
48
|
+
if (!this.selectionBox)
|
49
|
+
throw new Error('No selection to snap!');
|
50
|
+
const topLeftOfBBox = this.selectionBox.region.topLeft;
|
51
|
+
const snapDistance = this.editor.viewport.snapToGrid(topLeftOfBBox).minus(topLeftOfBBox);
|
52
|
+
const oldTransform = this.selectionBox.getTransform();
|
53
|
+
this.selectionBox.setTransform(oldTransform.rightMul(Mat33.translation(snapDistance)));
|
54
|
+
this.selectionBox.finalizeTransform();
|
55
|
+
}
|
56
|
+
onPointerDown({ allPointers, current }) {
|
57
|
+
const snapToGrid = this.ctrlKeyPressed;
|
58
|
+
if (snapToGrid) {
|
59
|
+
current = current.snappedToGrid(this.editor.viewport);
|
60
|
+
}
|
61
|
+
if (allPointers.length === 1 && current.isPrimary) {
|
62
|
+
let transforming = false;
|
63
|
+
if (this.lastEvtTarget && this.selectionBox) {
|
64
|
+
if (snapToGrid) {
|
65
|
+
this.snapSelectionToGrid();
|
66
|
+
}
|
67
|
+
const dragStartResult = this.selectionBox.onDragStart(current, this.lastEvtTarget);
|
68
|
+
if (dragStartResult) {
|
69
|
+
transforming = true;
|
70
|
+
this.selectionBoxHandlingEvt = true;
|
71
|
+
this.expandingSelectionBox = false;
|
72
|
+
}
|
54
73
|
}
|
55
|
-
|
74
|
+
if (!transforming) {
|
56
75
|
// Shift key: Combine the new and old selection boxes at the end of the gesture.
|
57
76
|
this.expandingSelectionBox = this.shiftKeyPressed;
|
58
|
-
this.makeSelectionBox(
|
77
|
+
this.makeSelectionBox(current.canvasPos);
|
59
78
|
}
|
60
79
|
return true;
|
61
80
|
}
|
@@ -64,11 +83,15 @@ export default class SelectionTool extends BaseTool {
|
|
64
83
|
onPointerMove(event) {
|
65
84
|
if (!this.selectionBox)
|
66
85
|
return;
|
86
|
+
let currentPointer = event.current;
|
87
|
+
if (this.ctrlKeyPressed) {
|
88
|
+
currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
|
89
|
+
}
|
67
90
|
if (this.selectionBoxHandlingEvt) {
|
68
|
-
this.selectionBox.onDragUpdate(
|
91
|
+
this.selectionBox.onDragUpdate(currentPointer);
|
69
92
|
}
|
70
93
|
else {
|
71
|
-
this.selectionBox.setToPoint(
|
94
|
+
this.selectionBox.setToPoint(currentPointer.canvasPos);
|
72
95
|
}
|
73
96
|
}
|
74
97
|
onSelectionUpdated() {
|
@@ -113,7 +136,11 @@ export default class SelectionTool extends BaseTool {
|
|
113
136
|
onPointerUp(event) {
|
114
137
|
if (!this.selectionBox)
|
115
138
|
return;
|
116
|
-
|
139
|
+
let currentPointer = event.current;
|
140
|
+
if (this.ctrlKeyPressed) {
|
141
|
+
currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
|
142
|
+
}
|
143
|
+
this.selectionBox.setToPoint(currentPointer.canvasPos);
|
117
144
|
// Were we expanding the previous selection?
|
118
145
|
if (this.expandingSelectionBox && this.prevSelectionBox) {
|
119
146
|
// If so, finish expanding.
|
@@ -142,6 +169,10 @@ export default class SelectionTool extends BaseTool {
|
|
142
169
|
this.expandingSelectionBox = false;
|
143
170
|
}
|
144
171
|
onKeyPress(event) {
|
172
|
+
if (event.key === 'Control') {
|
173
|
+
this.ctrlKeyPressed = true;
|
174
|
+
return true;
|
175
|
+
}
|
145
176
|
if (this.selectionBox && event.ctrlKey && event.key === 'd') {
|
146
177
|
// Handle duplication on key up — we don't want to accidentally duplicate
|
147
178
|
// many times.
|
@@ -235,13 +266,19 @@ export default class SelectionTool extends BaseTool {
|
|
235
266
|
return handled;
|
236
267
|
}
|
237
268
|
onKeyUp(evt) {
|
269
|
+
if (evt.key === 'Control') {
|
270
|
+
this.ctrlKeyPressed = false;
|
271
|
+
return true;
|
272
|
+
}
|
238
273
|
if (evt.key === 'Shift') {
|
239
274
|
this.shiftKeyPressed = false;
|
240
275
|
return true;
|
241
276
|
}
|
242
277
|
else if (evt.ctrlKey) {
|
243
278
|
if (this.selectionBox && evt.key === 'd') {
|
244
|
-
this.
|
279
|
+
this.selectionBox.duplicateSelectedObjects().then(command => {
|
280
|
+
this.editor.dispatch(command);
|
281
|
+
});
|
245
282
|
return true;
|
246
283
|
}
|
247
284
|
else if (evt.key === 'a') {
|
@@ -289,6 +326,8 @@ export default class SelectionTool extends BaseTool {
|
|
289
326
|
// Clear the selection
|
290
327
|
this.handleOverlay.replaceChildren();
|
291
328
|
this.selectionBox = null;
|
329
|
+
this.shiftKeyPressed = false;
|
330
|
+
this.ctrlKeyPressed = false;
|
292
331
|
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
293
332
|
if (enabled) {
|
294
333
|
this.handleOverlay.tabIndex = 0;
|
@@ -353,4 +392,5 @@ SelectionTool.handleableKeys = [
|
|
353
392
|
'e', 'j', 'ArrowDown',
|
354
393
|
'r', 'R',
|
355
394
|
'i', 'I', 'o', 'O',
|
395
|
+
'Control',
|
356
396
|
];
|
@@ -69,12 +69,13 @@ export default class TextTool extends BaseTool {
|
|
69
69
|
flushInput(removeInput = true) {
|
70
70
|
if (this.textInputElem && this.textTargetPosition) {
|
71
71
|
const content = this.textInputElem.value.trimEnd();
|
72
|
+
this.textInputElem.value = '';
|
72
73
|
if (removeInput) {
|
73
|
-
|
74
|
+
// In some browsers, .remove() triggers a .blur event (synchronously).
|
75
|
+
// Clear this.textInputElem before removal
|
76
|
+
const input = this.textInputElem;
|
74
77
|
this.textInputElem = null;
|
75
|
-
|
76
|
-
else {
|
77
|
-
this.textInputElem.value = '';
|
78
|
+
input.remove();
|
78
79
|
}
|
79
80
|
if (content === '') {
|
80
81
|
return;
|
@@ -145,9 +146,12 @@ export default class TextTool extends BaseTool {
|
|
145
146
|
// Delay removing the input -- flushInput may be called within a blur()
|
146
147
|
// event handler
|
147
148
|
const removeInput = false;
|
148
|
-
this.flushInput(removeInput);
|
149
149
|
const input = this.textInputElem;
|
150
|
-
|
150
|
+
this.flushInput(removeInput);
|
151
|
+
this.textInputElem = null;
|
152
|
+
setTimeout(() => {
|
153
|
+
input === null || input === void 0 ? void 0 : input.remove();
|
154
|
+
}, 0);
|
151
155
|
};
|
152
156
|
this.textInputElem.onkeyup = (evt) => {
|
153
157
|
var _a, _b;
|
@@ -1,6 +1,14 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import { KeyPressEvent } from '../types';
|
3
3
|
import BaseTool from './BaseTool';
|
4
|
+
/**
|
5
|
+
* Handles keyboard events used, by default, to select tools. By default,
|
6
|
+
* 1 maps to the first primary tool, 2 to the second primary tool, ... .
|
7
|
+
*
|
8
|
+
* This is in the default set of {@link ToolController} tools.
|
9
|
+
*
|
10
|
+
* @deprecated This may be replaced in the future.
|
11
|
+
*/
|
4
12
|
export default class ToolSwitcherShortcut extends BaseTool {
|
5
13
|
private editor;
|
6
14
|
constructor(editor: Editor);
|
@@ -1,12 +1,18 @@
|
|
1
|
-
// Handles ctrl+1, ctrl+2, ctrl+3, ..., shortcuts for switching tools.
|
2
|
-
// @packageDocumentation
|
3
1
|
import BaseTool from './BaseTool';
|
4
|
-
|
2
|
+
/**
|
3
|
+
* Handles keyboard events used, by default, to select tools. By default,
|
4
|
+
* 1 maps to the first primary tool, 2 to the second primary tool, ... .
|
5
|
+
*
|
6
|
+
* This is in the default set of {@link ToolController} tools.
|
7
|
+
*
|
8
|
+
* @deprecated This may be replaced in the future.
|
9
|
+
*/
|
5
10
|
export default class ToolSwitcherShortcut extends BaseTool {
|
6
11
|
constructor(editor) {
|
7
12
|
super(editor.notifier, editor.localization.changeTool);
|
8
13
|
this.editor = editor;
|
9
14
|
}
|
15
|
+
// @internal
|
10
16
|
onKeyPress({ key }) {
|
11
17
|
const toolController = this.editor.toolController;
|
12
18
|
const primaryTools = toolController.getPrimaryTools();
|
@@ -1,13 +1,11 @@
|
|
1
|
-
// Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
|
2
|
-
// @packageDocumentation
|
3
1
|
import BaseTool from './BaseTool';
|
4
|
-
//
|
2
|
+
// Handles ctrl+Z, ctrl+Shift+Z keyboard shortcuts.
|
5
3
|
export default class UndoRedoShortcut extends BaseTool {
|
6
4
|
constructor(editor) {
|
7
5
|
super(editor.notifier, editor.localization.undoRedoTool);
|
8
6
|
this.editor = editor;
|
9
7
|
}
|
10
|
-
//
|
8
|
+
// @internal
|
11
9
|
onKeyPress({ key, ctrlKey }) {
|
12
10
|
if (ctrlKey) {
|
13
11
|
if (key === 'z') {
|
package/dist/src/types.d.ts
CHANGED
@@ -129,8 +129,8 @@ export interface ToolbarDropdownShownEvent {
|
|
129
129
|
readonly parentWidget: BaseWidget;
|
130
130
|
}
|
131
131
|
export type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
|
132
|
-
export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
133
|
-
export type ComponentAddedListener = (component: AbstractComponent) => void;
|
132
|
+
export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null | void;
|
133
|
+
export type ComponentAddedListener = (component: AbstractComponent) => Promise<void> | void;
|
134
134
|
export type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
135
135
|
export interface ImageLoader {
|
136
136
|
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.13.0",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "./dist/src/lib.d.ts",
|
6
6
|
"types": "./dist/src/lib.js",
|
@@ -101,7 +101,7 @@
|
|
101
101
|
"ts-jest": "^29.0.3",
|
102
102
|
"ts-loader": "^9.4.2",
|
103
103
|
"ts-node": "^10.9.1",
|
104
|
-
"typedoc": "^0.23.
|
104
|
+
"typedoc": "^0.23.24",
|
105
105
|
"typescript": "^4.9.4",
|
106
106
|
"webpack": "^5.75.0"
|
107
107
|
},
|
package/src/Color4.test.ts
CHANGED
@@ -16,4 +16,15 @@ describe('Color4', () => {
|
|
16
16
|
expect(Color4.fromString('rgba ( 255, 0,\t 0, 0.5)')).objEq(Color4.ofRGBA(1, 0, 0, 0.5));
|
17
17
|
expect(Color4.fromString('rgba( 0, 0, 128, 0)')).objEq(Color4.ofRGBA(0, 0, 128/255, 0));
|
18
18
|
});
|
19
|
+
|
20
|
+
it('should mix blue and red to get dark purple', () => {
|
21
|
+
expect(Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 0, 1), 0.5)).objEq(Color4.ofRGB(0.5, 0, 0.5));
|
22
|
+
expect(Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 0, 1), 0.1)).objEq(Color4.ofRGB(0.9, 0, 0.1));
|
23
|
+
});
|
24
|
+
|
25
|
+
it('should mix red and green to get yellow', () => {
|
26
|
+
expect(Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.3)).objEq(
|
27
|
+
Color4.ofRGB(0.7, 0.3, 0)
|
28
|
+
);
|
29
|
+
});
|
19
30
|
});
|
package/src/Color4.ts
CHANGED
@@ -129,6 +129,28 @@ export default class Color4 {
|
|
129
129
|
return this.toHexString() === other.toHexString();
|
130
130
|
}
|
131
131
|
|
132
|
+
/**
|
133
|
+
* If `fractionTo` is not in the range [0, 1], it will be clamped to the nearest number
|
134
|
+
* in that range. For example, `a.mix(b, -1)` is equivalent to `a.mix(b, 0)`.
|
135
|
+
*
|
136
|
+
* @returns a color `fractionTo` of the way from this color to `other`.
|
137
|
+
*
|
138
|
+
* @example
|
139
|
+
* ```ts
|
140
|
+
* Color4.ofRGB(1, 0, 0).mix(Color4.ofRGB(0, 1, 0), 0.1) // -> Color4(0.9, 0.1, 0)
|
141
|
+
* ```
|
142
|
+
*/
|
143
|
+
public mix(other: Color4, fractionTo: number): Color4 {
|
144
|
+
fractionTo = Math.min(Math.max(fractionTo, 0), 1);
|
145
|
+
const fractionOfThis = 1 - fractionTo;
|
146
|
+
return new Color4(
|
147
|
+
this.r * fractionOfThis + other.r * fractionTo,
|
148
|
+
this.g * fractionOfThis + other.g * fractionTo,
|
149
|
+
this.b * fractionOfThis + other.b * fractionTo,
|
150
|
+
this.a * fractionOfThis + other.a * fractionTo,
|
151
|
+
);
|
152
|
+
}
|
153
|
+
|
132
154
|
private hexString: string|null = null;
|
133
155
|
|
134
156
|
/**
|
@@ -176,5 +198,6 @@ export default class Color4 {
|
|
176
198
|
public static yellow = Color4.ofRGB(1, 1, 0.1);
|
177
199
|
public static clay = Color4.ofRGB(0.8, 0.4, 0.2);
|
178
200
|
public static black = Color4.ofRGB(0, 0, 0);
|
201
|
+
public static gray = Color4.ofRGB(0.5, 0.5, 0.5);
|
179
202
|
public static white = Color4.ofRGB(1, 1, 1);
|
180
203
|
}
|
package/src/Editor.ts
CHANGED
@@ -1,22 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* The main entrypoint for the full editor.
|
3
|
-
*
|
4
|
-
* @example
|
5
|
-
* To create an editor with a toolbar,
|
6
|
-
* ```
|
7
|
-
* const editor = new Editor(document.body);
|
8
|
-
*
|
9
|
-
* const toolbar = editor.addToolbar();
|
10
|
-
* toolbar.addActionButton('Save', () => {
|
11
|
-
* const saveData = editor.toSVG().outerHTML;
|
12
|
-
* // Do something with saveData...
|
13
|
-
* });
|
14
|
-
* ```
|
15
|
-
*
|
16
|
-
* @packageDocumentation
|
17
|
-
*/
|
18
|
-
|
19
|
-
|
20
1
|
import EditorImage from './EditorImage';
|
21
2
|
import ToolController from './tools/ToolController';
|
22
3
|
import { InputEvtType, PointerEvt, EditorNotifier, EditorEventType, ImageLoader } from './types';
|
@@ -70,12 +51,27 @@ export interface EditorSettings {
|
|
70
51
|
iconProvider: IconProvider,
|
71
52
|
}
|
72
53
|
|
73
|
-
|
54
|
+
/**
|
55
|
+
* The main entrypoint for the full editor.
|
56
|
+
*
|
57
|
+
* @example
|
58
|
+
* To create an editor with a toolbar,
|
59
|
+
* ```
|
60
|
+
* const editor = new Editor(document.body);
|
61
|
+
*
|
62
|
+
* const toolbar = editor.addToolbar();
|
63
|
+
* toolbar.addActionButton('Save', () => {
|
64
|
+
* const saveData = editor.toSVG().outerHTML;
|
65
|
+
* // Do something with saveData...
|
66
|
+
* });
|
67
|
+
* ```
|
68
|
+
*/
|
74
69
|
export class Editor {
|
75
70
|
// Wrapper around the viewport and toolbar
|
76
71
|
private container: HTMLElement;
|
77
72
|
private renderingRegion: HTMLElement;
|
78
73
|
|
74
|
+
/** Manages drawing surfaces/{@link lib!AbstractRenderer}s. */
|
79
75
|
public display: Display;
|
80
76
|
|
81
77
|
/**
|
@@ -116,11 +112,23 @@ export class Editor {
|
|
116
112
|
/** Viewport for the exported/imported image. */
|
117
113
|
private importExportViewport: Viewport;
|
118
114
|
|
115
|
+
/**
|
116
|
+
* Allows transforming the view and querying information about
|
117
|
+
* what is currently visible.
|
118
|
+
*/
|
119
|
+
public readonly viewport: Viewport;
|
120
|
+
|
119
121
|
/** @internal */
|
120
122
|
public readonly localization: EditorLocalization;
|
121
123
|
|
124
|
+
/** {@link lib!EditorSettings.iconProvider} */
|
122
125
|
public readonly icons: IconProvider;
|
123
|
-
|
126
|
+
|
127
|
+
/**
|
128
|
+
* Controls the list of tools. See
|
129
|
+
* [the custom tool example](https://github.com/personalizedrefrigerator/js-draw/tree/main/docs/example-custom-tools)
|
130
|
+
* for more.
|
131
|
+
*/
|
124
132
|
public readonly toolController: ToolController;
|
125
133
|
|
126
134
|
/**
|
@@ -292,8 +300,7 @@ export class Editor {
|
|
292
300
|
const toolbar = new HTMLToolbar(this, this.container, this.localization);
|
293
301
|
|
294
302
|
if (defaultLayout) {
|
295
|
-
toolbar.
|
296
|
-
toolbar.addDefaultActionButtons();
|
303
|
+
toolbar.addDefaults();
|
297
304
|
}
|
298
305
|
|
299
306
|
return toolbar;
|
@@ -766,12 +773,18 @@ export class Editor {
|
|
766
773
|
this.nextRerenderListeners = [];
|
767
774
|
}
|
768
775
|
|
776
|
+
/**
|
777
|
+
* @see {@link Display.getWetInkRenderer} {@link Display.flatten}
|
778
|
+
*/
|
769
779
|
public drawWetInk(...path: RenderablePathSpec[]) {
|
770
780
|
for (const part of path) {
|
771
781
|
this.display.getWetInkRenderer().drawPath(part);
|
772
782
|
}
|
773
783
|
}
|
774
784
|
|
785
|
+
/**
|
786
|
+
* @see {@link Display.getWetInkRenderer}
|
787
|
+
*/
|
775
788
|
public clearWetInk() {
|
776
789
|
this.display.getWetInkRenderer().clear();
|
777
790
|
}
|
@@ -896,7 +909,7 @@ export class Editor {
|
|
896
909
|
public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
|
897
910
|
const canvas = document.createElement('canvas');
|
898
911
|
|
899
|
-
const resolution = this.importExportViewport.
|
912
|
+
const resolution = this.importExportViewport.getScreenRectSize();
|
900
913
|
|
901
914
|
canvas.width = resolution.x;
|
902
915
|
canvas.height = resolution.y;
|
@@ -946,8 +959,8 @@ export class Editor {
|
|
946
959
|
this.showLoadingWarning(0);
|
947
960
|
this.display.setDraftMode(true);
|
948
961
|
|
949
|
-
await loader.start((component) => {
|
950
|
-
this.dispatchNoAnnounce(EditorImage.addElement(component));
|
962
|
+
await loader.start(async (component) => {
|
963
|
+
await this.dispatchNoAnnounce(EditorImage.addElement(component));
|
951
964
|
}, (countProcessed: number, totalToProcess: number) => {
|
952
965
|
if (countProcessed % 500 === 0) {
|
953
966
|
this.showLoadingWarning(countProcessed / totalToProcess);
|
package/src/EditorImage.ts
CHANGED
@@ -75,6 +75,11 @@ export default class EditorImage {
|
|
75
75
|
delete this.componentsById[elem.getId()];
|
76
76
|
}
|
77
77
|
|
78
|
+
/**
|
79
|
+
* @returns the AbstractComponent with `id`, if it exists.
|
80
|
+
*
|
81
|
+
* @see {@link AbstractComponent.getId}
|
82
|
+
*/
|
78
83
|
public lookupElement(id: string): AbstractComponent|null {
|
79
84
|
return this.componentsById[id] ?? null;
|
80
85
|
}
|
@@ -84,6 +89,13 @@ export default class EditorImage {
|
|
84
89
|
return this.root.addLeaf(elem);
|
85
90
|
}
|
86
91
|
|
92
|
+
/**
|
93
|
+
* Returns a command that adds the given element to the `EditorImage`.
|
94
|
+
* If `applyByFlattening` is true, the content of the wet ink renderer is
|
95
|
+
* rendered onto the main rendering canvas instead of doing a full re-render.
|
96
|
+
*
|
97
|
+
* @see {@link Display.flatten}
|
98
|
+
*/
|
87
99
|
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): SerializableCommand {
|
88
100
|
return new EditorImage.AddElementCommand(elem, applyByFlattening);
|
89
101
|
}
|
package/src/Pointer.ts
CHANGED
@@ -36,6 +36,25 @@ export default class Pointer {
|
|
36
36
|
) {
|
37
37
|
}
|
38
38
|
|
39
|
+
// Snaps this pointer to the nearest grid point (rounds the coordinates of this
|
40
|
+
// pointer based on the current zoom). Returns a new Pointer and does not modify
|
41
|
+
// this.
|
42
|
+
public snappedToGrid(viewport: Viewport): Pointer {
|
43
|
+
const snappedCanvasPos = viewport.snapToGrid(this.canvasPos);
|
44
|
+
const snappedScreenPos = viewport.canvasToScreen(snappedCanvasPos);
|
45
|
+
|
46
|
+
return new Pointer(
|
47
|
+
snappedScreenPos,
|
48
|
+
snappedCanvasPos,
|
49
|
+
this.pressure,
|
50
|
+
this.isPrimary,
|
51
|
+
this.down,
|
52
|
+
this.device,
|
53
|
+
this.id,
|
54
|
+
this.timeStamp,
|
55
|
+
);
|
56
|
+
}
|
57
|
+
|
39
58
|
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
|
40
59
|
// considered the top left of `relativeTo`.
|
41
60
|
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer {
|
package/src/SVGLoader.ts
CHANGED
@@ -31,6 +31,7 @@ export type SVGLoaderUnknownStyleAttribute = { key: string, value: string, prior
|
|
31
31
|
|
32
32
|
const supportedStrokeFillStyleAttrs = [ 'stroke', 'fill', 'stroke-width' ];
|
33
33
|
|
34
|
+
// Handles loading images from SVG.
|
34
35
|
export default class SVGLoader implements ImageLoader {
|
35
36
|
private onAddComponent: ComponentAddedListener|null = null;
|
36
37
|
private onProgress: OnProgressListener|null = null;
|
@@ -156,7 +157,7 @@ export default class SVGLoader implements ImageLoader {
|
|
156
157
|
}
|
157
158
|
|
158
159
|
// Adds a stroke with a single path
|
159
|
-
private addPath(node: SVGPathElement) {
|
160
|
+
private async addPath(node: SVGPathElement) {
|
160
161
|
let elem: AbstractComponent;
|
161
162
|
try {
|
162
163
|
const strokeData = this.strokeDataFromElem(node);
|
@@ -181,7 +182,7 @@ export default class SVGLoader implements ImageLoader {
|
|
181
182
|
return;
|
182
183
|
}
|
183
184
|
}
|
184
|
-
this.onAddComponent?.(elem);
|
185
|
+
await this.onAddComponent?.(elem);
|
185
186
|
}
|
186
187
|
|
187
188
|
// If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
|
@@ -274,10 +275,10 @@ export default class SVGLoader implements ImageLoader {
|
|
274
275
|
return result;
|
275
276
|
}
|
276
277
|
|
277
|
-
private addText(elem: SVGTextElement|SVGTSpanElement) {
|
278
|
+
private async addText(elem: SVGTextElement|SVGTSpanElement) {
|
278
279
|
try {
|
279
280
|
const textElem = this.makeText(elem);
|
280
|
-
this.onAddComponent?.(textElem);
|
281
|
+
await this.onAddComponent?.(textElem);
|
281
282
|
} catch (e) {
|
282
283
|
console.error('Invalid text object in node', elem, '. Continuing.... Error:', e);
|
283
284
|
this.addUnknownNode(elem);
|
@@ -300,17 +301,17 @@ export default class SVGLoader implements ImageLoader {
|
|
300
301
|
new Set([ 'transform' ])
|
301
302
|
);
|
302
303
|
|
303
|
-
this.onAddComponent?.(imageElem);
|
304
|
+
await this.onAddComponent?.(imageElem);
|
304
305
|
} catch (e) {
|
305
306
|
console.error('Error loading image:', e, '. Element: ', elem, '. Continuing...');
|
306
|
-
this.addUnknownNode(elem);
|
307
|
+
await this.addUnknownNode(elem);
|
307
308
|
}
|
308
309
|
}
|
309
310
|
|
310
|
-
private addUnknownNode(node: SVGElement) {
|
311
|
+
private async addUnknownNode(node: SVGElement) {
|
311
312
|
if (this.storeUnknown) {
|
312
313
|
const component = new UnknownSVGObject(node);
|
313
|
-
this.onAddComponent?.(component);
|
314
|
+
await this.onAddComponent?.(component);
|
314
315
|
}
|
315
316
|
}
|
316
317
|
|
@@ -335,9 +336,9 @@ export default class SVGLoader implements ImageLoader {
|
|
335
336
|
this.onDetermineExportRect?.(this.rootViewBox);
|
336
337
|
}
|
337
338
|
|
338
|
-
private updateSVGAttrs(node: SVGSVGElement) {
|
339
|
+
private async updateSVGAttrs(node: SVGSVGElement) {
|
339
340
|
if (this.storeUnknown) {
|
340
|
-
this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
341
|
+
await this.onAddComponent?.(new SVGGlobalAttributesObject(this.getSourceAttrs(node)));
|
341
342
|
}
|
342
343
|
}
|
343
344
|
|
@@ -350,10 +351,10 @@ export default class SVGLoader implements ImageLoader {
|
|
350
351
|
// Continue -- visit the node's children.
|
351
352
|
break;
|
352
353
|
case 'path':
|
353
|
-
this.addPath(node as SVGPathElement);
|
354
|
+
await this.addPath(node as SVGPathElement);
|
354
355
|
break;
|
355
356
|
case 'text':
|
356
|
-
this.addText(node as SVGTextElement);
|
357
|
+
await this.addText(node as SVGTextElement);
|
357
358
|
visitChildren = false;
|
358
359
|
break;
|
359
360
|
case 'image':
|
@@ -367,7 +368,7 @@ export default class SVGLoader implements ImageLoader {
|
|
367
368
|
this.updateSVGAttrs(node as SVGSVGElement);
|
368
369
|
break;
|
369
370
|
case 'style':
|
370
|
-
this.addUnknownNode(node as SVGStyleElement);
|
371
|
+
await this.addUnknownNode(node as SVGStyleElement);
|
371
372
|
break;
|
372
373
|
default:
|
373
374
|
console.warn('Unknown SVG element,', node);
|
@@ -377,7 +378,7 @@ export default class SVGLoader implements ImageLoader {
|
|
377
378
|
);
|
378
379
|
}
|
379
380
|
|
380
|
-
this.addUnknownNode(node as SVGElement);
|
381
|
+
await this.addUnknownNode(node as SVGElement);
|
381
382
|
return;
|
382
383
|
}
|
383
384
|
|
@@ -422,7 +423,11 @@ export default class SVGLoader implements ImageLoader {
|
|
422
423
|
this.onFinish?.();
|
423
424
|
}
|
424
425
|
|
425
|
-
|
426
|
+
/**
|
427
|
+
* @see {@link Editor.loadFrom}
|
428
|
+
* @param text - Textual representation of the SVG (e.g. `<svg viewbox='...'>...</svg>`).
|
429
|
+
* @param sanitize - if `true`, don't store unknown attributes.
|
430
|
+
*/
|
426
431
|
public static fromString(text: string, sanitize: boolean = false): SVGLoader {
|
427
432
|
const sandbox = document.createElement('iframe');
|
428
433
|
sandbox.src = 'about:blank';
|