js-draw 1.9.0 → 1.10.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/dist/Editor.css +48 -1
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +41 -0
- package/dist/cjs/Editor.js +33 -13
- package/dist/cjs/Pointer.js +1 -1
- package/dist/cjs/Viewport.d.ts +6 -0
- package/dist/cjs/Viewport.js +6 -1
- package/dist/cjs/commands/Erase.d.ts +22 -2
- package/dist/cjs/commands/Erase.js +22 -2
- package/dist/cjs/commands/uniteCommands.d.ts +36 -0
- package/dist/cjs/commands/uniteCommands.js +36 -0
- package/dist/cjs/components/ImageComponent.d.ts +12 -0
- package/dist/cjs/components/ImageComponent.js +16 -9
- package/dist/cjs/components/Stroke.d.ts +16 -2
- package/dist/cjs/components/Stroke.js +17 -1
- package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
- package/dist/cjs/components/builders/CircleBuilder.js +3 -3
- package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
- package/dist/cjs/components/builders/LineBuilder.js +3 -3
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
- package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
- package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
- package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
- package/dist/cjs/components/builders/types.d.ts +1 -0
- package/dist/cjs/image/EditorImage.d.ts +32 -1
- package/dist/cjs/image/EditorImage.js +32 -1
- package/dist/cjs/rendering/Display.js +8 -1
- package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
- package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
- package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
- package/dist/cjs/toolbar/IconProvider.js +17 -0
- package/dist/cjs/toolbar/localization.d.ts +3 -0
- package/dist/cjs/toolbar/localization.js +4 -1
- package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
- package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
- package/dist/cjs/tools/Pen.d.ts +9 -0
- package/dist/cjs/tools/Pen.js +77 -3
- package/dist/cjs/tools/TextTool.js +5 -1
- package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
- package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
- package/dist/cjs/util/ReactiveValue.d.ts +2 -0
- package/dist/cjs/util/ReactiveValue.js +2 -0
- package/dist/cjs/util/lib.d.ts +1 -0
- package/dist/cjs/util/lib.js +4 -1
- package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
- package/dist/cjs/util/waitForImageLoaded.js +12 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +41 -0
- package/dist/mjs/Editor.mjs +33 -13
- package/dist/mjs/Pointer.mjs +1 -1
- package/dist/mjs/Viewport.d.ts +6 -0
- package/dist/mjs/Viewport.mjs +6 -1
- package/dist/mjs/commands/Erase.d.ts +22 -2
- package/dist/mjs/commands/Erase.mjs +22 -2
- package/dist/mjs/commands/uniteCommands.d.ts +36 -0
- package/dist/mjs/commands/uniteCommands.mjs +36 -0
- package/dist/mjs/components/ImageComponent.d.ts +12 -0
- package/dist/mjs/components/ImageComponent.mjs +16 -9
- package/dist/mjs/components/Stroke.d.ts +16 -2
- package/dist/mjs/components/Stroke.mjs +17 -1
- package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
- package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
- package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
- package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
- package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
- package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
- package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
- package/dist/mjs/components/builders/types.d.ts +1 -0
- package/dist/mjs/image/EditorImage.d.ts +32 -1
- package/dist/mjs/image/EditorImage.mjs +32 -1
- package/dist/mjs/rendering/Display.mjs +8 -1
- package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
- package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
- package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
- package/dist/mjs/toolbar/IconProvider.mjs +17 -0
- package/dist/mjs/toolbar/localization.d.ts +3 -0
- package/dist/mjs/toolbar/localization.mjs +4 -1
- package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
- package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
- package/dist/mjs/tools/Pen.d.ts +9 -0
- package/dist/mjs/tools/Pen.mjs +77 -3
- package/dist/mjs/tools/TextTool.mjs +5 -1
- package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
- package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
- package/dist/mjs/util/ReactiveValue.d.ts +2 -0
- package/dist/mjs/util/ReactiveValue.mjs +2 -0
- package/dist/mjs/util/lib.d.ts +1 -0
- package/dist/mjs/util/lib.mjs +1 -0
- package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
- package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
- package/dist/mjs/version.mjs +1 -1
- package/package.json +3 -3
- package/src/Editor.scss +7 -0
- package/src/toolbar/AbstractToolbar.scss +20 -0
- package/src/toolbar/toolbar.scss +1 -1
- package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
- package/src/toolbar/widgets/PenToolWidget.scss +33 -0
- package/src/tools/SelectionTool/SelectionTool.scss +6 -0
- package/src/toolbar/widgets/PenToolWidget.css +0 -2
@@ -3,10 +3,10 @@ import { ToolbarLocalization } from '../localization';
|
|
3
3
|
import BaseWidget from './BaseWidget';
|
4
4
|
export default class InsertImageWidget extends BaseWidget {
|
5
5
|
private imagePreview;
|
6
|
+
private image;
|
6
7
|
private selectedFiles;
|
7
8
|
private imageAltTextInput;
|
8
9
|
private statusView;
|
9
|
-
private imageBase64URL;
|
10
10
|
private submitButton;
|
11
11
|
constructor(editor: Editor, localization?: ToolbarLocalization);
|
12
12
|
protected getTitle(): string;
|
@@ -15,6 +15,7 @@ export default class InsertImageWidget extends BaseWidget {
|
|
15
15
|
protected handleClick(): void;
|
16
16
|
private static nextInputId;
|
17
17
|
protected fillDropdown(dropdown: HTMLElement): boolean;
|
18
|
+
private onImageDataUpdate;
|
18
19
|
private hideDialog;
|
19
20
|
private updateImageSizeDisplay;
|
20
21
|
private updateInputs;
|
@@ -14,10 +14,48 @@ const BaseWidget_1 = __importDefault(require("./BaseWidget"));
|
|
14
14
|
const types_1 = require("../../types");
|
15
15
|
const constants_1 = require("../constants");
|
16
16
|
const makeFileInput_1 = __importDefault(require("./components/makeFileInput"));
|
17
|
+
class ImageWrapper {
|
18
|
+
constructor(imageBase64Url, preview, onUrlUpdate) {
|
19
|
+
this.imageBase64Url = imageBase64Url;
|
20
|
+
this.preview = preview;
|
21
|
+
this.onUrlUpdate = onUrlUpdate;
|
22
|
+
this.originalSrc = imageBase64Url;
|
23
|
+
preview.src = imageBase64Url;
|
24
|
+
}
|
25
|
+
updateImageData(base64DataUrl) {
|
26
|
+
this.preview.src = base64DataUrl;
|
27
|
+
this.imageBase64Url = base64DataUrl;
|
28
|
+
this.onUrlUpdate();
|
29
|
+
}
|
30
|
+
decreaseSize(resizeFactor = 3 / 4) {
|
31
|
+
const canvas = document.createElement('canvas');
|
32
|
+
canvas.width = this.preview.naturalWidth * resizeFactor;
|
33
|
+
canvas.height = this.preview.naturalHeight * resizeFactor;
|
34
|
+
const ctx = canvas.getContext('2d');
|
35
|
+
ctx?.drawImage(this.preview, 0, 0, canvas.width, canvas.height);
|
36
|
+
// JPEG can be much smaller than PNG for the same image size. Prefer it if
|
37
|
+
// the image is already a JPEG.
|
38
|
+
const format = this.originalSrc?.startsWith('data:image/jpeg;') ? 'image/jpeg' : 'image/png';
|
39
|
+
this.updateImageData(canvas.toDataURL(format));
|
40
|
+
}
|
41
|
+
reset() {
|
42
|
+
this.updateImageData(this.originalSrc);
|
43
|
+
}
|
44
|
+
isChanged() {
|
45
|
+
return this.imageBase64Url !== this.originalSrc;
|
46
|
+
}
|
47
|
+
getBase64Url() {
|
48
|
+
return this.imageBase64Url;
|
49
|
+
}
|
50
|
+
static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
|
51
|
+
return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
|
52
|
+
}
|
53
|
+
}
|
17
54
|
class InsertImageWidget extends BaseWidget_1.default {
|
18
55
|
constructor(editor, localization) {
|
19
56
|
localization ??= editor.localization;
|
20
57
|
super(editor, 'insert-image-widget', localization);
|
58
|
+
this.image = null;
|
21
59
|
// Make the dropdown showable
|
22
60
|
this.container.classList.add('dropdownShowable');
|
23
61
|
editor.notifier.on(types_1.EditorEventType.SelectionUpdated, event => {
|
@@ -51,6 +89,7 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
51
89
|
this.statusView = document.createElement('div');
|
52
90
|
const actionButtonRow = document.createElement('div');
|
53
91
|
actionButtonRow.classList.add('action-button-row');
|
92
|
+
this.statusView.classList.add('insert-image-image-status-view');
|
54
93
|
this.submitButton = document.createElement('button');
|
55
94
|
this.selectedFiles = selectedFiles;
|
56
95
|
this.imageAltTextInput = document.createElement('input');
|
@@ -65,9 +104,8 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
65
104
|
this.submitButton.innerText = this.localizationTable.submit;
|
66
105
|
this.selectedFiles.onUpdateAndNow(async (files) => {
|
67
106
|
if (files.length === 0) {
|
68
|
-
this.
|
69
|
-
this.
|
70
|
-
this.submitButton.style.display = 'none';
|
107
|
+
this.image = null;
|
108
|
+
this.onImageDataUpdate();
|
71
109
|
return;
|
72
110
|
}
|
73
111
|
this.imagePreview.style.display = 'block';
|
@@ -79,18 +117,13 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
79
117
|
catch (e) {
|
80
118
|
this.statusView.innerText = this.localizationTable.imageLoadError(e);
|
81
119
|
}
|
82
|
-
this.imageBase64URL = data;
|
83
120
|
if (data) {
|
84
|
-
this.
|
85
|
-
this.submitButton.disabled = false;
|
86
|
-
this.submitButton.style.display = '';
|
87
|
-
this.updateImageSizeDisplay();
|
121
|
+
this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
|
88
122
|
}
|
89
123
|
else {
|
90
|
-
this.
|
91
|
-
this.submitButton.style.display = 'none';
|
92
|
-
this.statusView.innerText = '';
|
124
|
+
this.image = null;
|
93
125
|
}
|
126
|
+
this.onImageDataUpdate();
|
94
127
|
});
|
95
128
|
altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
|
96
129
|
actionButtonRow.replaceChildren(this.submitButton);
|
@@ -98,11 +131,27 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
98
131
|
dropdown.replaceChildren(container);
|
99
132
|
return true;
|
100
133
|
}
|
134
|
+
onImageDataUpdate() {
|
135
|
+
const base64Data = this.image?.getBase64Url();
|
136
|
+
if (base64Data) {
|
137
|
+
this.submitButton.disabled = false;
|
138
|
+
this.submitButton.style.display = '';
|
139
|
+
this.imagePreview.style.display = '';
|
140
|
+
this.updateImageSizeDisplay();
|
141
|
+
}
|
142
|
+
else {
|
143
|
+
this.submitButton.disabled = true;
|
144
|
+
this.submitButton.style.display = 'none';
|
145
|
+
this.statusView.innerText = '';
|
146
|
+
this.imagePreview.style.display = 'none';
|
147
|
+
this.submitButton.disabled = true;
|
148
|
+
}
|
149
|
+
}
|
101
150
|
hideDialog() {
|
102
151
|
this.setDropdownVisible(false);
|
103
152
|
}
|
104
153
|
updateImageSizeDisplay() {
|
105
|
-
const imageData = this.
|
154
|
+
const imageData = this.image?.getBase64Url() ?? '';
|
106
155
|
const sizeInKiB = imageData.length / 1024;
|
107
156
|
const sizeInMiB = sizeInKiB / 1024;
|
108
157
|
let units = 'KiB';
|
@@ -111,7 +160,27 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
111
160
|
size = sizeInMiB;
|
112
161
|
units = 'MiB';
|
113
162
|
}
|
114
|
-
|
163
|
+
const sizeText = document.createElement('span');
|
164
|
+
sizeText.innerText = this.localizationTable.imageSize(Math.round(size), units);
|
165
|
+
// Add a button to allow decreasing the size of large images.
|
166
|
+
const decreaseSizeButton = document.createElement('button');
|
167
|
+
decreaseSizeButton.innerText = this.localizationTable.decreaseImageSize;
|
168
|
+
decreaseSizeButton.onclick = () => {
|
169
|
+
this.image?.decreaseSize();
|
170
|
+
};
|
171
|
+
const resetSizeButton = document.createElement('button');
|
172
|
+
resetSizeButton.innerText = this.localizationTable.resetImage;
|
173
|
+
resetSizeButton.onclick = () => {
|
174
|
+
this.image?.reset();
|
175
|
+
};
|
176
|
+
this.statusView.replaceChildren(sizeText);
|
177
|
+
const largeImageThreshold = 0.25; // MiB
|
178
|
+
if (sizeInMiB > largeImageThreshold) {
|
179
|
+
this.statusView.appendChild(decreaseSizeButton);
|
180
|
+
}
|
181
|
+
else if (this.image?.isChanged()) {
|
182
|
+
this.statusView.appendChild(resetSizeButton);
|
183
|
+
}
|
115
184
|
}
|
116
185
|
updateInputs() {
|
117
186
|
const resetInputs = () => {
|
@@ -131,11 +200,8 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
131
200
|
if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent_1.default) {
|
132
201
|
editingImage = selectedObjects[0];
|
133
202
|
this.imageAltTextInput.value = editingImage.getAltText() ?? '';
|
134
|
-
this.
|
135
|
-
this.
|
136
|
-
this.imageBase64URL = editingImage.getURL();
|
137
|
-
this.imagePreview.src = this.imageBase64URL;
|
138
|
-
this.updateImageSizeDisplay();
|
203
|
+
this.image = ImageWrapper.fromSrcAndPreview(editingImage.getURL(), this.imagePreview, () => this.onImageDataUpdate());
|
204
|
+
this.onImageDataUpdate();
|
139
205
|
}
|
140
206
|
else if (selectedObjects.length > 0) {
|
141
207
|
// If not, clear the selection.
|
@@ -149,13 +215,21 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
149
215
|
}
|
150
216
|
};
|
151
217
|
this.submitButton.onclick = async () => {
|
152
|
-
if (!this.
|
218
|
+
if (!this.image) {
|
153
219
|
return;
|
154
220
|
}
|
155
221
|
const image = new Image();
|
156
|
-
image.src = this.
|
222
|
+
image.src = this.image.getBase64Url();
|
157
223
|
image.setAttribute('alt', this.imageAltTextInput.value);
|
158
|
-
|
224
|
+
let component;
|
225
|
+
try {
|
226
|
+
component = await ImageComponent_1.default.fromImage(image, math_1.Mat33.identity);
|
227
|
+
}
|
228
|
+
catch (error) {
|
229
|
+
console.error('Error loading image', error);
|
230
|
+
this.statusView.innerText = this.localizationTable.imageLoadError(error);
|
231
|
+
return;
|
232
|
+
}
|
159
233
|
if (component.getBBox().area === 0) {
|
160
234
|
this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
|
161
235
|
return;
|
@@ -163,9 +237,15 @@ class InsertImageWidget extends BaseWidget_1.default {
|
|
163
237
|
this.hideDialog();
|
164
238
|
if (editingImage) {
|
165
239
|
const eraseCommand = new Erase_1.default([editingImage]);
|
240
|
+
// Try to preserve the original width
|
241
|
+
const originalTransform = editingImage.getTransformation();
|
242
|
+
// || 1: Prevent division by zero
|
243
|
+
const originalWidth = editingImage.getBBox().width || 1;
|
244
|
+
const newWidth = component.getBBox().transformedBoundingBox(originalTransform).width || 1;
|
245
|
+
const widthAdjustTransform = math_1.Mat33.scaling2D(originalWidth / newWidth);
|
166
246
|
await this.editor.dispatch((0, uniteCommands_1.default)([
|
167
247
|
EditorImage_1.default.addElement(component),
|
168
|
-
component.transformBy(
|
248
|
+
component.transformBy(originalTransform.rightMul(widthAdjustTransform)),
|
169
249
|
component.setZIndex(editingImage.getZIndex()),
|
170
250
|
eraseCommand,
|
171
251
|
]));
|
@@ -24,8 +24,7 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
24
24
|
private createIconForRecord;
|
25
25
|
protected createIcon(): Element;
|
26
26
|
private createPenTypeSelector;
|
27
|
-
|
28
|
-
protected createStabilizationOption(): {
|
27
|
+
protected createStrokeCorrectionOptions(): {
|
29
28
|
update: () => void;
|
30
29
|
addTo: (parent: HTMLElement) => void;
|
31
30
|
};
|
@@ -152,27 +152,51 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
152
152
|
},
|
153
153
|
};
|
154
154
|
}
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
155
|
+
createStrokeCorrectionOptions() {
|
156
|
+
const container = document.createElement('div');
|
157
|
+
container.classList.add('action-button-row', `${constants_1.toolbarCSSPrefix}-pen-tool-toggle-buttons`);
|
158
|
+
const addToggleButton = (labelText, icon) => {
|
159
|
+
const button = document.createElement('button');
|
160
|
+
button.classList.add(`${constants_1.toolbarCSSPrefix}-toggle-button`);
|
161
|
+
const iconElement = icon.cloneNode(true);
|
162
|
+
iconElement.classList.add('icon');
|
163
|
+
const label = document.createElement('span');
|
164
|
+
label.innerText = labelText;
|
165
|
+
button.replaceChildren(iconElement, label);
|
166
|
+
button.setAttribute('role', 'switch');
|
167
|
+
container.appendChild(button);
|
168
|
+
let checked = false;
|
169
|
+
let onChangeListener = (_checked) => { };
|
170
|
+
const result = {
|
171
|
+
setChecked(newChecked) {
|
172
|
+
checked = newChecked;
|
173
|
+
button.setAttribute('aria-checked', `${checked}`);
|
174
|
+
onChangeListener(checked);
|
175
|
+
},
|
176
|
+
setOnInputListener(listener) {
|
177
|
+
onChangeListener = listener;
|
178
|
+
},
|
179
|
+
};
|
180
|
+
button.onclick = () => {
|
181
|
+
result.setChecked(!checked);
|
182
|
+
};
|
183
|
+
return result;
|
169
184
|
};
|
185
|
+
const stabilizationOption = addToggleButton(this.localizationTable.inputStabilization, this.editor.icons.makeStrokeSmoothingIcon());
|
186
|
+
stabilizationOption.setOnInputListener(enabled => {
|
187
|
+
this.tool.setHasStabilization(enabled);
|
188
|
+
});
|
189
|
+
const autocorrectOption = addToggleButton(this.localizationTable.strokeAutocorrect, this.editor.icons.makeShapeAutocorrectIcon());
|
190
|
+
autocorrectOption.setOnInputListener(enabled => {
|
191
|
+
this.tool.setStrokeAutocorrectEnabled(enabled);
|
192
|
+
});
|
170
193
|
return {
|
171
194
|
update: () => {
|
172
|
-
|
195
|
+
stabilizationOption.setChecked(!!this.tool.getInputMapper());
|
196
|
+
autocorrectOption.setChecked(this.tool.getStrokeAutocorrectionEnabled());
|
173
197
|
},
|
174
198
|
addTo: (parent) => {
|
175
|
-
parent.appendChild(
|
199
|
+
parent.appendChild(container);
|
176
200
|
}
|
177
201
|
};
|
178
202
|
}
|
@@ -194,20 +218,22 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
194
218
|
colorLabel.setAttribute('for', colorInput.id);
|
195
219
|
colorRow.appendChild(colorLabel);
|
196
220
|
colorRow.appendChild(colorInputContainer);
|
197
|
-
const
|
221
|
+
const toggleButtonRow = this.createStrokeCorrectionOptions();
|
198
222
|
this.updateInputs = () => {
|
199
223
|
setColorInputValue(this.tool.getColor());
|
200
224
|
setThickness(this.tool.getThickness());
|
201
225
|
penTypeSelect.updateIcons();
|
202
226
|
// Update the selected stroke factory.
|
203
227
|
penTypeSelect.setValue(this.getCurrentPenTypeIdx());
|
204
|
-
|
228
|
+
toggleButtonRow.update();
|
205
229
|
};
|
206
230
|
this.updateInputs();
|
207
231
|
container.replaceChildren(colorRow, thicknessRow);
|
208
232
|
penTypeSelect.addTo(container);
|
209
|
-
stabilizationOption.addTo(container);
|
210
233
|
dropdown.replaceChildren(container);
|
234
|
+
// Add the toggle button row *outside* of the main content (use different
|
235
|
+
// spacing with respect to the sides of the container).
|
236
|
+
toggleButtonRow.addTo(dropdown);
|
211
237
|
return true;
|
212
238
|
}
|
213
239
|
onKeyPress(event) {
|
@@ -237,6 +263,7 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
237
263
|
thickness: this.tool.getThickness(),
|
238
264
|
strokeFactoryId: this.getCurrentPenType()?.id,
|
239
265
|
inputStabilization: !!this.tool.getInputMapper(),
|
266
|
+
strokeAutocorrect: this.tool.getStrokeAutocorrectionEnabled(),
|
240
267
|
};
|
241
268
|
}
|
242
269
|
deserializeFrom(state) {
|
@@ -267,7 +294,10 @@ class PenToolWidget extends BaseToolWidget_1.default {
|
|
267
294
|
}
|
268
295
|
}
|
269
296
|
if (state.inputStabilization !== undefined) {
|
270
|
-
this.
|
297
|
+
this.tool.setHasStabilization(!!state.inputStabilization);
|
298
|
+
}
|
299
|
+
if (state.strokeAutocorrect !== undefined) {
|
300
|
+
this.tool.setStrokeAutocorrectEnabled(!!state.strokeAutocorrect);
|
271
301
|
}
|
272
302
|
}
|
273
303
|
}
|
package/dist/cjs/tools/Pen.d.ts
CHANGED
@@ -19,6 +19,11 @@ export default class Pen extends BaseTool {
|
|
19
19
|
private currentDeviceType;
|
20
20
|
private styleValue;
|
21
21
|
private style;
|
22
|
+
private shapeAutocompletionEnabled;
|
23
|
+
private autocorrectedShape;
|
24
|
+
private lastAutocorrectedShape;
|
25
|
+
private removedAutocorrectedShapeTime;
|
26
|
+
private stationaryDetector;
|
22
27
|
constructor(editor: Editor, description: string, style: Partial<PenStyle>);
|
23
28
|
private getPressureMultiplier;
|
24
29
|
protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
|
@@ -30,12 +35,16 @@ export default class Pen extends BaseTool {
|
|
30
35
|
onPointerMove({ current }: PointerEvt): void;
|
31
36
|
onPointerUp({ current }: PointerEvt): boolean;
|
32
37
|
onGestureCancel(): void;
|
38
|
+
private removedAutocorrectedShapeRecently;
|
39
|
+
private autocorrectShape;
|
33
40
|
private finalizeStroke;
|
34
41
|
private noteUpdated;
|
35
42
|
setColor(color: Color4): void;
|
36
43
|
setThickness(thickness: number): void;
|
37
44
|
setStrokeFactory(factory: ComponentBuilderFactory): void;
|
38
45
|
setHasStabilization(hasStabilization: boolean): void;
|
46
|
+
setStrokeAutocorrectEnabled(enabled: boolean): void;
|
47
|
+
getStrokeAutocorrectionEnabled(): boolean;
|
39
48
|
getThickness(): number;
|
40
49
|
getColor(): Color4;
|
41
50
|
getStrokeFactory(): ComponentBuilderFactory;
|
package/dist/cjs/tools/Pen.js
CHANGED
@@ -13,6 +13,7 @@ const keybindings_1 = require("./keybindings");
|
|
13
13
|
const keybindings_2 = require("./keybindings");
|
14
14
|
const InputStabilizer_1 = __importDefault(require("./InputFilter/InputStabilizer"));
|
15
15
|
const ReactiveValue_1 = require("../util/ReactiveValue");
|
16
|
+
const StationaryPenDetector_1 = __importDefault(require("./util/StationaryPenDetector"));
|
16
17
|
class Pen extends BaseTool_1.default {
|
17
18
|
constructor(editor, description, style) {
|
18
19
|
super(editor.notifier, description);
|
@@ -21,6 +22,11 @@ class Pen extends BaseTool_1.default {
|
|
21
22
|
this.lastPoint = null;
|
22
23
|
this.startPoint = null;
|
23
24
|
this.currentDeviceType = null;
|
25
|
+
this.shapeAutocompletionEnabled = false;
|
26
|
+
this.autocorrectedShape = null;
|
27
|
+
this.lastAutocorrectedShape = null;
|
28
|
+
this.removedAutocorrectedShapeTime = 0;
|
29
|
+
this.stationaryDetector = null;
|
24
30
|
this.styleValue = ReactiveValue_1.ReactiveValue.fromInitialValue({
|
25
31
|
factory: FreehandLineBuilder_1.makeFreehandLineBuilder,
|
26
32
|
color: math_1.Color4.blue,
|
@@ -58,7 +64,14 @@ class Pen extends BaseTool_1.default {
|
|
58
64
|
// Displays the stroke that is currently being built with the display's `wetInkRenderer`.
|
59
65
|
previewStroke() {
|
60
66
|
this.editor.clearWetInk();
|
61
|
-
this.
|
67
|
+
const wetInkRenderer = this.editor.display.getWetInkRenderer();
|
68
|
+
if (this.autocorrectedShape) {
|
69
|
+
const visibleRect = this.editor.viewport.visibleRect;
|
70
|
+
this.autocorrectedShape.render(wetInkRenderer, visibleRect);
|
71
|
+
}
|
72
|
+
else {
|
73
|
+
this.builder?.preview(wetInkRenderer);
|
74
|
+
}
|
62
75
|
}
|
63
76
|
// Throws if no stroke builder exists.
|
64
77
|
addPointToStroke(point) {
|
@@ -87,6 +100,19 @@ class Pen extends BaseTool_1.default {
|
|
87
100
|
this.startPoint = this.toStrokePoint(current);
|
88
101
|
this.builder = this.style.factory(this.startPoint, this.editor.viewport);
|
89
102
|
this.currentDeviceType = current.device;
|
103
|
+
if (this.shapeAutocompletionEnabled) {
|
104
|
+
const stationaryDetectionConfig = {
|
105
|
+
maxSpeed: 5,
|
106
|
+
maxRadius: 10,
|
107
|
+
minTimeSeconds: 0.5, // s
|
108
|
+
};
|
109
|
+
this.stationaryDetector = new StationaryPenDetector_1.default(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
|
110
|
+
}
|
111
|
+
else {
|
112
|
+
this.stationaryDetector = null;
|
113
|
+
}
|
114
|
+
this.lastAutocorrectedShape = null;
|
115
|
+
this.removedAutocorrectedShapeTime = 0;
|
90
116
|
return true;
|
91
117
|
}
|
92
118
|
return false;
|
@@ -114,7 +140,14 @@ class Pen extends BaseTool_1.default {
|
|
114
140
|
return;
|
115
141
|
if (current.device !== this.currentDeviceType)
|
116
142
|
return;
|
117
|
-
this.
|
143
|
+
const isStationary = this.stationaryDetector?.onPointerMove(current);
|
144
|
+
if (!isStationary) {
|
145
|
+
this.addPointToStroke(this.toStrokePoint(current));
|
146
|
+
if (this.autocorrectedShape) {
|
147
|
+
this.removedAutocorrectedShapeTime = performance.now();
|
148
|
+
this.autocorrectedShape = null;
|
149
|
+
}
|
150
|
+
}
|
118
151
|
}
|
119
152
|
onPointerUp({ current }) {
|
120
153
|
if (!this.builder)
|
@@ -124,6 +157,7 @@ class Pen extends BaseTool_1.default {
|
|
124
157
|
// device type.
|
125
158
|
return true;
|
126
159
|
}
|
160
|
+
this.stationaryDetector?.onPointerUp(current);
|
127
161
|
// onPointerUp events can have zero pressure. Use the last pressure instead.
|
128
162
|
const currentPoint = this.toStrokePoint(current);
|
129
163
|
const strokePoint = {
|
@@ -139,10 +173,37 @@ class Pen extends BaseTool_1.default {
|
|
139
173
|
onGestureCancel() {
|
140
174
|
this.builder = null;
|
141
175
|
this.editor.clearWetInk();
|
176
|
+
this.stationaryDetector?.destroy();
|
177
|
+
this.stationaryDetector = null;
|
178
|
+
}
|
179
|
+
removedAutocorrectedShapeRecently() {
|
180
|
+
return this.removedAutocorrectedShapeTime > performance.now() - 320;
|
181
|
+
}
|
182
|
+
async autocorrectShape(_lastPointer) {
|
183
|
+
if (!this.builder || !this.builder.autocorrectShape)
|
184
|
+
return;
|
185
|
+
if (!this.shapeAutocompletionEnabled)
|
186
|
+
return;
|
187
|
+
// If already corrected, do nothing
|
188
|
+
if (this.autocorrectedShape)
|
189
|
+
return;
|
190
|
+
// Activate stroke fitting
|
191
|
+
const correctedShape = await this.builder.autocorrectShape();
|
192
|
+
if (!this.builder || !correctedShape) {
|
193
|
+
return;
|
194
|
+
}
|
195
|
+
this.autocorrectedShape = correctedShape;
|
196
|
+
this.lastAutocorrectedShape = correctedShape;
|
197
|
+
this.previewStroke();
|
142
198
|
}
|
143
199
|
finalizeStroke() {
|
144
200
|
if (this.builder) {
|
145
|
-
|
201
|
+
// If autocorrectedShape was cleared recently enough, it was
|
202
|
+
// probably by mistake. Reset it.
|
203
|
+
if (this.lastAutocorrectedShape && this.removedAutocorrectedShapeRecently()) {
|
204
|
+
this.autocorrectedShape = this.lastAutocorrectedShape;
|
205
|
+
}
|
206
|
+
const stroke = this.autocorrectedShape ?? this.builder.build();
|
146
207
|
this.previewStroke();
|
147
208
|
if (stroke.getBBox().area > 0) {
|
148
209
|
const canFlatten = true;
|
@@ -155,7 +216,11 @@ class Pen extends BaseTool_1.default {
|
|
155
216
|
}
|
156
217
|
this.builder = null;
|
157
218
|
this.lastPoint = null;
|
219
|
+
this.autocorrectedShape = null;
|
220
|
+
this.lastAutocorrectedShape = null;
|
158
221
|
this.editor.clearWetInk();
|
222
|
+
this.stationaryDetector?.destroy();
|
223
|
+
this.stationaryDetector = null;
|
159
224
|
}
|
160
225
|
noteUpdated() {
|
161
226
|
this.editor.notifier.dispatch(types_1.EditorEventType.ToolUpdated, {
|
@@ -201,6 +266,15 @@ class Pen extends BaseTool_1.default {
|
|
201
266
|
}
|
202
267
|
this.noteUpdated();
|
203
268
|
}
|
269
|
+
setStrokeAutocorrectEnabled(enabled) {
|
270
|
+
if (enabled !== this.shapeAutocompletionEnabled) {
|
271
|
+
this.shapeAutocompletionEnabled = enabled;
|
272
|
+
this.noteUpdated();
|
273
|
+
}
|
274
|
+
}
|
275
|
+
getStrokeAutocorrectionEnabled() {
|
276
|
+
return this.shapeAutocompletionEnabled;
|
277
|
+
}
|
204
278
|
getThickness() { return this.style.thickness; }
|
205
279
|
getColor() { return this.style.color; }
|
206
280
|
getStrokeFactory() { return this.style.factory; }
|
@@ -44,6 +44,10 @@ class TextTool extends BaseTool_1.default {
|
|
44
44
|
.${overlayCSSClass} {
|
45
45
|
height: 0;
|
46
46
|
overflow: visible;
|
47
|
+
|
48
|
+
/* Allows absolutely-positioned textareas to scroll with
|
49
|
+
the containing overlay. */
|
50
|
+
position: relative;
|
47
51
|
}
|
48
52
|
|
49
53
|
.${overlayCSSClass} textarea {
|
@@ -126,7 +130,7 @@ class TextTool extends BaseTool_1.default {
|
|
126
130
|
this.textInputElem.style.fontWeight = this.textStyle.fontWeight ?? '';
|
127
131
|
this.textInputElem.style.fontSize = `${this.textStyle.size}px`;
|
128
132
|
this.textInputElem.style.color = this.textStyle.renderingStyle.fill.toHexString();
|
129
|
-
this.textInputElem.style.position = '
|
133
|
+
this.textInputElem.style.position = 'absolute';
|
130
134
|
this.textInputElem.style.left = `${textScreenPos.x}px`;
|
131
135
|
this.textInputElem.style.top = `${textScreenPos.y}px`;
|
132
136
|
this.textInputElem.style.margin = '0';
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import Pointer from '../../Pointer';
|
2
|
+
interface Config {
|
3
|
+
maxSpeed: number;
|
4
|
+
minTimeSeconds: number;
|
5
|
+
maxRadius: number;
|
6
|
+
}
|
7
|
+
type OnStationaryCallback = (lastPointer: Pointer) => void;
|
8
|
+
export default class StationaryPenDetector {
|
9
|
+
private config;
|
10
|
+
private onStationary;
|
11
|
+
private stationaryStartPointer;
|
12
|
+
private lastPointer;
|
13
|
+
private averageVelocity;
|
14
|
+
private timeout;
|
15
|
+
constructor(startPointer: Pointer, config: Config, onStationary: OnStationaryCallback);
|
16
|
+
onPointerMove(currentPointer: Pointer): boolean | undefined;
|
17
|
+
onPointerUp(pointer: Pointer): void;
|
18
|
+
destroy(): void;
|
19
|
+
private cancelStationaryTimeout;
|
20
|
+
private setStationaryTimeout;
|
21
|
+
}
|
22
|
+
export {};
|
@@ -0,0 +1,95 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
const math_1 = require("@js-draw/math");
|
4
|
+
class StationaryPenDetector {
|
5
|
+
// Only handles one pen. As such, `startPointer` should be the same device/finger
|
6
|
+
// as `updatedPointer` in `onPointerMove`.
|
7
|
+
//
|
8
|
+
// A new `StationaryPenDetector` should be created for each gesture.
|
9
|
+
constructor(startPointer, config, onStationary) {
|
10
|
+
this.config = config;
|
11
|
+
this.onStationary = onStationary;
|
12
|
+
this.timeout = null;
|
13
|
+
this.stationaryStartPointer = startPointer;
|
14
|
+
this.lastPointer = startPointer;
|
15
|
+
this.averageVelocity = math_1.Vec2.zero;
|
16
|
+
}
|
17
|
+
// Returns true if stationary
|
18
|
+
onPointerMove(currentPointer) {
|
19
|
+
if (!this.stationaryStartPointer) {
|
20
|
+
// Destoroyed
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
if (currentPointer.id !== this.stationaryStartPointer.id) {
|
24
|
+
return false;
|
25
|
+
}
|
26
|
+
// dx: "Δx" Displacement from last.
|
27
|
+
const dxFromLast = currentPointer.screenPos.minus(this.lastPointer.screenPos);
|
28
|
+
const dxFromStationaryStart = currentPointer.screenPos.minus(this.stationaryStartPointer.screenPos);
|
29
|
+
// dt: Delta time:
|
30
|
+
// /1000: Convert to s.
|
31
|
+
let dtFromLast = (currentPointer.timeStamp - this.lastPointer.timeStamp) / 1000; // s
|
32
|
+
// Don't divide by zero
|
33
|
+
if (dtFromLast === 0) {
|
34
|
+
dtFromLast = 1;
|
35
|
+
}
|
36
|
+
const currentVelocity = dxFromLast.times(1 / dtFromLast); // px/s
|
37
|
+
// Slight smoothing of the velocity to prevent input jitter from affecting the
|
38
|
+
// velocity too significantly.
|
39
|
+
this.averageVelocity = this.averageVelocity.lerp(currentVelocity, 0.5); // px/s
|
40
|
+
const dtFromStart = currentPointer.timeStamp - this.stationaryStartPointer.timeStamp; // ms
|
41
|
+
// If not stationary
|
42
|
+
if (dxFromStationaryStart.length() > this.config.maxRadius
|
43
|
+
|| this.averageVelocity.length() > this.config.maxSpeed
|
44
|
+
|| dtFromStart < this.config.minTimeSeconds) {
|
45
|
+
this.stationaryStartPointer = currentPointer;
|
46
|
+
this.lastPointer = currentPointer;
|
47
|
+
this.setStationaryTimeout(this.config.minTimeSeconds * 1000);
|
48
|
+
return false;
|
49
|
+
}
|
50
|
+
const stationaryTimeoutMs = this.config.minTimeSeconds * 1000 - dtFromStart;
|
51
|
+
this.lastPointer = currentPointer;
|
52
|
+
return stationaryTimeoutMs <= 0;
|
53
|
+
}
|
54
|
+
onPointerUp(pointer) {
|
55
|
+
if (pointer.id !== this.stationaryStartPointer?.id) {
|
56
|
+
this.cancelStationaryTimeout();
|
57
|
+
}
|
58
|
+
}
|
59
|
+
destroy() {
|
60
|
+
this.cancelStationaryTimeout();
|
61
|
+
this.stationaryStartPointer = null;
|
62
|
+
}
|
63
|
+
cancelStationaryTimeout() {
|
64
|
+
if (this.timeout !== null) {
|
65
|
+
clearTimeout(this.timeout);
|
66
|
+
this.timeout = null;
|
67
|
+
}
|
68
|
+
}
|
69
|
+
setStationaryTimeout(timeoutMs) {
|
70
|
+
if (this.timeout !== null) {
|
71
|
+
return;
|
72
|
+
}
|
73
|
+
if (timeoutMs <= 0) {
|
74
|
+
this.onStationary(this.lastPointer);
|
75
|
+
}
|
76
|
+
else {
|
77
|
+
this.timeout = setTimeout(() => {
|
78
|
+
this.timeout = null;
|
79
|
+
if (!this.stationaryStartPointer) {
|
80
|
+
// Destroyed
|
81
|
+
return;
|
82
|
+
}
|
83
|
+
const timeSinceStationaryStart = performance.now() - this.stationaryStartPointer.timeStamp;
|
84
|
+
const timeRemaining = this.config.minTimeSeconds * 1000 - timeSinceStationaryStart;
|
85
|
+
if (timeRemaining <= 0) {
|
86
|
+
this.onStationary(this.lastPointer);
|
87
|
+
}
|
88
|
+
else {
|
89
|
+
this.setStationaryTimeout(timeRemaining);
|
90
|
+
}
|
91
|
+
}, timeoutMs);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
}
|
95
|
+
exports.default = StationaryPenDetector;
|
@@ -13,6 +13,8 @@ type UpdateCallback<T> = (value: T) => void;
|
|
13
13
|
*
|
14
14
|
* Static methods in the `ReactiveValue` and `MutableReactiveValue` classes are
|
15
15
|
* constructors (e.g. `fromImmutable`).
|
16
|
+
*
|
17
|
+
* Avoid extending this class from an external library, as that may not be stable.
|
16
18
|
*/
|
17
19
|
export declare abstract class ReactiveValue<T> {
|
18
20
|
/**
|