js-draw 1.9.1 → 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 +9 -0
- package/dist/cjs/Pointer.js +1 -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/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 +9 -0
- package/dist/mjs/Pointer.mjs +1 -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/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
@@ -0,0 +1,166 @@
|
|
1
|
+
import { Rect2, LineSegment2 } from '@js-draw/math';
|
2
|
+
const makeShapeFitAutocorrect = (sourceFactory) => {
|
3
|
+
return (startPoint, viewport) => {
|
4
|
+
return new ShapeFitBuilder(sourceFactory, startPoint, viewport);
|
5
|
+
};
|
6
|
+
};
|
7
|
+
export default makeShapeFitAutocorrect;
|
8
|
+
const makeLineTemplate = (startPoint, points, _bbox) => {
|
9
|
+
const templatePoints = [
|
10
|
+
startPoint,
|
11
|
+
points[points.length - 1],
|
12
|
+
];
|
13
|
+
return { points: templatePoints, };
|
14
|
+
};
|
15
|
+
const makeRectangleTemplate = (_startPoint, _points, bbox) => {
|
16
|
+
return { points: [...bbox.corners, bbox.corners[0]], };
|
17
|
+
};
|
18
|
+
class ShapeFitBuilder {
|
19
|
+
constructor(sourceFactory, startPoint, viewport) {
|
20
|
+
this.sourceFactory = sourceFactory;
|
21
|
+
this.startPoint = startPoint;
|
22
|
+
this.viewport = viewport;
|
23
|
+
this.builder = sourceFactory(startPoint, viewport);
|
24
|
+
this.points = [startPoint];
|
25
|
+
}
|
26
|
+
getBBox() {
|
27
|
+
return this.builder.getBBox();
|
28
|
+
}
|
29
|
+
build() {
|
30
|
+
return this.builder.build();
|
31
|
+
}
|
32
|
+
preview(renderer) {
|
33
|
+
this.builder.preview(renderer);
|
34
|
+
}
|
35
|
+
addPoint(point) {
|
36
|
+
this.points.push(point);
|
37
|
+
this.builder.addPoint(point);
|
38
|
+
}
|
39
|
+
async autocorrectShape() {
|
40
|
+
// Use screen points so that autocorrected shapes rotate with the screen.
|
41
|
+
const startPoint = this.viewport.canvasToScreen(this.startPoint.pos);
|
42
|
+
const points = this.points.map(point => this.viewport.canvasToScreen(point.pos));
|
43
|
+
const bbox = Rect2.bboxOf(points);
|
44
|
+
const snappedStartPoint = this.viewport.canvasToScreen(this.viewport.snapToGrid(this.startPoint.pos));
|
45
|
+
const snappedPoints = this.points.map(point => this.viewport.canvasToScreen(this.viewport.snapToGrid(point.pos)));
|
46
|
+
const snappedBBox = Rect2.bboxOf(snappedPoints);
|
47
|
+
// Only fit larger shapes
|
48
|
+
if (bbox.maxDimension < 32) {
|
49
|
+
return null;
|
50
|
+
}
|
51
|
+
const maxError = Math.min(30, bbox.maxDimension / 4);
|
52
|
+
// Create templates
|
53
|
+
const templates = [
|
54
|
+
{
|
55
|
+
...makeLineTemplate(snappedStartPoint, snappedPoints, snappedBBox),
|
56
|
+
toleranceMultiplier: 0.5,
|
57
|
+
},
|
58
|
+
makeLineTemplate(startPoint, points, bbox),
|
59
|
+
{
|
60
|
+
...makeRectangleTemplate(snappedStartPoint, snappedPoints, snappedBBox),
|
61
|
+
toleranceMultiplier: 0.6,
|
62
|
+
},
|
63
|
+
makeRectangleTemplate(startPoint, points, bbox),
|
64
|
+
];
|
65
|
+
// Find a good fit fit
|
66
|
+
const selectTemplate = (maximumAllowedError) => {
|
67
|
+
for (const template of templates) {
|
68
|
+
const templatePoints = template.points;
|
69
|
+
// Maximum square error to accept the template
|
70
|
+
const acceptMaximumSquareError = maximumAllowedError * maximumAllowedError * (template.toleranceMultiplier ?? 1);
|
71
|
+
// Gets the point at index, wrapping the the start of the template if
|
72
|
+
// outside the array of points.
|
73
|
+
const templateAt = (index) => {
|
74
|
+
while (index < 0) {
|
75
|
+
index += templatePoints.length;
|
76
|
+
}
|
77
|
+
index %= templatePoints.length;
|
78
|
+
return templatePoints[index];
|
79
|
+
};
|
80
|
+
let closestToFirst = null;
|
81
|
+
let closestToFirstSqrDist = Infinity;
|
82
|
+
let templateStartIndex = 0;
|
83
|
+
// Find the closest point to the startPoint
|
84
|
+
for (let i = 0; i < templatePoints.length; i++) {
|
85
|
+
const current = templatePoints[i];
|
86
|
+
const currentSqrDist = current.minus(startPoint).magnitudeSquared();
|
87
|
+
if (!closestToFirst || currentSqrDist < closestToFirstSqrDist) {
|
88
|
+
closestToFirstSqrDist = currentSqrDist;
|
89
|
+
closestToFirst = current;
|
90
|
+
templateStartIndex = i;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
// Walk through the points and find the maximum error
|
94
|
+
let maximumSqrError = 0;
|
95
|
+
let templateIndex = templateStartIndex;
|
96
|
+
for (const point of points) {
|
97
|
+
let minimumCurrentSqrError = Infinity;
|
98
|
+
let minimumErrorAtIndex = templateIndex;
|
99
|
+
const windowRadius = 6;
|
100
|
+
for (let i = -windowRadius; i <= windowRadius; i++) {
|
101
|
+
const index = templateIndex + i;
|
102
|
+
const prevTemplatePoint = templateAt(index - 1);
|
103
|
+
const currentTemplatePoint = templateAt(index);
|
104
|
+
const nextTemplatePoint = templateAt(index + 1);
|
105
|
+
const prevToCurrent = new LineSegment2(prevTemplatePoint, currentTemplatePoint);
|
106
|
+
const currentToNext = new LineSegment2(currentTemplatePoint, nextTemplatePoint);
|
107
|
+
const prevToCurrentDist = prevToCurrent.distance(point);
|
108
|
+
const nextToCurrentDist = currentToNext.distance(point);
|
109
|
+
const error = Math.min(prevToCurrentDist, nextToCurrentDist);
|
110
|
+
const squareError = error * error;
|
111
|
+
if (squareError < minimumCurrentSqrError) {
|
112
|
+
minimumCurrentSqrError = squareError;
|
113
|
+
minimumErrorAtIndex = index;
|
114
|
+
}
|
115
|
+
}
|
116
|
+
templateIndex = minimumErrorAtIndex;
|
117
|
+
maximumSqrError = Math.max(minimumCurrentSqrError, maximumSqrError);
|
118
|
+
if (maximumSqrError > acceptMaximumSquareError) {
|
119
|
+
break;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
if (maximumSqrError < acceptMaximumSquareError) {
|
123
|
+
return templatePoints;
|
124
|
+
}
|
125
|
+
}
|
126
|
+
return null;
|
127
|
+
};
|
128
|
+
const template = selectTemplate(maxError);
|
129
|
+
if (!template) {
|
130
|
+
return null;
|
131
|
+
}
|
132
|
+
const lastDataPoint = this.points[this.points.length - 1];
|
133
|
+
const startWidth = this.startPoint.width;
|
134
|
+
const endWidth = lastDataPoint.width;
|
135
|
+
const startColor = this.startPoint.color;
|
136
|
+
const endColor = lastDataPoint.color;
|
137
|
+
const startTime = this.startPoint.time;
|
138
|
+
const endTime = lastDataPoint.time;
|
139
|
+
const templateIndexToStrokeDataPoint = (index) => {
|
140
|
+
const prevPoint = template[Math.max(0, Math.floor(index))];
|
141
|
+
const nextPoint = template[Math.min(Math.ceil(index), template.length - 1)];
|
142
|
+
const point = prevPoint.lerp(nextPoint, index - Math.floor(index));
|
143
|
+
const fractionToEnd = index / template.length;
|
144
|
+
return {
|
145
|
+
pos: this.viewport.screenToCanvas(point),
|
146
|
+
width: startWidth * (1 - fractionToEnd) + endWidth * fractionToEnd,
|
147
|
+
color: startColor.mix(endColor, fractionToEnd),
|
148
|
+
time: startTime * (1 - fractionToEnd) + endTime * fractionToEnd,
|
149
|
+
};
|
150
|
+
};
|
151
|
+
const builder = this.sourceFactory(templateIndexToStrokeDataPoint(0), this.viewport);
|
152
|
+
// Prevent the original builder from doing stroke smoothing if the template is short
|
153
|
+
// enough to likely have sharp corners.
|
154
|
+
const preventSmoothing = template.length < 10;
|
155
|
+
for (let i = 0; i < template.length; i++) {
|
156
|
+
if (preventSmoothing) {
|
157
|
+
builder.addPoint(templateIndexToStrokeDataPoint(i - 0.001));
|
158
|
+
}
|
159
|
+
builder.addPoint(templateIndexToStrokeDataPoint(i));
|
160
|
+
if (preventSmoothing) {
|
161
|
+
builder.addPoint(templateIndexToStrokeDataPoint(i + 0.001));
|
162
|
+
}
|
163
|
+
}
|
164
|
+
return builder.build();
|
165
|
+
}
|
166
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
const makeSnapToGridAutocorrect = (sourceFactory) => {
|
2
|
+
return (startPoint, viewport) => {
|
3
|
+
return new SnapToGridAutocompleteBuilder(sourceFactory, startPoint, viewport);
|
4
|
+
};
|
5
|
+
};
|
6
|
+
export default makeSnapToGridAutocorrect;
|
7
|
+
class SnapToGridAutocompleteBuilder {
|
8
|
+
constructor(sourceFactory, startPoint, viewport) {
|
9
|
+
this.sourceFactory = sourceFactory;
|
10
|
+
this.startPoint = startPoint;
|
11
|
+
this.viewport = viewport;
|
12
|
+
this.builder = sourceFactory(startPoint, viewport);
|
13
|
+
this.points = [startPoint];
|
14
|
+
}
|
15
|
+
getBBox() {
|
16
|
+
return this.builder.getBBox();
|
17
|
+
}
|
18
|
+
build() {
|
19
|
+
return this.builder.build();
|
20
|
+
}
|
21
|
+
preview(renderer) {
|
22
|
+
this.builder.preview(renderer);
|
23
|
+
}
|
24
|
+
addPoint(point) {
|
25
|
+
this.points.push(point);
|
26
|
+
this.builder.addPoint(point);
|
27
|
+
}
|
28
|
+
async autocorrectShape() {
|
29
|
+
const snapToGrid = (point) => {
|
30
|
+
return {
|
31
|
+
...point,
|
32
|
+
pos: this.viewport.snapToGrid(point.pos),
|
33
|
+
};
|
34
|
+
};
|
35
|
+
// Use screen points so that snapped shapes rotate with the screen.
|
36
|
+
const startPoint = snapToGrid(this.startPoint);
|
37
|
+
const builder = this.sourceFactory(startPoint, this.viewport);
|
38
|
+
const points = this.points.map(point => snapToGrid(point));
|
39
|
+
for (const point of points) {
|
40
|
+
builder.addPoint(point);
|
41
|
+
}
|
42
|
+
return builder.build();
|
43
|
+
}
|
44
|
+
}
|
@@ -7,6 +7,7 @@ export interface ComponentBuilder {
|
|
7
7
|
getBBox(): Rect2;
|
8
8
|
build(): AbstractComponent;
|
9
9
|
preview(renderer: AbstractRenderer): void;
|
10
|
+
autocorrectShape?: () => Promise<AbstractComponent | null>;
|
10
11
|
addPoint(point: StrokeDataPoint): void;
|
11
12
|
}
|
12
13
|
export type ComponentBuilderFactory = (startPoint: StrokeDataPoint, viewport: Viewport) => ComponentBuilder;
|
@@ -87,6 +87,9 @@ export default class EditorImage {
|
|
87
87
|
* rendered onto the main rendering canvas instead of doing a full re-render.
|
88
88
|
*
|
89
89
|
* @see {@link Display.flatten}
|
90
|
+
*
|
91
|
+
* @example
|
92
|
+
* [[include:doc-pages/inline-examples/adding-a-stroke.md]]
|
90
93
|
*/
|
91
94
|
static addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
|
92
95
|
/** @see EditorImage.addElement */
|
@@ -105,8 +108,36 @@ export default class EditorImage {
|
|
105
108
|
* autoresize (if it was previously enabled).
|
106
109
|
*/
|
107
110
|
setImportExportRect(imageRect: Rect2): SerializableCommand;
|
111
|
+
/** @see {@link setAutoresizeEnabled} */
|
108
112
|
getAutoresizeEnabled(): boolean;
|
109
|
-
/**
|
113
|
+
/**
|
114
|
+
* Returns a `Command` that sets whether the image should autoresize when
|
115
|
+
* {@link AbstractComponent}s are added/removed.
|
116
|
+
*
|
117
|
+
* @example
|
118
|
+
*
|
119
|
+
* ```ts,runnable
|
120
|
+
* import { Editor } from 'js-draw';
|
121
|
+
*
|
122
|
+
* const editor = new Editor(document.body);
|
123
|
+
* const toolbar = editor.addToolbar();
|
124
|
+
*
|
125
|
+
* // Add a save button to demonstrate what the output looks like
|
126
|
+
* // (it should change size to fit whatever was drawn)
|
127
|
+
* toolbar.addSaveButton(() => {
|
128
|
+
* document.body.replaceChildren(editor.toSVG({ sanitize: true }));
|
129
|
+
* });
|
130
|
+
*
|
131
|
+
* // Actually using setAutoresizeEnabled:
|
132
|
+
* //
|
133
|
+
* // To set autoresize without announcing for accessibility/making undoable
|
134
|
+
* const addToHistory = false;
|
135
|
+
* editor.dispatchNoAnnounce(editor.image.setAutoresizeEnabled(true), addToHistory);
|
136
|
+
*
|
137
|
+
* // Add to undo history **and** announce for accessibility
|
138
|
+
* //editor.dispatch(editor.image.setAutoresizeEnabled(true), true);
|
139
|
+
* ```
|
140
|
+
*/
|
110
141
|
setAutoresizeEnabled(autoresize: boolean): Command;
|
111
142
|
private setAutoresizeEnabledDirectly;
|
112
143
|
/** Updates the size/position of the viewport */
|
@@ -180,6 +180,9 @@ class EditorImage {
|
|
180
180
|
* rendered onto the main rendering canvas instead of doing a full re-render.
|
181
181
|
*
|
182
182
|
* @see {@link Display.flatten}
|
183
|
+
*
|
184
|
+
* @example
|
185
|
+
* [[include:doc-pages/inline-examples/adding-a-stroke.md]]
|
183
186
|
*/
|
184
187
|
static addElement(elem, applyByFlattening = false) {
|
185
188
|
return new _a.AddElementCommand(elem, applyByFlattening);
|
@@ -207,10 +210,38 @@ class EditorImage {
|
|
207
210
|
setImportExportRect(imageRect) {
|
208
211
|
return _a.SetImportExportRectCommand.of(this, imageRect, false);
|
209
212
|
}
|
213
|
+
/** @see {@link setAutoresizeEnabled} */
|
210
214
|
getAutoresizeEnabled() {
|
211
215
|
return this.shouldAutoresizeExportViewport;
|
212
216
|
}
|
213
|
-
/**
|
217
|
+
/**
|
218
|
+
* Returns a `Command` that sets whether the image should autoresize when
|
219
|
+
* {@link AbstractComponent}s are added/removed.
|
220
|
+
*
|
221
|
+
* @example
|
222
|
+
*
|
223
|
+
* ```ts,runnable
|
224
|
+
* import { Editor } from 'js-draw';
|
225
|
+
*
|
226
|
+
* const editor = new Editor(document.body);
|
227
|
+
* const toolbar = editor.addToolbar();
|
228
|
+
*
|
229
|
+
* // Add a save button to demonstrate what the output looks like
|
230
|
+
* // (it should change size to fit whatever was drawn)
|
231
|
+
* toolbar.addSaveButton(() => {
|
232
|
+
* document.body.replaceChildren(editor.toSVG({ sanitize: true }));
|
233
|
+
* });
|
234
|
+
*
|
235
|
+
* // Actually using setAutoresizeEnabled:
|
236
|
+
* //
|
237
|
+
* // To set autoresize without announcing for accessibility/making undoable
|
238
|
+
* const addToHistory = false;
|
239
|
+
* editor.dispatchNoAnnounce(editor.image.setAutoresizeEnabled(true), addToHistory);
|
240
|
+
*
|
241
|
+
* // Add to undo history **and** announce for accessibility
|
242
|
+
* //editor.dispatch(editor.image.setAutoresizeEnabled(true), true);
|
243
|
+
* ```
|
244
|
+
*/
|
214
245
|
setAutoresizeEnabled(autoresize) {
|
215
246
|
if (autoresize === this.shouldAutoresizeExportViewport) {
|
216
247
|
return Command.empty;
|
@@ -6,11 +6,15 @@ interface RenderablePathSpec {
|
|
6
6
|
style: RenderingStyle;
|
7
7
|
path?: Path;
|
8
8
|
}
|
9
|
-
interface RenderablePathSpecWithPath extends RenderablePathSpec {
|
9
|
+
export interface RenderablePathSpecWithPath extends RenderablePathSpec {
|
10
10
|
path: Path;
|
11
11
|
}
|
12
12
|
/** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
|
13
13
|
export declare const pathFromRenderable: (renderable: RenderablePathSpec) => Path;
|
14
|
+
/**
|
15
|
+
* Converts `path` into a format that can be rendered (by passing to a {@link Stroke} constructor
|
16
|
+
* or directly to an {@link AbstractRenderer.drawPath}).
|
17
|
+
*/
|
14
18
|
export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpecWithPath;
|
15
19
|
interface RectangleSimplificationResult {
|
16
20
|
rectangle: Rect2;
|
@@ -6,6 +6,10 @@ export const pathFromRenderable = (renderable) => {
|
|
6
6
|
}
|
7
7
|
return new Path(renderable.startPoint, renderable.commands);
|
8
8
|
};
|
9
|
+
/**
|
10
|
+
* Converts `path` into a format that can be rendered (by passing to a {@link Stroke} constructor
|
11
|
+
* or directly to an {@link AbstractRenderer.drawPath}).
|
12
|
+
*/
|
9
13
|
export const pathToRenderable = (path, style) => {
|
10
14
|
return {
|
11
15
|
startPoint: path.startPoint,
|
@@ -51,6 +51,8 @@ export default class IconProvider {
|
|
51
51
|
makePenIcon(penStyle: PenStyle): IconElemType;
|
52
52
|
makeIconFromFactory(penStyle: PenStyle): IconElemType;
|
53
53
|
makePipetteIcon(color?: Color4): IconElemType;
|
54
|
+
makeShapeAutocorrectIcon(): IconElemType;
|
55
|
+
makeStrokeSmoothingIcon(): IconElemType;
|
54
56
|
/** Unused. @deprecated */
|
55
57
|
makeFormatSelectionIcon(): IconElemType;
|
56
58
|
makeResizeImageToSelectionIcon(): IconElemType;
|
@@ -644,6 +644,23 @@ class IconProvider {
|
|
644
644
|
icon.setAttribute('viewBox', '5 -40 140 140');
|
645
645
|
return icon;
|
646
646
|
}
|
647
|
+
makeShapeAutocorrectIcon() {
|
648
|
+
const fill = 'none';
|
649
|
+
const strokeColor = 'var(--icon-color)';
|
650
|
+
return this.makeIconFromPath(`
|
651
|
+
m 79.129476,33.847107 9.967823,-0.03218 v 55 h -55 l 0.03218,-9.96782
|
652
|
+
M 71.1,40.8 a 30,30 0 0 1 -30,30 30,30 0 0 1 -30,-30 30,30 0 0 1 30,-30 30,30 0 0 1 30,30 L 71.1,40.8
|
653
|
+
M 34.1,58.8 v -25 h 25 v 0
|
654
|
+
`, fill, strokeColor, '7px');
|
655
|
+
}
|
656
|
+
makeStrokeSmoothingIcon() {
|
657
|
+
const fill = 'none';
|
658
|
+
const strokeColor = 'var(--icon-color)';
|
659
|
+
return this.makeIconFromPath(`
|
660
|
+
m 31,83.2 c -50,0 30,-65 -20,-65
|
661
|
+
M 75,17.3 40,59.7 38.2,77.6 55.5,72.4 90.5,30 Z
|
662
|
+
`, fill, strokeColor, '7px');
|
663
|
+
}
|
647
664
|
/** Unused. @deprecated */
|
648
665
|
makeFormatSelectionIcon() {
|
649
666
|
return this.makeIconFromPath(`
|
@@ -10,6 +10,8 @@ export interface ToolbarLocalization {
|
|
10
10
|
arrowPen: string;
|
11
11
|
image: string;
|
12
12
|
inputAltText: string;
|
13
|
+
decreaseImageSize: string;
|
14
|
+
resetImage: string;
|
13
15
|
chooseFile: string;
|
14
16
|
dragAndDropHereOrBrowse: string;
|
15
17
|
cancel: string;
|
@@ -48,6 +50,7 @@ export interface ToolbarLocalization {
|
|
48
50
|
toggleOverflow: string;
|
49
51
|
about: string;
|
50
52
|
inputStabilization: string;
|
53
|
+
strokeAutocorrect: string;
|
51
54
|
errorImageHasZeroSize: string;
|
52
55
|
closeSidebar: (toolName: string) => string;
|
53
56
|
dropdownShown: (toolName: string) => string;
|
@@ -7,6 +7,8 @@ export const defaultToolbarLocalization = {
|
|
7
7
|
image: 'Image',
|
8
8
|
reformatSelection: 'Format selection',
|
9
9
|
inputAltText: 'Alt text',
|
10
|
+
decreaseImageSize: 'Decrease size',
|
11
|
+
resetImage: 'Reset',
|
10
12
|
chooseFile: 'Choose file',
|
11
13
|
dragAndDropHereOrBrowse: 'Drag and drop here\nor\n{{browse}}',
|
12
14
|
submit: 'Submit',
|
@@ -37,7 +39,8 @@ export const defaultToolbarLocalization = {
|
|
37
39
|
enableAutoresizeOption: 'Auto-resize',
|
38
40
|
toggleOverflow: 'More',
|
39
41
|
about: 'About',
|
40
|
-
inputStabilization: '
|
42
|
+
inputStabilization: 'Stabilization',
|
43
|
+
strokeAutocorrect: 'Autocorrect',
|
41
44
|
touchPanning: 'Touchscreen panning',
|
42
45
|
roundedTipPen: 'Round',
|
43
46
|
flatTipPen: 'Flat',
|
@@ -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;
|
@@ -9,10 +9,48 @@ import BaseWidget from './BaseWidget.mjs';
|
|
9
9
|
import { EditorEventType } from '../../types.mjs';
|
10
10
|
import { toolbarCSSPrefix } from '../constants.mjs';
|
11
11
|
import makeFileInput from './components/makeFileInput.mjs';
|
12
|
+
class ImageWrapper {
|
13
|
+
constructor(imageBase64Url, preview, onUrlUpdate) {
|
14
|
+
this.imageBase64Url = imageBase64Url;
|
15
|
+
this.preview = preview;
|
16
|
+
this.onUrlUpdate = onUrlUpdate;
|
17
|
+
this.originalSrc = imageBase64Url;
|
18
|
+
preview.src = imageBase64Url;
|
19
|
+
}
|
20
|
+
updateImageData(base64DataUrl) {
|
21
|
+
this.preview.src = base64DataUrl;
|
22
|
+
this.imageBase64Url = base64DataUrl;
|
23
|
+
this.onUrlUpdate();
|
24
|
+
}
|
25
|
+
decreaseSize(resizeFactor = 3 / 4) {
|
26
|
+
const canvas = document.createElement('canvas');
|
27
|
+
canvas.width = this.preview.naturalWidth * resizeFactor;
|
28
|
+
canvas.height = this.preview.naturalHeight * resizeFactor;
|
29
|
+
const ctx = canvas.getContext('2d');
|
30
|
+
ctx?.drawImage(this.preview, 0, 0, canvas.width, canvas.height);
|
31
|
+
// JPEG can be much smaller than PNG for the same image size. Prefer it if
|
32
|
+
// the image is already a JPEG.
|
33
|
+
const format = this.originalSrc?.startsWith('data:image/jpeg;') ? 'image/jpeg' : 'image/png';
|
34
|
+
this.updateImageData(canvas.toDataURL(format));
|
35
|
+
}
|
36
|
+
reset() {
|
37
|
+
this.updateImageData(this.originalSrc);
|
38
|
+
}
|
39
|
+
isChanged() {
|
40
|
+
return this.imageBase64Url !== this.originalSrc;
|
41
|
+
}
|
42
|
+
getBase64Url() {
|
43
|
+
return this.imageBase64Url;
|
44
|
+
}
|
45
|
+
static fromSrcAndPreview(initialBase64Src, preview, onUrlUpdate) {
|
46
|
+
return new ImageWrapper(initialBase64Src, preview, onUrlUpdate);
|
47
|
+
}
|
48
|
+
}
|
12
49
|
class InsertImageWidget extends BaseWidget {
|
13
50
|
constructor(editor, localization) {
|
14
51
|
localization ??= editor.localization;
|
15
52
|
super(editor, 'insert-image-widget', localization);
|
53
|
+
this.image = null;
|
16
54
|
// Make the dropdown showable
|
17
55
|
this.container.classList.add('dropdownShowable');
|
18
56
|
editor.notifier.on(EditorEventType.SelectionUpdated, event => {
|
@@ -46,6 +84,7 @@ class InsertImageWidget extends BaseWidget {
|
|
46
84
|
this.statusView = document.createElement('div');
|
47
85
|
const actionButtonRow = document.createElement('div');
|
48
86
|
actionButtonRow.classList.add('action-button-row');
|
87
|
+
this.statusView.classList.add('insert-image-image-status-view');
|
49
88
|
this.submitButton = document.createElement('button');
|
50
89
|
this.selectedFiles = selectedFiles;
|
51
90
|
this.imageAltTextInput = document.createElement('input');
|
@@ -60,9 +99,8 @@ class InsertImageWidget extends BaseWidget {
|
|
60
99
|
this.submitButton.innerText = this.localizationTable.submit;
|
61
100
|
this.selectedFiles.onUpdateAndNow(async (files) => {
|
62
101
|
if (files.length === 0) {
|
63
|
-
this.
|
64
|
-
this.
|
65
|
-
this.submitButton.style.display = 'none';
|
102
|
+
this.image = null;
|
103
|
+
this.onImageDataUpdate();
|
66
104
|
return;
|
67
105
|
}
|
68
106
|
this.imagePreview.style.display = 'block';
|
@@ -74,18 +112,13 @@ class InsertImageWidget extends BaseWidget {
|
|
74
112
|
catch (e) {
|
75
113
|
this.statusView.innerText = this.localizationTable.imageLoadError(e);
|
76
114
|
}
|
77
|
-
this.imageBase64URL = data;
|
78
115
|
if (data) {
|
79
|
-
this.
|
80
|
-
this.submitButton.disabled = false;
|
81
|
-
this.submitButton.style.display = '';
|
82
|
-
this.updateImageSizeDisplay();
|
116
|
+
this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
|
83
117
|
}
|
84
118
|
else {
|
85
|
-
this.
|
86
|
-
this.submitButton.style.display = 'none';
|
87
|
-
this.statusView.innerText = '';
|
119
|
+
this.image = null;
|
88
120
|
}
|
121
|
+
this.onImageDataUpdate();
|
89
122
|
});
|
90
123
|
altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
|
91
124
|
actionButtonRow.replaceChildren(this.submitButton);
|
@@ -93,11 +126,27 @@ class InsertImageWidget extends BaseWidget {
|
|
93
126
|
dropdown.replaceChildren(container);
|
94
127
|
return true;
|
95
128
|
}
|
129
|
+
onImageDataUpdate() {
|
130
|
+
const base64Data = this.image?.getBase64Url();
|
131
|
+
if (base64Data) {
|
132
|
+
this.submitButton.disabled = false;
|
133
|
+
this.submitButton.style.display = '';
|
134
|
+
this.imagePreview.style.display = '';
|
135
|
+
this.updateImageSizeDisplay();
|
136
|
+
}
|
137
|
+
else {
|
138
|
+
this.submitButton.disabled = true;
|
139
|
+
this.submitButton.style.display = 'none';
|
140
|
+
this.statusView.innerText = '';
|
141
|
+
this.imagePreview.style.display = 'none';
|
142
|
+
this.submitButton.disabled = true;
|
143
|
+
}
|
144
|
+
}
|
96
145
|
hideDialog() {
|
97
146
|
this.setDropdownVisible(false);
|
98
147
|
}
|
99
148
|
updateImageSizeDisplay() {
|
100
|
-
const imageData = this.
|
149
|
+
const imageData = this.image?.getBase64Url() ?? '';
|
101
150
|
const sizeInKiB = imageData.length / 1024;
|
102
151
|
const sizeInMiB = sizeInKiB / 1024;
|
103
152
|
let units = 'KiB';
|
@@ -106,7 +155,27 @@ class InsertImageWidget extends BaseWidget {
|
|
106
155
|
size = sizeInMiB;
|
107
156
|
units = 'MiB';
|
108
157
|
}
|
109
|
-
|
158
|
+
const sizeText = document.createElement('span');
|
159
|
+
sizeText.innerText = this.localizationTable.imageSize(Math.round(size), units);
|
160
|
+
// Add a button to allow decreasing the size of large images.
|
161
|
+
const decreaseSizeButton = document.createElement('button');
|
162
|
+
decreaseSizeButton.innerText = this.localizationTable.decreaseImageSize;
|
163
|
+
decreaseSizeButton.onclick = () => {
|
164
|
+
this.image?.decreaseSize();
|
165
|
+
};
|
166
|
+
const resetSizeButton = document.createElement('button');
|
167
|
+
resetSizeButton.innerText = this.localizationTable.resetImage;
|
168
|
+
resetSizeButton.onclick = () => {
|
169
|
+
this.image?.reset();
|
170
|
+
};
|
171
|
+
this.statusView.replaceChildren(sizeText);
|
172
|
+
const largeImageThreshold = 0.25; // MiB
|
173
|
+
if (sizeInMiB > largeImageThreshold) {
|
174
|
+
this.statusView.appendChild(decreaseSizeButton);
|
175
|
+
}
|
176
|
+
else if (this.image?.isChanged()) {
|
177
|
+
this.statusView.appendChild(resetSizeButton);
|
178
|
+
}
|
110
179
|
}
|
111
180
|
updateInputs() {
|
112
181
|
const resetInputs = () => {
|
@@ -126,11 +195,8 @@ class InsertImageWidget extends BaseWidget {
|
|
126
195
|
if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
|
127
196
|
editingImage = selectedObjects[0];
|
128
197
|
this.imageAltTextInput.value = editingImage.getAltText() ?? '';
|
129
|
-
this.
|
130
|
-
this.
|
131
|
-
this.imageBase64URL = editingImage.getURL();
|
132
|
-
this.imagePreview.src = this.imageBase64URL;
|
133
|
-
this.updateImageSizeDisplay();
|
198
|
+
this.image = ImageWrapper.fromSrcAndPreview(editingImage.getURL(), this.imagePreview, () => this.onImageDataUpdate());
|
199
|
+
this.onImageDataUpdate();
|
134
200
|
}
|
135
201
|
else if (selectedObjects.length > 0) {
|
136
202
|
// If not, clear the selection.
|
@@ -144,13 +210,21 @@ class InsertImageWidget extends BaseWidget {
|
|
144
210
|
}
|
145
211
|
};
|
146
212
|
this.submitButton.onclick = async () => {
|
147
|
-
if (!this.
|
213
|
+
if (!this.image) {
|
148
214
|
return;
|
149
215
|
}
|
150
216
|
const image = new Image();
|
151
|
-
image.src = this.
|
217
|
+
image.src = this.image.getBase64Url();
|
152
218
|
image.setAttribute('alt', this.imageAltTextInput.value);
|
153
|
-
|
219
|
+
let component;
|
220
|
+
try {
|
221
|
+
component = await ImageComponent.fromImage(image, Mat33.identity);
|
222
|
+
}
|
223
|
+
catch (error) {
|
224
|
+
console.error('Error loading image', error);
|
225
|
+
this.statusView.innerText = this.localizationTable.imageLoadError(error);
|
226
|
+
return;
|
227
|
+
}
|
154
228
|
if (component.getBBox().area === 0) {
|
155
229
|
this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
|
156
230
|
return;
|
@@ -158,9 +232,15 @@ class InsertImageWidget extends BaseWidget {
|
|
158
232
|
this.hideDialog();
|
159
233
|
if (editingImage) {
|
160
234
|
const eraseCommand = new Erase([editingImage]);
|
235
|
+
// Try to preserve the original width
|
236
|
+
const originalTransform = editingImage.getTransformation();
|
237
|
+
// || 1: Prevent division by zero
|
238
|
+
const originalWidth = editingImage.getBBox().width || 1;
|
239
|
+
const newWidth = component.getBBox().transformedBoundingBox(originalTransform).width || 1;
|
240
|
+
const widthAdjustTransform = Mat33.scaling2D(originalWidth / newWidth);
|
161
241
|
await this.editor.dispatch(uniteCommands([
|
162
242
|
EditorImage.addElement(component),
|
163
|
-
component.transformBy(
|
243
|
+
component.transformBy(originalTransform.rightMul(widthAdjustTransform)),
|
164
244
|
component.setZIndex(editingImage.getZIndex()),
|
165
245
|
eraseCommand,
|
166
246
|
]));
|
@@ -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
|
};
|