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
@@ -11,12 +11,17 @@ import { toolbarCSSPrefix } from '../HTMLToolbar';
11
11
  import { ToolbarLocalization } from '../localization';
12
12
  import makeColorInput from '../makeColorInput';
13
13
  import BaseToolWidget from './BaseToolWidget';
14
+ import Color4 from '../../Color4';
15
+ import { SavedToolbuttonState } from './BaseWidget';
14
16
 
15
17
 
16
18
  export interface PenTypeRecord {
17
19
  // Description of the factory (e.g. 'Freehand line')
18
20
  name: string;
19
21
 
22
+ // A unique ID for the facotory (e.g. 'chisel-tip-pen')
23
+ id: string;
24
+
20
25
  // Creates an `AbstractComponent` from pen input.
21
26
  factory: ComponentBuilderFactory;
22
27
  }
@@ -26,34 +31,46 @@ export default class PenToolWidget extends BaseToolWidget {
26
31
  protected penTypes: PenTypeRecord[];
27
32
 
28
33
  public constructor(
29
- editor: Editor, private tool: Pen, localization: ToolbarLocalization
34
+ editor: Editor, private tool: Pen, localization?: ToolbarLocalization
30
35
  ) {
31
- super(editor, tool, localization);
36
+ super(editor, tool, 'pen', localization);
32
37
 
33
38
  // Default pen types
34
39
  this.penTypes = [
35
40
  {
36
- name: localization.pressureSensitiveFreehandPen,
41
+ name: this.localizationTable.pressureSensitiveFreehandPen,
42
+ id: 'pressure-sensitive-pen',
43
+
37
44
  factory: makePressureSensitiveFreehandLineBuilder,
38
45
  },
39
46
  {
40
- name: localization.freehandPen,
47
+ name: this.localizationTable.freehandPen,
48
+ id: 'freehand-pen',
49
+
41
50
  factory: makeFreehandLineBuilder,
42
51
  },
43
52
  {
44
- name: localization.arrowPen,
53
+ name: this.localizationTable.arrowPen,
54
+ id: 'arrow',
55
+
45
56
  factory: makeArrowBuilder,
46
57
  },
47
58
  {
48
- name: localization.linePen,
59
+ name: this.localizationTable.linePen,
60
+ id: 'line',
61
+
49
62
  factory: makeLineBuilder,
50
63
  },
51
64
  {
52
- name: localization.filledRectanglePen,
65
+ name: this.localizationTable.filledRectanglePen,
66
+ id: 'filled-rectangle',
67
+
53
68
  factory: makeFilledRectangleBuilder,
54
69
  },
55
70
  {
56
- name: localization.outlinedRectanglePen,
71
+ name: this.localizationTable.outlinedRectanglePen,
72
+ id: 'outlined-rectangle',
73
+
57
74
  factory: makeOutlinedRectangleBuilder,
58
75
  },
59
76
  ];
@@ -75,6 +92,30 @@ export default class PenToolWidget extends BaseToolWidget {
75
92
  return this.targetTool.description;
76
93
  }
77
94
 
95
+ // Return the index of this tool's stroke factory in the list of
96
+ // all stroke factories.
97
+ //
98
+ // Returns -1 if the stroke factory is not in the list of all stroke factories.
99
+ private getCurrentPenTypeIdx(): number {
100
+ const currentFactory = this.tool.getStrokeFactory();
101
+
102
+ for (let i = 0; i < this.penTypes.length; i ++) {
103
+ if (this.penTypes[i].factory === currentFactory) {
104
+ return i;
105
+ }
106
+ }
107
+ return -1;
108
+ }
109
+
110
+ private getCurrentPenType(): PenTypeRecord|null {
111
+ for (const penType of this.penTypes) {
112
+ if (penType.factory === this.tool.getStrokeFactory()) {
113
+ return penType;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
78
119
  protected createIcon(): Element {
79
120
  const strokeFactory = this.tool.getStrokeFactory();
80
121
  if (strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
@@ -138,7 +179,7 @@ export default class PenToolWidget extends BaseToolWidget {
138
179
 
139
180
  const colorRow = document.createElement('div');
140
181
  const colorLabel = document.createElement('label');
141
- const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
182
+ const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
142
183
  this.tool.setColor(color);
143
184
  });
144
185
 
@@ -150,9 +191,10 @@ export default class PenToolWidget extends BaseToolWidget {
150
191
  colorRow.appendChild(colorInputContainer);
151
192
 
152
193
  this.updateInputs = () => {
153
- colorInput.value = this.tool.getColor().toHexString();
194
+ setColorInputValue(this.tool.getColor());
154
195
  thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
155
196
 
197
+ // Update the list of stroke factories
156
198
  objectTypeSelect.replaceChildren();
157
199
  for (let i = 0; i < this.penTypes.length; i ++) {
158
200
  const penType = this.penTypes[i];
@@ -161,10 +203,14 @@ export default class PenToolWidget extends BaseToolWidget {
161
203
  option.innerText = penType.name;
162
204
 
163
205
  objectTypeSelect.appendChild(option);
206
+ }
164
207
 
165
- if (penType.factory === this.tool.getStrokeFactory()) {
166
- objectTypeSelect.value = i.toString();
167
- }
208
+ // Update the selected stroke factory.
209
+ const strokeFactoryIdx = this.getCurrentPenTypeIdx();
210
+ if (strokeFactoryIdx === -1) {
211
+ objectTypeSelect.value = '';
212
+ } else {
213
+ objectTypeSelect.value = strokeFactoryIdx.toString();
168
214
  }
169
215
  };
170
216
  this.updateInputs();
@@ -190,4 +236,50 @@ export default class PenToolWidget extends BaseToolWidget {
190
236
 
191
237
  return false;
192
238
  }
239
+
240
+ public serializeState(): SavedToolbuttonState {
241
+ return {
242
+ ...super.serializeState(),
243
+
244
+ color: this.tool.getColor().toHexString(),
245
+ thickness: this.tool.getThickness(),
246
+ strokeFactoryId: this.getCurrentPenType()?.id,
247
+ };
248
+ }
249
+
250
+ public deserializeFrom(state: SavedToolbuttonState) {
251
+ super.deserializeFrom(state);
252
+
253
+ const verifyPropertyType = (propertyName: string, expectedType: 'string'|'number'|'object') => {
254
+ const actualType = typeof(state[propertyName]);
255
+ if (actualType !== expectedType) {
256
+ throw new Error(
257
+ `Deserializing property ${propertyName}: Invalid type. Expected ${expectedType},` +
258
+ ` was ${actualType}.`
259
+ );
260
+ }
261
+ };
262
+
263
+ if (state.color) {
264
+ verifyPropertyType('color', 'string');
265
+ this.tool.setColor(Color4.fromHex(state.color));
266
+ }
267
+
268
+ if (state.thickness) {
269
+ verifyPropertyType('thickness', 'number');
270
+ this.tool.setThickness(state.thickness);
271
+ }
272
+
273
+ if (state.strokeFactoryId) {
274
+ verifyPropertyType('strokeFactoryId', 'string');
275
+
276
+ const factoryId: string = state.strokeFactoryId;
277
+ for (const penType of this.penTypes) {
278
+ if (factoryId === penType.id) {
279
+ this.tool.setStrokeFactory(penType.factory);
280
+ break;
281
+ }
282
+ }
283
+ }
284
+ }
193
285
  }
@@ -7,20 +7,21 @@ import BaseToolWidget from './BaseToolWidget';
7
7
 
8
8
  export default class SelectionToolWidget extends BaseToolWidget {
9
9
  public constructor(
10
- editor: Editor, private tool: SelectionTool, localization: ToolbarLocalization
10
+ editor: Editor, private tool: SelectionTool, localization?: ToolbarLocalization
11
11
  ) {
12
- super(editor, tool, localization);
12
+ super(editor, tool, 'selection-tool-widget', localization);
13
13
 
14
14
  const resizeButton = new ActionButtonWidget(
15
- editor, localization,
15
+ editor, 'resize-btn',
16
16
  () => editor.icons.makeResizeViewportIcon(),
17
17
  this.localizationTable.resizeImageToSelection,
18
18
  () => {
19
19
  this.resizeImageToSelection();
20
20
  },
21
+ localization,
21
22
  );
22
23
  const deleteButton = new ActionButtonWidget(
23
- editor, localization,
24
+ editor, 'delete-btn',
24
25
  () => editor.icons.makeDeleteSelectionIcon(),
25
26
  this.localizationTable.deleteSelection,
26
27
  () => {
@@ -28,15 +29,17 @@ export default class SelectionToolWidget extends BaseToolWidget {
28
29
  this.editor.dispatch(selection!.deleteSelectedObjects());
29
30
  this.tool.clearSelection();
30
31
  },
32
+ localization,
31
33
  );
32
34
  const duplicateButton = new ActionButtonWidget(
33
- editor, localization,
35
+ editor, 'duplicate-btn',
34
36
  () => editor.icons.makeDuplicateSelectionIcon(),
35
37
  this.localizationTable.duplicateSelection,
36
38
  () => {
37
39
  const selection = this.tool.getSelection();
38
40
  this.editor.dispatch(selection!.duplicateSelectedObjects());
39
41
  },
42
+ localization,
40
43
  );
41
44
 
42
45
  this.addSubWidget(resizeButton);
@@ -1,3 +1,4 @@
1
+ import Color4 from '../../Color4';
1
2
  import Editor from '../../Editor';
2
3
  import TextTool from '../../tools/TextTool';
3
4
  import { EditorEventType } from '../../types';
@@ -5,11 +6,12 @@ import { toolbarCSSPrefix } from '../HTMLToolbar';
5
6
  import { ToolbarLocalization } from '../localization';
6
7
  import makeColorInput from '../makeColorInput';
7
8
  import BaseToolWidget from './BaseToolWidget';
9
+ import { SavedToolbuttonState } from './BaseWidget';
8
10
 
9
11
  export default class TextToolWidget extends BaseToolWidget {
10
12
  private updateDropdownInputs: (()=>void)|null = null;
11
- public constructor(editor: Editor, private tool: TextTool, localization: ToolbarLocalization) {
12
- super(editor, tool, localization);
13
+ public constructor(editor: Editor, private tool: TextTool, localization?: ToolbarLocalization) {
14
+ super(editor, tool, 'text-tool-widget', localization);
13
15
 
14
16
  editor.notifier.on(EditorEventType.ToolUpdated, evt => {
15
17
  if (evt.kind === EditorEventType.ToolUpdated && evt.tool === tool) {
@@ -36,7 +38,7 @@ export default class TextToolWidget extends BaseToolWidget {
36
38
  const fontInput = document.createElement('select');
37
39
  const fontLabel = document.createElement('label');
38
40
 
39
- const [ colorInput, colorInputContainer ] = makeColorInput(this.editor, color => {
41
+ const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
40
42
  this.tool.setColor(color);
41
43
  });
42
44
  const colorLabel = document.createElement('label');
@@ -74,7 +76,7 @@ export default class TextToolWidget extends BaseToolWidget {
74
76
 
75
77
  this.updateDropdownInputs = () => {
76
78
  const style = this.tool.getTextStyle();
77
- colorInput.value = style.renderingStyle.fill.toHexString();
79
+ setColorInputValue(style.renderingStyle.fill);
78
80
 
79
81
  if (!fontsInInput.has(style.fontFamily)) {
80
82
  addFontToInput(style.fontFamily);
@@ -86,4 +88,27 @@ export default class TextToolWidget extends BaseToolWidget {
86
88
  dropdown.replaceChildren(colorRow, fontRow);
87
89
  return true;
88
90
  }
91
+
92
+ public serializeState(): SavedToolbuttonState {
93
+ const textStyle = this.tool.getTextStyle();
94
+
95
+ return {
96
+ ...super.serializeState(),
97
+
98
+ fontFamily: textStyle.fontFamily,
99
+ color: textStyle.renderingStyle.fill.toHexString(),
100
+ };
101
+ }
102
+
103
+ public deserializeFrom(state: SavedToolbuttonState) {
104
+ if (state.fontFamily && typeof(state.fontFamily) === 'string') {
105
+ this.tool.setFontFamily(state.fontFamily);
106
+ }
107
+
108
+ if (state.color && typeof(state.color) === 'string') {
109
+ this.tool.setColor(Color4.fromHex(state.color));
110
+ }
111
+
112
+ super.deserializeFrom(state);
113
+ }
89
114
  }
@@ -21,6 +21,8 @@ export enum PanZoomMode {
21
21
  RightClickDrags = 0x1 << 2,
22
22
  SinglePointerGestures = 0x1 << 3,
23
23
  Keyboard = 0x1 << 4,
24
+
25
+ RotationLocked = 0x1 << 5,
24
26
  }
25
27
 
26
28
  export default class PanZoom extends BaseTool {
@@ -90,10 +92,15 @@ export default class PanZoom extends BaseTool {
90
92
  const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
91
93
 
92
94
  const delta = this.getCenterDelta(screenCenter);
95
+ let rotation = angle - this.lastAngle;
96
+
97
+ if (this.isRotationLocked()) {
98
+ rotation = 0;
99
+ }
93
100
 
94
101
  const transformUpdate = Mat33.translation(delta)
95
102
  .rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
96
- .rightMul(Mat33.zRotation(angle - this.lastAngle, canvasCenter));
103
+ .rightMul(Mat33.zRotation(rotation, canvasCenter));
97
104
  this.lastScreenCenter = screenCenter;
98
105
  this.lastDist = dist;
99
106
  this.lastAngle = angle;
@@ -250,6 +257,10 @@ export default class PanZoom extends BaseTool {
250
257
  rotation += 0.0001;
251
258
  }
252
259
 
260
+ if (this.isRotationLocked()) {
261
+ rotation = 0;
262
+ }
263
+
253
264
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
254
265
 
255
266
  // Transform without translating (treat toCanvas as a linear instead of
@@ -270,6 +281,22 @@ export default class PanZoom extends BaseTool {
270
281
  return true;
271
282
  }
272
283
 
284
+ private isRotationLocked(): boolean {
285
+ return !!(this.mode & PanZoomMode.RotationLocked);
286
+ }
287
+
288
+ // Sets whether the given `mode` is enabled. `mode` should be a single
289
+ // mode from the `PanZoomMode` enum.
290
+ public setModeEnabled(mode: PanZoomMode, enabled: boolean) {
291
+ let newMode = this.mode;
292
+ if (enabled) {
293
+ newMode |= mode;
294
+ } else {
295
+ newMode &= ~mode;
296
+ }
297
+ this.setMode(newMode);
298
+ }
299
+
273
300
  public setMode(mode: PanZoomMode) {
274
301
  if (mode !== this.mode) {
275
302
  this.mode = mode;
@@ -453,7 +453,7 @@ export default class Selection {
453
453
 
454
454
  public setSelectedObjects(objects: AbstractComponent[], bbox: Rect2) {
455
455
  this.originalRegion = bbox;
456
- this.selectedElems = objects;
456
+ this.selectedElems = objects.filter(object => object.isSelectable());
457
457
  this.updateUI();
458
458
  }
459
459