js-draw 0.7.2 → 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.
Files changed (82) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +1 -0
  2. package/CHANGELOG.md +12 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.js +3 -0
  5. package/dist/src/SVGLoader.js +5 -7
  6. package/dist/src/components/Stroke.js +10 -3
  7. package/dist/src/components/builders/FreehandLineBuilder.d.ts +10 -23
  8. package/dist/src/components/builders/FreehandLineBuilder.js +70 -396
  9. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +36 -0
  10. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +339 -0
  11. package/dist/src/components/lib.d.ts +2 -0
  12. package/dist/src/components/lib.js +2 -0
  13. package/dist/src/components/util/StrokeSmoother.d.ts +35 -0
  14. package/dist/src/components/util/StrokeSmoother.js +206 -0
  15. package/dist/src/math/Mat33.d.ts +2 -0
  16. package/dist/src/math/Mat33.js +4 -0
  17. package/dist/src/math/Path.d.ts +2 -0
  18. package/dist/src/math/Path.js +44 -0
  19. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -0
  20. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
  21. package/dist/src/rendering/renderers/SVGRenderer.js +20 -0
  22. package/dist/src/toolbar/HTMLToolbar.d.ts +3 -0
  23. package/dist/src/toolbar/HTMLToolbar.js +24 -0
  24. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  25. package/dist/src/toolbar/IconProvider.js +43 -1
  26. package/dist/src/toolbar/localization.d.ts +2 -0
  27. package/dist/src/toolbar/localization.js +2 -0
  28. package/dist/src/toolbar/makeColorInput.d.ts +2 -1
  29. package/dist/src/toolbar/makeColorInput.js +13 -2
  30. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +1 -1
  31. package/dist/src/toolbar/widgets/ActionButtonWidget.js +2 -2
  32. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -2
  33. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -3
  34. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -2
  35. package/dist/src/toolbar/widgets/BaseWidget.js +67 -6
  36. package/dist/src/toolbar/widgets/EraserToolWidget.d.ts +4 -0
  37. package/dist/src/toolbar/widgets/EraserToolWidget.js +3 -0
  38. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +3 -1
  39. package/dist/src/toolbar/widgets/HandToolWidget.js +22 -13
  40. package/dist/src/toolbar/widgets/PenToolWidget.d.ts +7 -1
  41. package/dist/src/toolbar/widgets/PenToolWidget.js +83 -12
  42. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  43. package/dist/src/toolbar/widgets/SelectionToolWidget.js +7 -7
  44. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +4 -1
  45. package/dist/src/toolbar/widgets/TextToolWidget.js +17 -3
  46. package/dist/src/tools/PanZoom.d.ts +4 -1
  47. package/dist/src/tools/PanZoom.js +24 -1
  48. package/dist/src/tools/Pen.d.ts +2 -2
  49. package/dist/src/tools/Pen.js +2 -2
  50. package/dist/src/tools/SelectionTool/Selection.d.ts +1 -0
  51. package/dist/src/tools/SelectionTool/Selection.js +8 -1
  52. package/dist/src/tools/ToolController.js +2 -1
  53. package/package.json +1 -1
  54. package/src/Color4.ts +2 -0
  55. package/src/SVGLoader.ts +8 -8
  56. package/src/components/Stroke.ts +16 -3
  57. package/src/components/builders/FreehandLineBuilder.ts +54 -495
  58. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +454 -0
  59. package/src/components/lib.ts +3 -1
  60. package/src/components/util/StrokeSmoother.ts +290 -0
  61. package/src/math/Mat33.ts +5 -0
  62. package/src/math/Path.test.ts +49 -0
  63. package/src/math/Path.ts +51 -0
  64. package/src/rendering/renderers/CanvasRenderer.ts +2 -0
  65. package/src/rendering/renderers/SVGRenderer.ts +24 -0
  66. package/src/toolbar/HTMLToolbar.ts +33 -0
  67. package/src/toolbar/IconProvider.ts +49 -1
  68. package/src/toolbar/localization.ts +4 -0
  69. package/src/toolbar/makeColorInput.ts +21 -3
  70. package/src/toolbar/widgets/ActionButtonWidget.ts +6 -3
  71. package/src/toolbar/widgets/BaseToolWidget.ts +4 -3
  72. package/src/toolbar/widgets/BaseWidget.ts +83 -5
  73. package/src/toolbar/widgets/EraserToolWidget.ts +11 -0
  74. package/src/toolbar/widgets/HandToolWidget.ts +48 -17
  75. package/src/toolbar/widgets/PenToolWidget.ts +110 -13
  76. package/src/toolbar/widgets/SelectionToolWidget.ts +8 -5
  77. package/src/toolbar/widgets/TextToolWidget.ts +29 -4
  78. package/src/tools/PanZoom.ts +28 -1
  79. package/src/tools/Pen.test.ts +2 -2
  80. package/src/tools/Pen.ts +1 -1
  81. package/src/tools/SelectionTool/Selection.ts +10 -1
  82. package/src/tools/ToolController.ts +2 -1
