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.
Files changed (65) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Color4.js +3 -0
  4. package/dist/src/Editor.d.ts +2 -0
  5. package/dist/src/Editor.js +31 -6
  6. package/dist/src/SVGLoader.js +5 -7
  7. package/dist/src/Viewport.js +2 -2
  8. package/dist/src/components/Stroke.js +2 -2
  9. package/dist/src/components/builders/LineBuilder.js +4 -0
  10. package/dist/src/components/util/StrokeSmoother.js +1 -1
  11. package/dist/src/math/Path.js +6 -1
  12. package/dist/src/rendering/renderers/SVGRenderer.js +6 -1
  13. package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
  14. package/dist/src/toolbar/HTMLToolbar.js +24 -0
  15. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  16. package/dist/src/toolbar/IconProvider.js +43 -1
  17. package/dist/src/toolbar/localization.d.ts +1 -0
  18. package/dist/src/toolbar/localization.js +1 -0
  19. package/dist/src/toolbar/makeColorInput.d.ts +2 -1
  20. package/dist/src/toolbar/makeColorInput.js +13 -2
  21. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
  22. package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
  23. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
  24. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
  25. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
  26. package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
  27. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
  28. package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
  29. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
  30. package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
  31. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
  32. package/dist/src/toolbar/widgets/PenToolWidget.js +78 -12
  33. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  34. package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
  35. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
  36. package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
  37. package/dist/src/tools/PanZoom.d.ts +4 -1
  38. package/dist/src/tools/PanZoom.js +24 -1
  39. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  40. package/package.json +1 -1
  41. package/src/Color4.ts +2 -0
  42. package/src/Editor.ts +43 -9
  43. package/src/SVGLoader.ts +8 -8
  44. package/src/Viewport.ts +2 -2
  45. package/src/components/Stroke.ts +1 -1
  46. package/src/components/builders/LineBuilder.ts +4 -0
  47. package/src/components/util/StrokeSmoother.ts +1 -1
  48. package/src/math/Path.test.ts +24 -0
  49. package/src/math/Path.ts +7 -1
  50. package/src/rendering/renderers/SVGRenderer.ts +5 -1
  51. package/src/toolbar/HTMLToolbar.ts +33 -0
  52. package/src/toolbar/IconProvider.ts +49 -1
  53. package/src/toolbar/localization.ts +2 -0
  54. package/src/toolbar/makeColorInput.ts +21 -3
  55. package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
  56. package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
  57. package/src/toolbar/widgets/BaseWidget.ts +83 -5
  58. package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
  59. package/src/toolbar/widgets/HandToolWidget.ts +48 -17
  60. package/src/toolbar/widgets/PenToolWidget.ts +105 -13
  61. package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
  62. package/src/toolbar/widgets/TextToolWidget.ts +29 -4
  63. package/src/tools/PanZoom.ts +28 -1
  64. package/src/tools/SelectionTool/Selection.ts +1 -1
  65. 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, localizationTable, tool, flag, makeIcon, title) {
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
- const mode = this.tool.getMode();
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
- this.touchPanningWidget = new HandModeWidget(editor, localizationTable, overridePanZoomTool, PanZoomMode.OneFingerTouchGestures, () => this.editor.icons.makeTouchPanningIcon(), localizationTable.touchPanning);
136
- this.addSubWidget(this.touchPanningWidget);
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: ToolbarLocalization);
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: localization.pressureSensitiveFreehandPen,
19
+ name: this.localizationTable.pressureSensitiveFreehandPen,
20
+ id: 'pressure-sensitive-pen',
19
21
  factory: makePressureSensitiveFreehandLineBuilder,
20
22
  },
21
23
  {
22
- name: localization.freehandPen,
24
+ name: this.localizationTable.freehandPen,
25
+ id: 'freehand-pen',
23
26
  factory: makeFreehandLineBuilder,
24
27
  },
25
28
  {
26
- name: localization.arrowPen,
29
+ name: this.localizationTable.arrowPen,
30
+ id: 'arrow',
27
31
  factory: makeArrowBuilder,
28
32
  },
29
33
  {
30
- name: localization.linePen,
34
+ name: this.localizationTable.linePen,
35
+ id: 'line',
31
36
  factory: makeLineBuilder,
32
37
  },
33
38
  {
34
- name: localization.filledRectanglePen,
39
+ name: this.localizationTable.filledRectanglePen,
40
+ id: 'filled-rectangle',
35
41
  factory: makeFilledRectangleBuilder,
36
42
  },
37
43
  {
38
- name: localization.outlinedRectanglePen,
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
- colorInput.value = this.tool.getColor().toHexString();
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
- if (penType.factory === this.tool.getStrokeFactory()) {
128
- objectTypeSelect.value = i.toString();
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: ToolbarLocalization);
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, localization, () => editor.icons.makeResizeViewportIcon(), this.localizationTable.resizeImageToSelection, () => {
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, localization, () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
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, localization, () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => {
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: ToolbarLocalization);
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
- colorInput.value = style.renderingStyle.fill.toHexString();
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(angle - this.lastAngle, canvasCenter));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
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
- const origTransform = importExportViewport.canvasToScreenTransform;
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([ ...supportedStyleAttrs, 'd' ]),
161
- new Set(supportedStyleAttrs)
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
- 'fill',
238
- 'transform'
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 roundAnountFactor = 2 ** roundAmount;
194
- scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
193
+ const roundAmountFactor = 2 ** roundAmount;
194
+ scaleRatio = Math.round(scaleRatio / decimalComponent * roundAmountFactor) / roundAmountFactor * decimalComponent;
195
195
 
196
196
  return scaleRatio;
197
197
  }
@@ -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 / 3));
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!');