js-draw 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.js +3 -0
- package/dist/src/Editor.d.ts +2 -0
- package/dist/src/Editor.js +31 -6
- package/dist/src/SVGLoader.js +5 -7
- package/dist/src/Viewport.js +2 -2
- package/dist/src/components/Stroke.js +2 -2
- package/dist/src/components/builders/LineBuilder.js +4 -0
- package/dist/src/components/util/StrokeSmoother.js +1 -1
- package/dist/src/math/Path.js +6 -1
- package/dist/src/rendering/renderers/SVGRenderer.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/dist/src/tools/SelectionTool/Selection.js +1 -1
- package/package.json +1 -1
- package/src/Color4.ts +2 -0
- package/src/Editor.ts +43 -9
- package/src/SVGLoader.ts +8 -8
- package/src/Viewport.ts +2 -2
- package/src/components/Stroke.ts +1 -1
- package/src/components/builders/LineBuilder.ts +4 -0
- package/src/components/util/StrokeSmoother.ts +1 -1
- package/src/math/Path.test.ts +24 -0
- package/src/math/Path.ts +7 -1
- package/src/rendering/renderers/SVGRenderer.ts +5 -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
- package/src/tools/SelectionTool/Selection.ts +1 -1
- package/.firebase/hosting.ZG9jcw.cache +0 -338
@@ -57,7 +57,7 @@ const makeZoomControl = (localizationTable, editor) => {
|
|
57
57
|
};
|
58
58
|
class ZoomWidget extends BaseWidget {
|
59
59
|
constructor(editor, localizationTable) {
|
60
|
-
super(editor, localizationTable);
|
60
|
+
super(editor, 'zoom-widget', localizationTable);
|
61
61
|
// Make it possible to open the dropdown, even if this widget isn't selected.
|
62
62
|
this.container.classList.add('dropdownShowable');
|
63
63
|
}
|
@@ -76,8 +76,8 @@ class ZoomWidget extends BaseWidget {
|
|
76
76
|
}
|
77
77
|
}
|
78
78
|
class HandModeWidget extends BaseWidget {
|
79
|
-
constructor(editor,
|
80
|
-
super(editor, localizationTable);
|
79
|
+
constructor(editor, tool, flag, makeIcon, title, localizationTable) {
|
80
|
+
super(editor, `pan-mode-${flag}`, localizationTable);
|
81
81
|
this.tool = tool;
|
82
82
|
this.flag = flag;
|
83
83
|
this.makeIcon = makeIcon;
|
@@ -94,13 +94,7 @@ class HandModeWidget extends BaseWidget {
|
|
94
94
|
this.setSelected(false);
|
95
95
|
}
|
96
96
|
setModeFlag(enabled) {
|
97
|
-
|
98
|
-
if (enabled) {
|
99
|
-
this.tool.setMode(mode | this.flag);
|
100
|
-
}
|
101
|
-
else {
|
102
|
-
this.tool.setMode(mode & ~this.flag);
|
103
|
-
}
|
97
|
+
this.tool.setModeEnabled(this.flag, enabled);
|
104
98
|
}
|
105
99
|
handleClick() {
|
106
100
|
this.setModeFlag(!this.isSelected());
|
@@ -122,7 +116,7 @@ export default class HandToolWidget extends BaseToolWidget {
|
|
122
116
|
overridePanZoomTool, localizationTable) {
|
123
117
|
const primaryHandTool = HandToolWidget.getPrimaryHandTool(editor.toolController);
|
124
118
|
const tool = primaryHandTool !== null && primaryHandTool !== void 0 ? primaryHandTool : overridePanZoomTool;
|
125
|
-
super(editor, tool, localizationTable);
|
119
|
+
super(editor, tool, 'hand-tool-widget', localizationTable);
|
126
120
|
this.overridePanZoomTool = overridePanZoomTool;
|
127
121
|
// Only allow toggling a hand tool if we're using the primary hand tool and not the override
|
128
122
|
// hand tool for this button.
|
@@ -132,8 +126,10 @@ export default class HandToolWidget extends BaseToolWidget {
|
|
132
126
|
this.container.classList.add('dropdownShowable');
|
133
127
|
}
|
134
128
|
// Controls for the overriding hand tool.
|
135
|
-
|
136
|
-
|
129
|
+
const touchPanningWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning, localizationTable);
|
130
|
+
const rotationLockWidget = new HandModeWidget(editor, overridePanZoomTool, PanZoomMode.RotationLocked, () => this.editor.icons.makeRotationLockIcon(), localizationTable.lockRotation, localizationTable);
|
131
|
+
this.addSubWidget(touchPanningWidget);
|
132
|
+
this.addSubWidget(rotationLockWidget);
|
137
133
|
this.addSubWidget(new ZoomWidget(editor, localizationTable));
|
138
134
|
}
|
139
135
|
static getPrimaryHandTool(toolController) {
|
@@ -160,4 +156,17 @@ export default class HandToolWidget extends BaseToolWidget {
|
|
160
156
|
super.setSelected(selected);
|
161
157
|
}
|
162
158
|
}
|
159
|
+
serializeState() {
|
160
|
+
const toolMode = this.overridePanZoomTool.getMode();
|
161
|
+
return Object.assign(Object.assign({}, super.serializeState()), { touchPanning: toolMode & PanZoomMode.OneFingerTouchGestures, rotationLocked: toolMode & PanZoomMode.RotationLocked });
|
162
|
+
}
|
163
|
+
deserializeFrom(state) {
|
164
|
+
if (state.touchPanning !== undefined) {
|
165
|
+
this.overridePanZoomTool.setModeEnabled(PanZoomMode.OneFingerTouchGestures, state.touchPanning);
|
166
|
+
}
|
167
|
+
if (state.rotationLocked !== undefined) {
|
168
|
+
this.overridePanZoomTool.setModeEnabled(PanZoomMode.RotationLocked, state.rotationLocked);
|
169
|
+
}
|
170
|
+
super.deserializeFrom(state);
|
171
|
+
}
|
163
172
|
}
|
@@ -4,18 +4,24 @@ import Pen from '../../tools/Pen';
|
|
4
4
|
import { KeyPressEvent } from '../../types';
|
5
5
|
import { ToolbarLocalization } from '../localization';
|
6
6
|
import BaseToolWidget from './BaseToolWidget';
|
7
|
+
import { SavedToolbuttonState } from './BaseWidget';
|
7
8
|
export interface PenTypeRecord {
|
8
9
|
name: string;
|
10
|
+
id: string;
|
9
11
|
factory: ComponentBuilderFactory;
|
10
12
|
}
|
11
13
|
export default class PenToolWidget extends BaseToolWidget {
|
12
14
|
private tool;
|
13
15
|
private updateInputs;
|
14
16
|
protected penTypes: PenTypeRecord[];
|
15
|
-
constructor(editor: Editor, tool: Pen, localization
|
17
|
+
constructor(editor: Editor, tool: Pen, localization?: ToolbarLocalization);
|
16
18
|
protected getTitle(): string;
|
19
|
+
private getCurrentPenTypeIdx;
|
20
|
+
private getCurrentPenType;
|
17
21
|
protected createIcon(): Element;
|
18
22
|
private static idCounter;
|
19
23
|
protected fillDropdown(dropdown: HTMLElement): boolean;
|
20
24
|
protected onKeyPress(event: KeyPressEvent): boolean;
|
25
|
+
serializeState(): SavedToolbuttonState;
|
26
|
+
deserializeFrom(state: SavedToolbuttonState): void;
|
21
27
|
}
|
@@ -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;
|
@@ -287,7 +287,7 @@ export default class Selection {
|
|
287
287
|
}
|
288
288
|
setSelectedObjects(objects, bbox) {
|
289
289
|
this.originalRegion = bbox;
|
290
|
-
this.selectedElems = objects;
|
290
|
+
this.selectedElems = objects.filter(object => object.isSelectable());
|
291
291
|
this.updateUI();
|
292
292
|
}
|
293
293
|
getSelectedObjects() {
|
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/Editor.ts
CHANGED
@@ -27,7 +27,7 @@ import EventDispatcher from './EventDispatcher';
|
|
27
27
|
import { Point2, Vec2 } from './math/Vec2';
|
28
28
|
import Vec3 from './math/Vec3';
|
29
29
|
import HTMLToolbar from './toolbar/HTMLToolbar';
|
30
|
-
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
30
|
+
import AbstractRenderer, { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
31
31
|
import Display, { RenderingMode } from './rendering/Display';
|
32
32
|
import SVGRenderer from './rendering/renderers/SVGRenderer';
|
33
33
|
import Color4 from './Color4';
|
@@ -39,6 +39,7 @@ import { EditorLocalization } from './localization';
|
|
39
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
40
40
|
import IconProvider from './toolbar/IconProvider';
|
41
41
|
import { toRoundedString } from './math/rounding';
|
42
|
+
import CanvasRenderer from './rendering/renderers/CanvasRenderer';
|
42
43
|
|
43
44
|
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
44
45
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
@@ -823,20 +824,36 @@ export class Editor {
|
|
823
824
|
});
|
824
825
|
}
|
825
826
|
|
827
|
+
// Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
|
828
|
+
// If `format` is not `image/png`, a PNG image URL may still be returned (as in the
|
829
|
+
// case of `HTMLCanvasElement::toDataURL`).
|
830
|
+
//
|
831
|
+
// The export resolution is the same as the size of the drawing canvas.
|
832
|
+
public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
|
833
|
+
const canvas = document.createElement('canvas');
|
834
|
+
|
835
|
+
const resolution = this.importExportViewport.getResolution();
|
836
|
+
|
837
|
+
canvas.width = resolution.x;
|
838
|
+
canvas.height = resolution.y;
|
839
|
+
|
840
|
+
const ctx = canvas.getContext('2d')!;
|
841
|
+
const renderer = new CanvasRenderer(ctx, this.importExportViewport);
|
842
|
+
|
843
|
+
// Render everything with no transform (0,0) should be (0,0) in the output image
|
844
|
+
this.renderAllWithTransform(renderer, this.importExportViewport, Mat33.identity);
|
845
|
+
|
846
|
+
const dataURL = canvas.toDataURL(format);
|
847
|
+
return dataURL;
|
848
|
+
}
|
849
|
+
|
826
850
|
public toSVG(): SVGElement {
|
827
851
|
const importExportViewport = this.importExportViewport;
|
828
852
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
829
853
|
const result = document.createElementNS(svgNameSpace, 'svg');
|
830
854
|
const renderer = new SVGRenderer(result, importExportViewport);
|
831
855
|
|
832
|
-
|
833
|
-
// Reset the transform to ensure that (0, 0) is (0, 0)
|
834
|
-
importExportViewport.resetTransform(Mat33.identity);
|
835
|
-
|
836
|
-
// Render **all** elements.
|
837
|
-
this.image.renderAll(renderer);
|
838
|
-
|
839
|
-
importExportViewport.resetTransform(origTransform);
|
856
|
+
this.renderAllWithTransform(renderer, importExportViewport);
|
840
857
|
|
841
858
|
// Just show the main region
|
842
859
|
const rect = importExportViewport.visibleRect;
|
@@ -854,6 +871,23 @@ export class Editor {
|
|
854
871
|
return result;
|
855
872
|
}
|
856
873
|
|
874
|
+
// Renders everything in this' image to `renderer`, but first transforming the given `viewport`
|
875
|
+
// such that its transform is `transform`. The given `viewport`'s transform is restored before this method
|
876
|
+
// returns.
|
877
|
+
//
|
878
|
+
// For example, rendering with `transform = Mat33.identity` *sets* `viewport`'s transform to `Mat33.identity`,
|
879
|
+
// renders everything in this' image to `renderer`, then restores `viewport`'s transform to whatever it was before.
|
880
|
+
private renderAllWithTransform(
|
881
|
+
renderer: AbstractRenderer, viewport: Viewport, transform: Mat33 = Mat33.identity
|
882
|
+
): void {
|
883
|
+
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
884
|
+
viewport.resetTransform(transform);
|
885
|
+
|
886
|
+
this.image.renderAll(renderer);
|
887
|
+
|
888
|
+
viewport.resetTransform(origTransform);
|
889
|
+
}
|
890
|
+
|
857
891
|
public async loadFrom(loader: ImageLoader) {
|
858
892
|
this.showLoadingWarning(0);
|
859
893
|
this.display.setDraftMode(true);
|
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/Viewport.ts
CHANGED
@@ -190,8 +190,8 @@ export class Viewport {
|
|
190
190
|
|
191
191
|
// Represent as k 10ⁿ for some n, k ∈ ℤ.
|
192
192
|
const decimalComponent = 10 ** Math.floor(Math.log10(Math.abs(scaleRatio)));
|
193
|
-
const
|
194
|
-
scaleRatio = Math.round(scaleRatio / decimalComponent *
|
193
|
+
const roundAmountFactor = 2 ** roundAmount;
|
194
|
+
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAmountFactor) / roundAmountFactor * decimalComponent;
|
195
195
|
|
196
196
|
return scaleRatio;
|
197
197
|
}
|
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
|
}
|
@@ -51,6 +51,10 @@ export default class LineBuilder implements ComponentBuilder {
|
|
51
51
|
kind: PathCommandType.LineTo,
|
52
52
|
point: endPoint.minus(scaledEndNormal),
|
53
53
|
},
|
54
|
+
{
|
55
|
+
kind: PathCommandType.LineTo,
|
56
|
+
point: startPoint.minus(scaledStartNormal),
|
57
|
+
},
|
54
58
|
],
|
55
59
|
style: {
|
56
60
|
fill: this.startPoint.color,
|
@@ -238,7 +238,7 @@ export class StrokeSmoother {
|
|
238
238
|
if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
239
239
|
// Position the control point closer to the first -- the connecting
|
240
240
|
// segment will be roughly a line.
|
241
|
-
controlPoint = segmentStart.plus(enteringVec.times(startEndDist /
|
241
|
+
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
|
242
242
|
}
|
243
243
|
|
244
244
|
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|