@@ -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
  }
@@ -1,36 +1,48 @@
1
1
  import { makeArrowBuilder } from '../../components/builders/ArrowBuilder';
2
2
  import { makeFreehandLineBuilder } from '../../components/builders/FreehandLineBuilder';
3
+ import { makePressureSensitiveFreehandLineBuilder } from '../../components/builders/PressureSensitiveFreehandLineBuilder';
3
4
  import { makeLineBuilder } from '../../components/builders/LineBuilder';
4
5
  import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../components/builders/RectangleBuilder';
5
6
  import { EditorEventType } from '../../types';
6
7
  import { toolbarCSSPrefix } from '../HTMLToolbar';
7
8
  import makeColorInput from '../makeColorInput';
8
9
  import BaseToolWidget from './BaseToolWidget';
10
+ import Color4 from '../../Color4';
9
11
  export default class PenToolWidget extends BaseToolWidget {
10
12
  constructor(editor, tool, localization) {
11
- super(editor, tool, localization);
13
+ super(editor, tool, 'pen', localization);
12
14
  this.tool = tool;
13
15
  this.updateInputs = () => { };
14
16
  // Default pen types
15
17
  this.penTypes = [
16
18
  {
17
- name: localization.freehandPen,
19
+ name: this.localizationTable.pressureSensitiveFreehandPen,
20
+ id: 'pressure-sensitive-pen',
21
+ factory: makePressureSensitiveFreehandLineBuilder,
22
+ },
23
+ {
24
+ name: this.localizationTable.freehandPen,
25
+ id: 'freehand-pen',
18
26
  factory: makeFreehandLineBuilder,
19
27
  },
20
28
  {
21
- name: localization.arrowPen,
29
+ name: this.localizationTable.arrowPen,
30
+ id: 'arrow',
22
31
  factory: makeArrowBuilder,
23
32
  },
24
33
  {
25
- name: localization.linePen,
34
+ name: this.localizationTable.linePen,
35
+ id: 'line',
26
36
  factory: makeLineBuilder,
27
37
  },
28
38
  {
29
- name: localization.filledRectanglePen,
39
+ name: this.localizationTable.filledRectanglePen,
40
+ id: 'filled-rectangle',
30
41
  factory: makeFilledRectangleBuilder,
31
42
  },
32
43
  {
33
- name: localization.outlinedRectanglePen,
44
+ name: this.localizationTable.outlinedRectanglePen,
45
+ id: 'outlined-rectangle',
34
46
  factory: makeOutlinedRectangleBuilder,
35
47
  },
36
48
  ];
@@ -48,9 +60,30 @@ export default class PenToolWidget extends BaseToolWidget {
48
60
  getTitle() {
49
61
  return this.targetTool.description;
50
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
+ }
51
84
  createIcon() {
52
85
  const strokeFactory = this.tool.getStrokeFactory();
53
- if (strokeFactory === makeFreehandLineBuilder) {
86
+ if (strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
54
87
  // Use a square-root scale to prevent the pen's tip from overflowing.
55
88
  const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
56
89
  const color = this.tool.getColor();
@@ -101,7 +134,7 @@ export default class PenToolWidget extends BaseToolWidget {
101
134
  objectTypeRow.appendChild(objectTypeSelect);
102
135
  const colorRow = document.createElement('div');
103
136
  const colorLabel = document.createElement('label');
104
- const [colorInput, colorInputContainer] = makeColorInput(this.editor, color => {
137
+ const [colorInput, colorInputContainer, setColorInputValue] = makeColorInput(this.editor, color => {
105
138
  this.tool.setColor(color);
106
139
  });
107
140
  colorInput.id = `${toolbarCSSPrefix}colorInput${PenToolWidget.idCounter++}`;
@@ -110,8 +143,9 @@ export default class PenToolWidget extends BaseToolWidget {
110
143
  colorRow.appendChild(colorLabel);
111
144
  colorRow.appendChild(colorInputContainer);
112
145
  this.updateInputs = () => {
113
- colorInput.value = this.tool.getColor().toHexString();
146
+ setColorInputValue(this.tool.getColor());
114
147
  thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
148
+ // Update the list of stroke factories
115
149
  objectTypeSelect.replaceChildren();
116
150
  for (let i = 0; i < this.penTypes.length; i++) {
117
151
  const penType = this.penTypes[i];
@@ -119,9 +153,14 @@ export default class PenToolWidget extends BaseToolWidget {
119
153
  option.value = i.toString();
120
154
  option.innerText = penType.name;
121
155
  objectTypeSelect.appendChild(option);
122
- if (penType.factory === this.tool.getStrokeFactory()) {
123
- objectTypeSelect.value = i.toString();
124
- }
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();
125
164
  }
126
165
  };
127
166
  this.updateInputs();
@@ -143,5 +182,37 @@ export default class PenToolWidget extends BaseToolWidget {
143
182
  }
144
183
  return false;
145
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
+ }
146
217
  }
147
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;
@@ -11,10 +11,10 @@ export interface PenStyle {
11
11
  export default class Pen extends BaseTool {
12
12
  private editor;
13
13
  private style;
14
+ private builderFactory;
14
15
  protected builder: ComponentBuilder | null;
15
- protected builderFactory: ComponentBuilderFactory;
16
16
  private lastPoint;
17
- constructor(editor: Editor, description: string, style: PenStyle);
17
+ constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
18
18
  private getPressureMultiplier;
19
19
  protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
20
20
  protected previewStroke(): void;
@@ -4,12 +4,12 @@ import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuil
4
4
  import { EditorEventType } from '../types';
5
5
  import BaseTool from './BaseTool';
6
6
  export default class Pen extends BaseTool {
7
- constructor(editor, description, style) {
7
+ constructor(editor, description, style, builderFactory = makeFreehandLineBuilder) {
8
8
  super(editor.notifier, description);
9
9
  this.editor = editor;
10
10
  this.style = style;
11
+ this.builderFactory = builderFactory;
11
12
  this.builder = null;
12
- this.builderFactory = makeFreehandLineBuilder;
13
13
  this.lastPoint = null;
14
14
  }
15
15
  getPressureMultiplier() {
@@ -18,6 +18,7 @@ export default class Selection {
18
18
  private selectedElems;
19
19
  private container;
20
20
  private backgroundElem;
21
+ private hasParent;
21
22
  constructor(startPoint: Point2, editor: Editor);
22
23
  getTransform(): Mat33;
23
24
  get preTransformRegion(): Rect2;
@@ -30,6 +30,7 @@ export default class Selection {
30
30
  this.transform = Mat33.identity;
31
31
  this.transformCommands = [];
32
32
  this.selectedElems = [];
33
+ this.hasParent = true;
33
34
  this.targetHandle = null;
34
35
  this.backgroundDragging = false;
35
36
  this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
@@ -96,7 +97,7 @@ export default class Selection {
96
97
  // Applies, previews, but doesn't finalize the given transformation.
97
98
  setTransform(transform, preview = true) {
98
99
  this.transform = transform;
99
- if (preview) {
100
+ if (preview && this.hasParent) {
100
101
  this.previewTransformCmds();
101
102
  this.scrollTo();
102
103
  }
@@ -190,6 +191,10 @@ export default class Selection {
190
191
  }
191
192
  // @internal
192
193
  updateUI() {
194
+ // Don't update old selections.
195
+ if (!this.hasParent) {
196
+ return;
197
+ }
193
198
  // marginLeft, marginTop: Display relative to the top left of the selection overlay.
194
199
  // left, top don't work for this.
195
200
  this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
@@ -267,6 +272,7 @@ export default class Selection {
267
272
  this.container.remove();
268
273
  }
269
274
  elem.appendChild(this.container);
275
+ this.hasParent = true;
270
276
  }
271
277
  setToPoint(point) {
272
278
  this.originalRegion = this.originalRegion.grownToPoint(point);
@@ -277,6 +283,7 @@ export default class Selection {
277
283
  this.container.remove();
278
284
  }
279
285
  this.originalRegion = Rect2.empty;
286
+ this.hasParent = false;
280
287
  }
281
288
  setSelectedObjects(objects, bbox) {
282
289
  this.originalRegion = bbox;
@@ -11,6 +11,7 @@ import PipetteTool from './PipetteTool';
11
11
  import ToolSwitcherShortcut from './ToolSwitcherShortcut';
12
12
  import PasteHandler from './PasteHandler';
13
13
  import ToolbarShortcutHandler from './ToolbarShortcutHandler';
14
+ import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
14
15
  export default class ToolController {
15
16
  /** @internal */
16
17
  constructor(editor, localization) {
@@ -25,7 +26,7 @@ export default class ToolController {
25
26
  primaryPenTool,
26
27
  new Pen(editor, localization.penTool(2), { color: Color4.clay, thickness: 4 }),
27
28
  // Highlighter-like pen with width=64
28
- new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
29
+ new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }, makePressureSensitiveFreehandLineBuilder),
29
30
  new Eraser(editor, localization.eraserTool),
30
31
  new SelectionTool(editor, localization.selectionTool),
31
32
  new TextTool(editor, localization.textTool, localization),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
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/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[] = [];
@@ -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.closedRoughlyIntersects(visibleRect)) {
64
+ if (muchBiggerThanVisible && !part.path.roughlyIntersects(visibleRect, part.style.stroke?.width ?? 0)) {
65
65
  continue;
66
66
  }
67
67
  }
@@ -87,7 +87,20 @@ export default class Stroke extends AbstractComponent {
87
87
  // Update each part
88
88
  this.parts = this.parts.map((part) => {
89
89
  const newPath = part.path.transformedBy(affineTransfm);
90
- const newBBox = this.bboxForPart(newPath.bbox, part.style);
90
+ const newStyle = {
91
+ ...part.style,
92
+ stroke: part.style.stroke ? {
93
+ ...part.style.stroke,
94
+ } : undefined,
95
+ };
96
+
97
+ // Approximate the scale factor.
98
+ if (newStyle.stroke) {
99
+ const scaleFactor = affineTransfm.getScaleFactor();
100
+ newStyle.stroke.width *= scaleFactor;
101
+ }
102
+
103
+ const newBBox = this.bboxForPart(newPath.bbox, newStyle);
91
104
 
92
105
  if (isFirstPart) {
93
106
  this.contentBBox = newBBox;
@@ -100,7 +113,7 @@ export default class Stroke extends AbstractComponent {
100
113
  path: newPath,
101
114
  startPoint: newPath.startPoint,
102
115
  commands: newPath.parts,
103
- style: part.style,
116
+ style: newStyle,
104
117
  };
105
118
  });
106
119
  }