js-draw 0.8.0 → 0.9.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 +8 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.js +3 -0
- package/dist/src/SVGLoader.js +5 -7
- package/dist/src/components/Stroke.js +2 -2
- package/dist/src/math/Path.js +6 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
- package/dist/src/toolbar/HTMLToolbar.js +24 -0
- package/dist/src/toolbar/IconProvider.d.ts +1 -0
- package/dist/src/toolbar/IconProvider.js +43 -1
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/toolbar/makeColorInput.d.ts +2 -1
- package/dist/src/toolbar/makeColorInput.js +13 -2
- package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
- package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
- package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
- package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
- package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
- package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
- package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
- package/dist/src/toolbar/widgets/PenToolWidget.js +78 -12
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
- package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
- package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
- package/dist/src/tools/PanZoom.d.ts +4 -1
- package/dist/src/tools/PanZoom.js +24 -1
- package/package.json +1 -1
- package/src/Color4.ts +2 -0
- package/src/SVGLoader.ts +8 -8
- package/src/components/Stroke.ts +1 -1
- package/src/math/Path.test.ts +24 -0
- package/src/math/Path.ts +7 -1
- package/src/toolbar/HTMLToolbar.ts +33 -0
- package/src/toolbar/IconProvider.ts +49 -1
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/makeColorInput.ts +21 -3
- package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
- package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
- package/src/toolbar/widgets/BaseWidget.ts +83 -5
- package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
- package/src/toolbar/widgets/HandToolWidget.ts +48 -17
- package/src/toolbar/widgets/PenToolWidget.ts +105 -13
- package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
- package/src/toolbar/widgets/TextToolWidget.ts +29 -4
- package/src/tools/PanZoom.ts +28 -1
@@ -7,35 +7,42 @@ import { EditorEventType } from '../../types';
|
|
7
7
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
8
8
|
import makeColorInput from '../makeColorInput';
|
9
9
|
import BaseToolWidget from './BaseToolWidget';
|
10
|
+
import Color4 from '../../Color4';
|
10
11
|
export default class PenToolWidget extends BaseToolWidget {
|
11
12
|
constructor(editor, tool, localization) {
|
12
|
-
super(editor, tool, localization);
|
13
|
+
super(editor, tool, 'pen', localization);
|
13
14
|
this.tool = tool;
|
14
15
|
this.updateInputs = () => { };
|
15
16
|
// Default pen types
|
16
17
|
this.penTypes = [
|
17
18
|
{
|
18
|
-
name:
|
19
|
+
name: this.localizationTable.pressureSensitiveFreehandPen,
|
20
|
+
id: 'pressure-sensitive-pen',
|
19
21
|
factory: makePressureSensitiveFreehandLineBuilder,
|
20
22
|
},
|
21
23
|
{
|
22
|
-
name:
|
24
|
+
name: this.localizationTable.freehandPen,
|
25
|
+
id: 'freehand-pen',
|
23
26
|
factory: makeFreehandLineBuilder,
|
24
27
|
},
|
25
28
|
{
|
26
|
-
name:
|
29
|
+
name: this.localizationTable.arrowPen,
|
30
|
+
id: 'arrow',
|
27
31
|
factory: makeArrowBuilder,
|
28
32
|
},
|
29
33
|
{
|
30
|
-
name:
|
34
|
+
name: this.localizationTable.linePen,
|
35
|
+
id: 'line',
|
31
36
|
factory: makeLineBuilder,
|
32
37
|
},
|
33
38
|
{
|
34
|
-
name:
|
39
|
+
name: this.localizationTable.filledRectanglePen,
|
40
|
+
id: 'filled-rectangle',
|
35
41
|
factory: makeFilledRectangleBuilder,
|
36
42
|
},
|
37
43
|
{
|
38
|
-
name:
|
44
|
+
name: this.localizationTable.outlinedRectanglePen,
|
45
|
+
id: 'outlined-rectangle',
|
39
46
|
factory: makeOutlinedRectangleBuilder,
|
40
47
|
},
|
41
48
|
];
|
@@ -53,6 +60,27 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
53
60
|
getTitle() {
|
54
61
|
return this.targetTool.description;
|
55
62
|
}
|
63
|
+
// Return the index of this tool's stroke factory in the list of
|
64
|
+
// all stroke factories.
|
65
|
+
//
|
66
|
+
// Returns -1 if the stroke factory is not in the list of all stroke factories.
|
67
|
+
getCurrentPenTypeIdx() {
|
68
|
+
const currentFactory = this.tool.getStrokeFactory();
|
69
|
+
for (let i = 0; i < this.penTypes.length; i++) {
|
70
|
+
if (this.penTypes[i].factory === currentFactory) {
|
71
|
+
return i;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
return -1;
|
75
|
+
}
|
76
|
+
getCurrentPenType() {
|
77
|
+
for (const penType of this.penTypes) {
|
78
|
+
if (penType.factory === this.tool.getStrokeFactory()) {
|
79
|
+
return penType;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
return null;
|
83
|
+
}
|
56
84
|
createIcon() {
|
57
85
|
const strokeFactory = this.tool.getStrokeFactory();
|
58
86
|
if (strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
|
@@ -106,7 +134,7 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
106
134
|
objectTypeRow.appendChild(objectTypeSelect);
|
107
135
|
const colorRow = document.createElement('div');
|
108
136
|
const colorLabel = document.createElement('label');
|
109
|
-
const [colorInput, colorInputContainer] = makeColorInput(this.editor, color => {
|
137
|
+
const [colorInput, colorInputContainer, setColorInputValue] = makeColorInput(this.editor, color => {
|
110
138
|
this.tool.setColor(color);
|
111
139
|
});
|
112
140
|
colorInput.id = `${toolbarCSSPrefix}colorInput${PenToolWidget.idCounter++}`;
|
@@ -115,8 +143,9 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
115
143
|
colorRow.appendChild(colorLabel);
|
116
144
|
colorRow.appendChild(colorInputContainer);
|
117
145
|
this.updateInputs = () => {
|
118
|
-
|
146
|
+
setColorInputValue(this.tool.getColor());
|
119
147
|
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
|
148
|
+
// Update the list of stroke factories
|
120
149
|
objectTypeSelect.replaceChildren();
|
121
150
|
for (let i = 0; i < this.penTypes.length; i++) {
|
122
151
|
const penType = this.penTypes[i];
|
@@ -124,9 +153,14 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
124
153
|
option.value = i.toString();
|
125
154
|
option.innerText = penType.name;
|
126
155
|
objectTypeSelect.appendChild(option);
|
127
|
-
|
128
|
-
|
129
|
-
|
156
|
+
}
|
157
|
+
// Update the selected stroke factory.
|
158
|
+
const strokeFactoryIdx = this.getCurrentPenTypeIdx();
|
159
|
+
if (strokeFactoryIdx === -1) {
|
160
|
+
objectTypeSelect.value = '';
|
161
|
+
}
|
162
|
+
else {
|
163
|
+
objectTypeSelect.value = strokeFactoryIdx.toString();
|
130
164
|
}
|
131
165
|
};
|
132
166
|
this.updateInputs();
|
@@ -148,5 +182,37 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
148
182
|
}
|
149
183
|
return false;
|
150
184
|
}
|
185
|
+
serializeState() {
|
186
|
+
var _a;
|
187
|
+
return Object.assign(Object.assign({}, super.serializeState()), { color: this.tool.getColor().toHexString(), thickness: this.tool.getThickness(), strokeFactoryId: (_a = this.getCurrentPenType()) === null || _a === void 0 ? void 0 : _a.id });
|
188
|
+
}
|
189
|
+
deserializeFrom(state) {
|
190
|
+
super.deserializeFrom(state);
|
191
|
+
const verifyPropertyType = (propertyName, expectedType) => {
|
192
|
+
const actualType = typeof (state[propertyName]);
|
193
|
+
if (actualType !== expectedType) {
|
194
|
+
throw new Error(`Deserializing property ${propertyName}: Invalid type. Expected ${expectedType},` +
|
195
|
+
` was ${actualType}.`);
|
196
|
+
}
|
197
|
+
};
|
198
|
+
if (state.color) {
|
199
|
+
verifyPropertyType('color', 'string');
|
200
|
+
this.tool.setColor(Color4.fromHex(state.color));
|
201
|
+
}
|
202
|
+
if (state.thickness) {
|
203
|
+
verifyPropertyType('thickness', 'number');
|
204
|
+
this.tool.setThickness(state.thickness);
|
205
|
+
}
|
206
|
+
if (state.strokeFactoryId) {
|
207
|
+
verifyPropertyType('strokeFactoryId', 'string');
|
208
|
+
const factoryId = state.strokeFactoryId;
|
209
|
+
for (const penType of this.penTypes) {
|
210
|
+
if (factoryId === penType.id) {
|
211
|
+
this.tool.setStrokeFactory(penType.factory);
|
212
|
+
break;
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
151
217
|
}
|
152
218
|
PenToolWidget.idCounter = 0;
|
@@ -5,7 +5,7 @@ import { ToolbarLocalization } from '../localization';
|
|
5
5
|
import BaseToolWidget from './BaseToolWidget';
|
6
6
|
export default class SelectionToolWidget extends BaseToolWidget {
|
7
7
|
private tool;
|
8
|
-
constructor(editor: Editor, tool: SelectionTool, localization
|
8
|
+
constructor(editor: Editor, tool: SelectionTool, localization?: ToolbarLocalization);
|
9
9
|
private resizeImageToSelection;
|
10
10
|
protected onKeyPress(event: KeyPressEvent): boolean;
|
11
11
|
protected getTitle(): string;
|
@@ -3,20 +3,20 @@ import ActionButtonWidget from './ActionButtonWidget';
|
|
3
3
|
import BaseToolWidget from './BaseToolWidget';
|
4
4
|
export default class SelectionToolWidget extends BaseToolWidget {
|
5
5
|
constructor(editor, tool, localization) {
|
6
|
-
super(editor, tool, localization);
|
6
|
+
super(editor, tool, 'selection-tool-widget', localization);
|
7
7
|
this.tool = tool;
|
8
|
-
const resizeButton = new ActionButtonWidget(editor,
|
8
|
+
const resizeButton = new ActionButtonWidget(editor, 'resize-btn', () => editor.icons.makeResizeViewportIcon(), this.localizationTable.resizeImageToSelection, () => {
|
9
9
|
this.resizeImageToSelection();
|
10
|
-
});
|
11
|
-
const deleteButton = new ActionButtonWidget(editor,
|
10
|
+
}, localization);
|
11
|
+
const deleteButton = new ActionButtonWidget(editor, 'delete-btn', () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
|
12
12
|
const selection = this.tool.getSelection();
|
13
13
|
this.editor.dispatch(selection.deleteSelectedObjects());
|
14
14
|
this.tool.clearSelection();
|
15
|
-
});
|
16
|
-
const duplicateButton = new ActionButtonWidget(editor,
|
15
|
+
}, localization);
|
16
|
+
const duplicateButton = new ActionButtonWidget(editor, 'duplicate-btn', () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => {
|
17
17
|
const selection = this.tool.getSelection();
|
18
18
|
this.editor.dispatch(selection.duplicateSelectedObjects());
|
19
|
-
});
|
19
|
+
}, localization);
|
20
20
|
this.addSubWidget(resizeButton);
|
21
21
|
this.addSubWidget(deleteButton);
|
22
22
|
this.addSubWidget(duplicateButton);
|
@@ -2,12 +2,15 @@ import Editor from '../../Editor';
|
|
2
2
|
import TextTool from '../../tools/TextTool';
|
3
3
|
import { ToolbarLocalization } from '../localization';
|
4
4
|
import BaseToolWidget from './BaseToolWidget';
|
5
|
+
import { SavedToolbuttonState } from './BaseWidget';
|
5
6
|
export default class TextToolWidget extends BaseToolWidget {
|
6
7
|
private tool;
|
7
8
|
private updateDropdownInputs;
|
8
|
-
constructor(editor: Editor, tool: TextTool, localization
|
9
|
+
constructor(editor: Editor, tool: TextTool, localization?: ToolbarLocalization);
|
9
10
|
protected getTitle(): string;
|
10
11
|
protected createIcon(): Element;
|
11
12
|
private static idCounter;
|
12
13
|
protected fillDropdown(dropdown: HTMLElement): boolean;
|
14
|
+
serializeState(): SavedToolbuttonState;
|
15
|
+
deserializeFrom(state: SavedToolbuttonState): void;
|
13
16
|
}
|
@@ -1,10 +1,11 @@
|
|
1
|
+
import Color4 from '../../Color4';
|
1
2
|
import { EditorEventType } from '../../types';
|
2
3
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
4
|
import makeColorInput from '../makeColorInput';
|
4
5
|
import BaseToolWidget from './BaseToolWidget';
|
5
6
|
export default class TextToolWidget extends BaseToolWidget {
|
6
7
|
constructor(editor, tool, localization) {
|
7
|
-
super(editor, tool, localization);
|
8
|
+
super(editor, tool, 'text-tool-widget', localization);
|
8
9
|
this.tool = tool;
|
9
10
|
this.updateDropdownInputs = null;
|
10
11
|
editor.notifier.on(EditorEventType.ToolUpdated, evt => {
|
@@ -27,7 +28,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
27
28
|
const colorRow = document.createElement('div');
|
28
29
|
const fontInput = document.createElement('select');
|
29
30
|
const fontLabel = document.createElement('label');
|
30
|
-
const [colorInput, colorInputContainer] = makeColorInput(this.editor, color => {
|
31
|
+
const [colorInput, colorInputContainer, setColorInputValue] = makeColorInput(this.editor, color => {
|
31
32
|
this.tool.setColor(color);
|
32
33
|
});
|
33
34
|
const colorLabel = document.createElement('label');
|
@@ -57,7 +58,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
57
58
|
fontRow.appendChild(fontInput);
|
58
59
|
this.updateDropdownInputs = () => {
|
59
60
|
const style = this.tool.getTextStyle();
|
60
|
-
|
61
|
+
setColorInputValue(style.renderingStyle.fill);
|
61
62
|
if (!fontsInInput.has(style.fontFamily)) {
|
62
63
|
addFontToInput(style.fontFamily);
|
63
64
|
}
|
@@ -67,5 +68,18 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
67
68
|
dropdown.replaceChildren(colorRow, fontRow);
|
68
69
|
return true;
|
69
70
|
}
|
71
|
+
serializeState() {
|
72
|
+
const textStyle = this.tool.getTextStyle();
|
73
|
+
return Object.assign(Object.assign({}, super.serializeState()), { fontFamily: textStyle.fontFamily, color: textStyle.renderingStyle.fill.toHexString() });
|
74
|
+
}
|
75
|
+
deserializeFrom(state) {
|
76
|
+
if (state.fontFamily && typeof (state.fontFamily) === 'string') {
|
77
|
+
this.tool.setFontFamily(state.fontFamily);
|
78
|
+
}
|
79
|
+
if (state.color && typeof (state.color) === 'string') {
|
80
|
+
this.tool.setColor(Color4.fromHex(state.color));
|
81
|
+
}
|
82
|
+
super.deserializeFrom(state);
|
83
|
+
}
|
70
84
|
}
|
71
85
|
TextToolWidget.idCounter = 0;
|
@@ -14,7 +14,8 @@ export declare enum PanZoomMode {
|
|
14
14
|
TwoFingerTouchGestures = 2,
|
15
15
|
RightClickDrags = 4,
|
16
16
|
SinglePointerGestures = 8,
|
17
|
-
Keyboard = 16
|
17
|
+
Keyboard = 16,
|
18
|
+
RotationLocked = 32
|
18
19
|
}
|
19
20
|
export default class PanZoom extends BaseTool {
|
20
21
|
private editor;
|
@@ -36,6 +37,8 @@ export default class PanZoom extends BaseTool {
|
|
36
37
|
private updateTransform;
|
37
38
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
38
39
|
onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean;
|
40
|
+
private isRotationLocked;
|
41
|
+
setModeEnabled(mode: PanZoomMode, enabled: boolean): void;
|
39
42
|
setMode(mode: PanZoomMode): void;
|
40
43
|
getMode(): PanZoomMode;
|
41
44
|
}
|
@@ -12,6 +12,7 @@ export var PanZoomMode;
|
|
12
12
|
PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
|
13
13
|
PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
|
14
14
|
PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
|
15
|
+
PanZoomMode[PanZoomMode["RotationLocked"] = 32] = "RotationLocked";
|
15
16
|
})(PanZoomMode || (PanZoomMode = {}));
|
16
17
|
export default class PanZoom extends BaseTool {
|
17
18
|
constructor(editor, mode, description) {
|
@@ -66,9 +67,13 @@ export default class PanZoom extends BaseTool {
|
|
66
67
|
handleTwoFingerMove(allPointers) {
|
67
68
|
const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
68
69
|
const delta = this.getCenterDelta(screenCenter);
|
70
|
+
let rotation = angle - this.lastAngle;
|
71
|
+
if (this.isRotationLocked()) {
|
72
|
+
rotation = 0;
|
73
|
+
}
|
69
74
|
const transformUpdate = Mat33.translation(delta)
|
70
75
|
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
71
|
-
.rightMul(Mat33.zRotation(
|
76
|
+
.rightMul(Mat33.zRotation(rotation, canvasCenter));
|
72
77
|
this.lastScreenCenter = screenCenter;
|
73
78
|
this.lastDist = dist;
|
74
79
|
this.lastAngle = angle;
|
@@ -195,6 +200,9 @@ export default class PanZoom extends BaseTool {
|
|
195
200
|
if (rotation !== 0) {
|
196
201
|
rotation += 0.0001;
|
197
202
|
}
|
203
|
+
if (this.isRotationLocked()) {
|
204
|
+
rotation = 0;
|
205
|
+
}
|
198
206
|
const toCanvas = this.editor.viewport.screenToCanvasTransform;
|
199
207
|
// Transform without translating (treat toCanvas as a linear instead of
|
200
208
|
// an affine transformation).
|
@@ -205,6 +213,21 @@ export default class PanZoom extends BaseTool {
|
|
205
213
|
this.updateTransform(transformUpdate, true);
|
206
214
|
return true;
|
207
215
|
}
|
216
|
+
isRotationLocked() {
|
217
|
+
return !!(this.mode & PanZoomMode.RotationLocked);
|
218
|
+
}
|
219
|
+
// Sets whether the given `mode` is enabled. `mode` should be a single
|
220
|
+
// mode from the `PanZoomMode` enum.
|
221
|
+
setModeEnabled(mode, enabled) {
|
222
|
+
let newMode = this.mode;
|
223
|
+
if (enabled) {
|
224
|
+
newMode |= mode;
|
225
|
+
}
|
226
|
+
else {
|
227
|
+
newMode &= ~mode;
|
228
|
+
}
|
229
|
+
this.setMode(newMode);
|
230
|
+
}
|
208
231
|
setMode(mode) {
|
209
232
|
if (mode !== this.mode) {
|
210
233
|
this.mode = mode;
|
package/package.json
CHANGED
package/src/Color4.ts
CHANGED
@@ -73,6 +73,8 @@ export default class Color4 {
|
|
73
73
|
public static fromString(text: string): Color4 {
|
74
74
|
if (text.startsWith('#')) {
|
75
75
|
return Color4.fromHex(text);
|
76
|
+
} else if (text === 'none' || text === 'transparent') {
|
77
|
+
return Color4.transparent;
|
76
78
|
} else {
|
77
79
|
// Otherwise, try to use an HTML5Canvas to determine the color
|
78
80
|
const canvas = document.createElement('canvas');
|
package/src/SVGLoader.ts
CHANGED
@@ -28,6 +28,9 @@ export type SVGLoaderUnknownAttribute = [ string, string ];
|
|
28
28
|
// [key, value, priority]
|
29
29
|
export type SVGLoaderUnknownStyleAttribute = { key: string, value: string, priority?: string };
|
30
30
|
|
31
|
+
|
32
|
+
const supportedStrokeFillStyleAttrs = [ 'stroke', 'fill', 'stroke-width' ];
|
33
|
+
|
31
34
|
export default class SVGLoader implements ImageLoader {
|
32
35
|
private onAddComponent: ComponentAddedListener|null = null;
|
33
36
|
private onProgress: OnProgressListener|null = null;
|
@@ -154,11 +157,10 @@ export default class SVGLoader implements ImageLoader {
|
|
154
157
|
|
155
158
|
elem = new Stroke(strokeData);
|
156
159
|
|
157
|
-
const supportedStyleAttrs = [ 'stroke', 'fill', 'stroke-width' ];
|
158
160
|
this.attachUnrecognisedAttrs(
|
159
161
|
elem, node,
|
160
|
-
new Set([ ...
|
161
|
-
new Set(
|
162
|
+
new Set([ ...supportedStrokeFillStyleAttrs, 'd' ]),
|
163
|
+
new Set(supportedStrokeFillStyleAttrs)
|
162
164
|
);
|
163
165
|
} catch (e) {
|
164
166
|
console.error(
|
@@ -234,8 +236,8 @@ export default class SVGLoader implements ImageLoader {
|
|
234
236
|
|
235
237
|
const supportedStyleAttrs = [
|
236
238
|
'fontFamily',
|
237
|
-
'
|
238
|
-
|
239
|
+
'transform',
|
240
|
+
...supportedStrokeFillStyleAttrs,
|
239
241
|
];
|
240
242
|
let fontSize = 12;
|
241
243
|
if (fontSizeMatch) {
|
@@ -245,9 +247,7 @@ export default class SVGLoader implements ImageLoader {
|
|
245
247
|
const style: TextStyle = {
|
246
248
|
size: fontSize,
|
247
249
|
fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
|
248
|
-
renderingStyle:
|
249
|
-
fill: Color4.fromString(computedStyles.fill || elem.style.fill || '#000')
|
250
|
-
},
|
250
|
+
renderingStyle: this.getStyle(elem),
|
251
251
|
};
|
252
252
|
|
253
253
|
const supportedAttrs: string[] = [];
|
package/src/components/Stroke.ts
CHANGED
@@ -61,7 +61,7 @@ export default class Stroke extends AbstractComponent {
|
|
61
61
|
}
|
62
62
|
|
63
63
|
const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
|
64
|
-
if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, part.style.stroke?.width)) {
|
64
|
+
if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, part.style.stroke?.width ?? 0)) {
|
65
65
|
continue;
|
66
66
|
}
|
67
67
|
}
|
package/src/math/Path.test.ts
CHANGED
@@ -171,4 +171,28 @@ describe('Path', () => {
|
|
171
171
|
).toBe(true);
|
172
172
|
});
|
173
173
|
});
|
174
|
+
|
175
|
+
describe('fromRect', () => {
|
176
|
+
const filledRect = Path.fromRect(Rect2.unitSquare);
|
177
|
+
const strokedRect = Path.fromRect(Rect2.unitSquare, 0.1);
|
178
|
+
|
179
|
+
it('filled should be closed shape', () => {
|
180
|
+
const lastSegment = filledRect.parts[filledRect.parts.length - 1];
|
181
|
+
|
182
|
+
if (lastSegment.kind !== PathCommandType.LineTo) {
|
183
|
+
throw new Error('Rectangles should only be made up of lines');
|
184
|
+
}
|
185
|
+
|
186
|
+
expect(filledRect.startPoint).objEq(lastSegment.point);
|
187
|
+
});
|
188
|
+
|
189
|
+
it('stroked should be closed shape', () => {
|
190
|
+
const lastSegment = strokedRect.parts[strokedRect.parts.length - 1];
|
191
|
+
if (lastSegment.kind !== PathCommandType.LineTo) {
|
192
|
+
throw new Error('Rectangles should only be made up of lines');
|
193
|
+
}
|
194
|
+
|
195
|
+
expect(strokedRect.startPoint).objEq(lastSegment.point);
|
196
|
+
});
|
197
|
+
});
|
174
198
|
});
|
package/src/math/Path.ts
CHANGED
@@ -285,7 +285,7 @@ export default class Path {
|
|
285
285
|
}
|
286
286
|
const isClosed = this.startPoint.eq(this.getEndPoint());
|
287
287
|
|
288
|
-
if (isClosed && strokeWidth
|
288
|
+
if (isClosed && strokeWidth === 0) {
|
289
289
|
return this.closedRoughlyIntersects(rect);
|
290
290
|
}
|
291
291
|
|
@@ -401,6 +401,12 @@ export default class Path {
|
|
401
401
|
});
|
402
402
|
}
|
403
403
|
|
404
|
+
// Close the shape
|
405
|
+
commands.push({
|
406
|
+
kind: PathCommandType.LineTo,
|
407
|
+
point: startPoint,
|
408
|
+
});
|
409
|
+
|
404
410
|
return new Path(startPoint, commands);
|
405
411
|
}
|
406
412
|
|
@@ -24,6 +24,8 @@ type UpdateColorisCallback = ()=>void;
|
|
24
24
|
export default class HTMLToolbar {
|
25
25
|
private container: HTMLElement;
|
26
26
|
|
27
|
+
private widgets: Record<string, BaseWidget> = {};
|
28
|
+
|
27
29
|
private static colorisStarted: boolean = false;
|
28
30
|
private updateColoris: UpdateColorisCallback|null = null;
|
29
31
|
|
@@ -121,10 +123,41 @@ export default class HTMLToolbar {
|
|
121
123
|
// Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
|
122
124
|
// (i.e. its `addTo` method should not have been called).
|
123
125
|
public addWidget(widget: BaseWidget) {
|
126
|
+
// Prevent name collisions
|
127
|
+
const id = widget.getUniqueIdIn(this.widgets);
|
128
|
+
|
129
|
+
// Add the widget
|
130
|
+
this.widgets[id] = widget;
|
131
|
+
|
132
|
+
// Add HTML elements.
|
124
133
|
widget.addTo(this.container);
|
125
134
|
this.setupColorPickers();
|
126
135
|
}
|
127
136
|
|
137
|
+
public serializeState(): string {
|
138
|
+
const result: Record<string, any> = {};
|
139
|
+
|
140
|
+
for (const widgetId in this.widgets) {
|
141
|
+
result[widgetId] = this.widgets[widgetId].serializeState();
|
142
|
+
}
|
143
|
+
|
144
|
+
return JSON.stringify(result);
|
145
|
+
}
|
146
|
+
|
147
|
+
// Deserialize toolbar widgets from the given state.
|
148
|
+
// Assumes that toolbar widgets are in the same order as when state was serialized.
|
149
|
+
public deserializeState(state: string) {
|
150
|
+
const data = JSON.parse(state);
|
151
|
+
|
152
|
+
for (const widgetId in data) {
|
153
|
+
if (!(widgetId in this.widgets)) {
|
154
|
+
console.warn(`Unable to deserialize widget ${widgetId} — no such widget.`);
|
155
|
+
}
|
156
|
+
|
157
|
+
this.widgets[widgetId].deserializeFrom(data[widgetId]);
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
128
161
|
public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
|
129
162
|
const button = document.createElement('button');
|
130
163
|
button.classList.add(`${toolbarCSSPrefix}button`);
|
@@ -8,6 +8,9 @@ import Pen from '../tools/Pen';
|
|
8
8
|
import { StrokeDataPoint } from '../types';
|
9
9
|
import Viewport from '../Viewport';
|
10
10
|
|
11
|
+
// Provides a default set of icons for the editor.
|
12
|
+
// Many of the icons were created with Inkscape.
|
13
|
+
|
11
14
|
type IconType = SVGSVGElement|HTMLImageElement;
|
12
15
|
|
13
16
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
@@ -138,7 +141,7 @@ export default class IconProvider {
|
|
138
141
|
const strokeColor = 'var(--icon-color)';
|
139
142
|
const strokeWidth = '3';
|
140
143
|
|
141
|
-
// Draw a cursor-like shape
|
144
|
+
// Draw a cursor-like shape
|
142
145
|
return this.makeIconFromPath(`
|
143
146
|
m 10,60
|
144
147
|
5,30
|
@@ -275,6 +278,51 @@ export default class IconProvider {
|
|
275
278
|
|
276
279
|
return icon;
|
277
280
|
}
|
281
|
+
|
282
|
+
public makeRotationLockIcon(): IconType {
|
283
|
+
const icon = this.makeIconFromPath(`
|
284
|
+
M 40.1 25.1
|
285
|
+
C 32.5 25 27.9 34.1 27.9 34.1
|
286
|
+
L 25.7 30
|
287
|
+
L 28 44.7
|
288
|
+
L 36.6 40.3
|
289
|
+
L 32.3 38.3
|
290
|
+
C 33.6 28 38.1 25.2 45.1 31.8
|
291
|
+
L 49.4 29.6
|
292
|
+
C 45.9 26.3 42.8 25.1 40.1 25.1
|
293
|
+
z
|
294
|
+
|
295
|
+
M 51.7 34.2
|
296
|
+
L 43.5 39.1
|
297
|
+
L 48 40.8
|
298
|
+
C 47.4 51.1 43.1 54.3 35.7 48.2
|
299
|
+
L 31.6 50.7
|
300
|
+
C 45.5 62.1 52.6 44.6 52.6 44.6
|
301
|
+
L 55.1 48.6
|
302
|
+
L 51.7 34.2
|
303
|
+
z
|
304
|
+
|
305
|
+
M 56.9 49.9
|
306
|
+
C 49.8 49.9 49.2 57.3 49.3 60.9
|
307
|
+
L 47.6 60.9
|
308
|
+
L 47.6 73.7
|
309
|
+
L 66.1 73.7
|
310
|
+
L 66.1 60.9
|
311
|
+
L 64.4 60.9
|
312
|
+
C 64.5 57.3 63.9 49.9 56.9 49.9
|
313
|
+
z
|
314
|
+
|
315
|
+
M 56.9 53.5
|
316
|
+
C 60.8 53.5 61 58.2 60.8 60.9
|
317
|
+
L 52.9 60.9
|
318
|
+
C 52.7 58.2 52.9 53.5 56.9 53.5
|
319
|
+
z
|
320
|
+
`);
|
321
|
+
|
322
|
+
icon.setAttribute('viewBox', '10 10 70 70');
|
323
|
+
|
324
|
+
return icon;
|
325
|
+
}
|
278
326
|
|
279
327
|
public makeTextIcon(textStyle: TextStyle): IconType {
|
280
328
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
@@ -3,6 +3,7 @@
|
|
3
3
|
export interface ToolbarLocalization {
|
4
4
|
fontLabel: string;
|
5
5
|
touchPanning: string;
|
6
|
+
lockRotation: string;
|
6
7
|
outlinedRectanglePen: string;
|
7
8
|
filledRectanglePen: string;
|
8
9
|
linePen: string;
|
@@ -61,6 +62,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
61
62
|
linePen: 'Line',
|
62
63
|
outlinedRectanglePen: 'Outlined rectangle',
|
63
64
|
filledRectanglePen: 'Filled rectangle',
|
65
|
+
lockRotation: 'Lock rotation',
|
64
66
|
|
65
67
|
dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
|
66
68
|
dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
|
@@ -4,10 +4,13 @@ import PipetteTool from '../tools/PipetteTool';
|
|
4
4
|
import { EditorEventType } from '../types';
|
5
5
|
|
6
6
|
type OnColorChangeListener = (color: Color4)=>void;
|
7
|
+
type SetColorCallback = (color: Color4|string) => void;
|
7
8
|
|
9
|
+
// Returns [ color input, input container, callback to change the color value ].
|
10
|
+
export const makeColorInput = (
|
11
|
+
editor: Editor, onColorChange: OnColorChangeListener
|
12
|
+
): [ HTMLInputElement, HTMLElement, SetColorCallback ] => {
|
8
13
|
|
9
|
-
// Returns [ color input, input container ].
|
10
|
-
export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListener): [ HTMLInputElement, HTMLElement ] => {
|
11
14
|
const colorInputContainer = document.createElement('span');
|
12
15
|
const colorInput = document.createElement('input');
|
13
16
|
|
@@ -31,6 +34,9 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
|
|
31
34
|
const handleColorInput = () => {
|
32
35
|
currentColor = Color4.fromHex(colorInput.value);
|
33
36
|
};
|
37
|
+
|
38
|
+
// Only change the pen color when we finish sending input (this limits the number of
|
39
|
+
// editor events triggered and accessibility announcements).
|
34
40
|
const onInputEnd = () => {
|
35
41
|
handleColorInput();
|
36
42
|
|
@@ -61,7 +67,19 @@ export const makeColorInput = (editor: Editor, onColorChange: OnColorChangeListe
|
|
61
67
|
onInputEnd();
|
62
68
|
});
|
63
69
|
|
64
|
-
|
70
|
+
const setColorInputValue = (color: Color4|string) => {
|
71
|
+
if (typeof color === 'object') {
|
72
|
+
color = color.toHexString();
|
73
|
+
}
|
74
|
+
|
75
|
+
colorInput.value = color;
|
76
|
+
|
77
|
+
// Fire all color event listeners. See
|
78
|
+
// https://github.com/mdbassit/Coloris#manually-updating-the-thumbnail
|
79
|
+
colorInput.dispatchEvent(new Event('input', { bubbles: true }));
|
80
|
+
};
|
81
|
+
|
82
|
+
return [ colorInput, colorInputContainer, setColorInputValue ];
|
65
83
|
};
|
66
84
|
|
67
85
|
const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: OnColorChangeListener) => {
|
@@ -4,13 +4,16 @@ import BaseWidget from './BaseWidget';
|
|
4
4
|
|
5
5
|
export default class ActionButtonWidget extends BaseWidget {
|
6
6
|
public constructor(
|
7
|
-
editor: Editor,
|
7
|
+
editor: Editor,
|
8
|
+
id: string,
|
9
|
+
|
8
10
|
protected makeIcon: ()=> Element,
|
9
11
|
protected title: string,
|
10
|
-
|
11
12
|
protected clickAction: ()=>void,
|
13
|
+
|
14
|
+
localizationTable?: ToolbarLocalization,
|
12
15
|
) {
|
13
|
-
super(editor, localizationTable);
|
16
|
+
super(editor, id, localizationTable);
|
14
17
|
}
|
15
18
|
|
16
19
|
protected handleClick() {
|