js-draw 0.22.1 → 0.23.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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/src/Pointer.d.ts +3 -1
  5. package/dist/cjs/src/Pointer.js +27 -2
  6. package/dist/cjs/src/commands/Duplicate.d.ts +24 -0
  7. package/dist/cjs/src/commands/Duplicate.js +26 -0
  8. package/dist/cjs/src/commands/Erase.d.ts +20 -0
  9. package/dist/cjs/src/commands/Erase.js +20 -0
  10. package/dist/cjs/src/commands/invertCommand.js +6 -0
  11. package/dist/cjs/src/commands/uniteCommands.js +9 -13
  12. package/dist/cjs/src/components/BackgroundComponent.js +9 -2
  13. package/dist/cjs/src/components/lib.d.ts +1 -1
  14. package/dist/cjs/src/components/lib.js +2 -2
  15. package/dist/cjs/src/components/localization.d.ts +1 -0
  16. package/dist/cjs/src/components/localization.js +1 -0
  17. package/dist/cjs/src/math/Vec2.d.ts +20 -0
  18. package/dist/cjs/src/math/Vec2.js +20 -0
  19. package/dist/cjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
  20. package/dist/cjs/src/rendering/renderers/AbstractRenderer.js +21 -3
  21. package/dist/cjs/src/rendering/renderers/CanvasRenderer.js +12 -7
  22. package/dist/cjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
  23. package/dist/cjs/src/toolbar/widgets/BaseWidget.js +22 -4
  24. package/dist/cjs/src/tools/Pen.d.ts +4 -0
  25. package/dist/cjs/src/tools/Pen.js +24 -1
  26. package/dist/cjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
  27. package/dist/cjs/src/tools/SelectionTool/SelectionTool.js +8 -0
  28. package/dist/cjs/src/util/waitForAll.d.ts +6 -0
  29. package/dist/cjs/src/util/waitForAll.js +17 -0
  30. package/dist/mjs/src/Pointer.d.ts +3 -1
  31. package/dist/mjs/src/Pointer.mjs +27 -2
  32. package/dist/mjs/src/commands/Duplicate.d.ts +24 -0
  33. package/dist/mjs/src/commands/Duplicate.mjs +26 -0
  34. package/dist/mjs/src/commands/Erase.d.ts +20 -0
  35. package/dist/mjs/src/commands/Erase.mjs +20 -0
  36. package/dist/mjs/src/commands/invertCommand.mjs +6 -0
  37. package/dist/mjs/src/commands/uniteCommands.mjs +9 -13
  38. package/dist/mjs/src/components/BackgroundComponent.mjs +9 -2
  39. package/dist/mjs/src/components/lib.d.ts +1 -1
  40. package/dist/mjs/src/components/lib.mjs +3 -1
  41. package/dist/mjs/src/components/localization.d.ts +1 -0
  42. package/dist/mjs/src/components/localization.mjs +1 -0
  43. package/dist/mjs/src/math/Vec2.d.ts +20 -0
  44. package/dist/mjs/src/math/Vec2.mjs +20 -0
  45. package/dist/mjs/src/rendering/renderers/AbstractRenderer.d.ts +17 -0
  46. package/dist/mjs/src/rendering/renderers/AbstractRenderer.mjs +21 -3
  47. package/dist/mjs/src/rendering/renderers/CanvasRenderer.mjs +12 -7
  48. package/dist/mjs/src/toolbar/widgets/BaseWidget.d.ts +1 -0
  49. package/dist/mjs/src/toolbar/widgets/BaseWidget.mjs +22 -4
  50. package/dist/mjs/src/tools/Pen.d.ts +4 -0
  51. package/dist/mjs/src/tools/Pen.mjs +24 -1
  52. package/dist/mjs/src/tools/SelectionTool/SelectionTool.d.ts +1 -0
  53. package/dist/mjs/src/tools/SelectionTool/SelectionTool.mjs +8 -0
  54. package/dist/mjs/src/util/waitForAll.d.ts +6 -0
  55. package/dist/mjs/src/util/waitForAll.mjs +15 -0
  56. package/package.json +9 -9
  57. package/src/toolbar/toolbar.css +35 -1
@@ -19,6 +19,11 @@ export interface RenderableImage {
19
19
  base64Url: string;
20
20
  label?: string;
21
21
  }
22
+ /**
23
+ * Abstract base class for renderers.
24
+ *
25
+ * @see {@link EditorImage.render}
26
+ */
22
27
  export default abstract class AbstractRenderer {
23
28
  private viewport;
24
29
  private selfTransform;
@@ -40,9 +45,21 @@ export default abstract class AbstractRenderer {
40
45
  protected objectLevel: number;
41
46
  private currentPaths;
42
47
  private flushPath;
48
+ /**
49
+ * Draws a styled path. If within an object started by {@link startObject},
50
+ * the resultant path may not be visible until {@link endObject} is called.
51
+ */
43
52
  drawPath(path: RenderablePathSpec): void;
44
53
  drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
54
+ /** Draws a filled rectangle. */
45
55
  fillRect(rect: Rect2, fill: Color4): void;
56
+ /**
57
+ * This should be called whenever a new object is being drawn.
58
+ *
59
+ * @param _boundingBox The bounding box of the object to be drawn.
60
+ * @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
61
+ * that override this method are not required to support `_clip`.
62
+ */
46
63
  startObject(_boundingBox: Rect2, _clip?: boolean): void;
47
64
  /**
48
65
  * Notes the end of an object.
@@ -1,6 +1,11 @@
1
1
  import Path, { PathCommandType } from '../../math/Path.mjs';
2
2
  import { Vec2 } from '../../math/Vec2.mjs';
3
3
  import { stylesEqual } from '../RenderingStyle.mjs';
4
+ /**
5
+ * Abstract base class for renderers.
6
+ *
7
+ * @see {@link EditorImage.render}
8
+ */
4
9
  export default class AbstractRenderer {
5
10
  constructor(viewport) {
6
11
  this.viewport = viewport;
@@ -49,7 +54,12 @@ export default class AbstractRenderer {
49
54
  if (lastStyle) {
50
55
  this.endPath(lastStyle);
51
56
  }
57
+ this.currentPaths = [];
52
58
  }
59
+ /**
60
+ * Draws a styled path. If within an object started by {@link startObject},
61
+ * the resultant path may not be visible until {@link endObject} is called.
62
+ */
53
63
  drawPath(path) {
54
64
  // If we're being called outside of an object,
55
65
  // we can't delay rendering
@@ -70,14 +80,22 @@ export default class AbstractRenderer {
70
80
  const path = Path.fromRect(rect, lineWidth);
71
81
  this.drawPath(path.toRenderable(lineFill));
72
82
  }
73
- // Fills a rectangle.
83
+ /** Draws a filled rectangle. */
74
84
  fillRect(rect, fill) {
75
85
  const path = Path.fromRect(rect);
76
86
  this.drawPath(path.toRenderable({ fill }));
77
87
  }
78
- // Note the start of an object with the given bounding box.
79
- // Renderers are not required to support [clip]
88
+ /**
89
+ * This should be called whenever a new object is being drawn.
90
+ *
91
+ * @param _boundingBox The bounding box of the object to be drawn.
92
+ * @param _clip Whether content outside `_boundingBox` should be drawn. Renderers
93
+ * that override this method are not required to support `_clip`.
94
+ */
80
95
  startObject(_boundingBox, _clip) {
96
+ if (this.objectLevel > 0) {
97
+ this.flushPath();
98
+ }
81
99
  this.currentPaths = [];
82
100
  this.objectLevel++;
83
101
  }
@@ -175,14 +175,19 @@ export default class CanvasRenderer extends AbstractRenderer {
175
175
  super.startObject(boundingBox);
176
176
  this.currentObjectBBox = boundingBox;
177
177
  if (!this.ignoringObject && clip) {
178
- this.clipLevels.push(this.objectLevel);
179
- this.ctx.save();
180
- this.ctx.beginPath();
181
- for (const corner of boundingBox.corners) {
182
- const screenCorner = this.canvasToScreen(corner);
183
- this.ctx.lineTo(screenCorner.x, screenCorner.y);
178
+ // Don't clip if it would only remove content already trimmed by
179
+ // the edge of the screen.
180
+ const clippedIsOutsideScreen = boundingBox.containsRect(this.getViewport().visibleRect);
181
+ if (!clippedIsOutsideScreen) {
182
+ this.clipLevels.push(this.objectLevel);
183
+ this.ctx.save();
184
+ this.ctx.beginPath();
185
+ for (const corner of boundingBox.corners) {
186
+ const screenCorner = this.canvasToScreen(corner);
187
+ this.ctx.lineTo(screenCorner.x, screenCorner.y);
188
+ }
189
+ this.ctx.clip();
184
190
  }
185
- this.ctx.clip();
186
191
  }
187
192
  }
188
193
  endObject() {
@@ -46,6 +46,7 @@ export default abstract class BaseWidget {
46
46
  protected updateIcon(): void;
47
47
  setDisabled(disabled: boolean): void;
48
48
  setSelected(selected: boolean): void;
49
+ private hideDropdownTimeout;
49
50
  protected setDropdownVisible(visible: boolean): void;
50
51
  canBeInOverflowMenu(): boolean;
51
52
  getButtonWidth(): number;
@@ -23,6 +23,7 @@ export default class BaseWidget {
23
23
  this.subWidgets = {};
24
24
  this.toplevel = true;
25
25
  this.toolbarWidgetToggleListener = null;
26
+ this.hideDropdownTimeout = null;
26
27
  this.localizationTable = localizationTable !== null && localizationTable !== void 0 ? localizationTable : editor.localization;
27
28
  this.icon = null;
28
29
  this.container = document.createElement('div');
@@ -230,6 +231,13 @@ export default class BaseWidget {
230
231
  if (currentlyVisible === visible) {
231
232
  return;
232
233
  }
234
+ // If waiting to hide the dropdown, cancel it.
235
+ if (this.hideDropdownTimeout) {
236
+ clearTimeout(this.hideDropdownTimeout);
237
+ this.hideDropdownTimeout = null;
238
+ this.dropdownContainer.classList.remove('hiding');
239
+ }
240
+ const animationDuration = 150; // ms
233
241
  if (visible) {
234
242
  this.dropdownContainer.classList.remove('hidden');
235
243
  this.container.classList.add('dropdownVisible');
@@ -240,10 +248,20 @@ export default class BaseWidget {
240
248
  });
241
249
  }
242
250
  else {
243
- this.dropdownContainer.classList.add('hidden');
244
251
  this.container.classList.remove('dropdownVisible');
245
252
  this.editor.announceForAccessibility(this.localizationTable.dropdownHidden(this.getTitle()));
253
+ this.dropdownContainer.classList.add('hiding');
254
+ // Hide the dropdown *slightly* before the animation finishes. This
255
+ // prevents flickering in some browsers.
256
+ const hideDelay = animationDuration * 0.95;
257
+ this.hideDropdownTimeout = setTimeout(() => {
258
+ this.dropdownContainer.classList.add('hidden');
259
+ this.dropdownContainer.classList.remove('hiding');
260
+ }, hideDelay);
246
261
  }
262
+ // Animate
263
+ const animationName = `var(--dropdown-${visible ? 'show' : 'hide'}-animation)`;
264
+ this.dropdownContainer.style.animation = `${animationDuration}ms ease ${animationName}`;
247
265
  this.repositionDropdown();
248
266
  }
249
267
  canBeInOverflowMenu() {
@@ -262,11 +280,11 @@ export default class BaseWidget {
262
280
  const dropdownBBox = this.dropdownContainer.getBoundingClientRect();
263
281
  const screenWidth = document.body.clientWidth;
264
282
  if (dropdownBBox.left > screenWidth / 2) {
265
- this.dropdownContainer.style.marginLeft = this.button.clientWidth + 'px';
266
- this.dropdownContainer.style.transform = 'translate(-100%, 0)';
283
+ // Use .translate so as not to conflict with CSS animating the
284
+ // transform property.
285
+ this.dropdownContainer.style.translate = `calc(${this.button.clientWidth + 'px'} - 100%) 0`;
267
286
  }
268
287
  else {
269
- this.dropdownContainer.style.marginLeft = '';
270
288
  this.dropdownContainer.style.transform = '';
271
289
  }
272
290
  }
@@ -14,9 +14,12 @@ export default class Pen extends BaseTool {
14
14
  private builderFactory;
15
15
  protected builder: ComponentBuilder | null;
16
16
  private lastPoint;
17
+ private startPoint;
17
18
  private ctrlKeyPressed;
19
+ private shiftKeyPressed;
18
20
  constructor(editor: Editor, description: string, style: PenStyle, builderFactory?: ComponentBuilderFactory);
19
21
  private getPressureMultiplier;
22
+ private xyAxesSnap;
20
23
  protected toStrokePoint(pointer: Pointer): StrokeDataPoint;
21
24
  protected previewStroke(): void;
22
25
  protected addPointToStroke(point: StrokeDataPoint): void;
@@ -34,6 +37,7 @@ export default class Pen extends BaseTool {
34
37
  getStrokeFactory(): ComponentBuilderFactory;
35
38
  setEnabled(enabled: boolean): void;
36
39
  private isSnappingToGrid;
40
+ private isAngleLocked;
37
41
  onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
38
42
  onKeyUp({ key }: KeyUpEvent): boolean;
39
43
  }
@@ -11,14 +11,26 @@ export default class Pen extends BaseTool {
11
11
  this.builderFactory = builderFactory;
12
12
  this.builder = null;
13
13
  this.lastPoint = null;
14
+ this.startPoint = null;
14
15
  this.ctrlKeyPressed = false;
16
+ this.shiftKeyPressed = false;
15
17
  }
16
18
  getPressureMultiplier() {
17
19
  return 1 / this.editor.viewport.getScaleFactor() * this.style.thickness;
18
20
  }
21
+ // Snap the given pointer to the nearer of the x/y axes.
22
+ xyAxesSnap(pointer) {
23
+ if (!this.startPoint) {
24
+ return pointer;
25
+ }
26
+ return pointer.lockedToXYAxes(this.startPoint.pos, this.editor.viewport);
27
+ }
19
28
  // Converts a `pointer` to a `StrokeDataPoint`.
20
29
  toStrokePoint(pointer) {
21
30
  var _a;
31
+ if (this.isAngleLocked() && this.lastPoint) {
32
+ pointer = this.xyAxesSnap(pointer);
33
+ }
22
34
  if (this.isSnappingToGrid()) {
23
35
  pointer = pointer.snappedToGrid(this.editor.viewport);
24
36
  }
@@ -64,7 +76,8 @@ export default class Pen extends BaseTool {
64
76
  }
65
77
  }
66
78
  if ((allPointers.length === 1 && !isEraser) || anyDeviceIsStylus) {
67
- this.builder = this.builderFactory(this.toStrokePoint(current), this.editor.viewport);
79
+ this.startPoint = this.toStrokePoint(current);
80
+ this.builder = this.builderFactory(this.startPoint, this.editor.viewport);
68
81
  return true;
69
82
  }
70
83
  return false;
@@ -104,6 +117,7 @@ export default class Pen extends BaseTool {
104
117
  }
105
118
  }
106
119
  this.builder = null;
120
+ this.lastPoint = null;
107
121
  this.editor.clearWetInk();
108
122
  }
109
123
  noteUpdated() {
@@ -138,6 +152,7 @@ export default class Pen extends BaseTool {
138
152
  this.ctrlKeyPressed = false;
139
153
  }
140
154
  isSnappingToGrid() { return this.ctrlKeyPressed; }
155
+ isAngleLocked() { return this.shiftKeyPressed; }
141
156
  onKeyPress({ key, ctrlKey }) {
142
157
  key = key.toLowerCase();
143
158
  let newThickness;
@@ -156,6 +171,10 @@ export default class Pen extends BaseTool {
156
171
  this.ctrlKeyPressed = true;
157
172
  return true;
158
173
  }
174
+ if (key === 'shift') {
175
+ this.shiftKeyPressed = true;
176
+ return true;
177
+ }
159
178
  // Ctrl+Z: End the stroke so that it can be undone/redone.
160
179
  if (key === 'z' && ctrlKey && this.builder) {
161
180
  this.finalizeStroke();
@@ -168,6 +187,10 @@ export default class Pen extends BaseTool {
168
187
  this.ctrlKeyPressed = false;
169
188
  return true;
170
189
  }
190
+ if (key === 'shift') {
191
+ this.shiftKeyPressed = false;
192
+ return true;
193
+ }
171
194
  return false;
172
195
  }
173
196
  }
@@ -10,6 +10,7 @@ export default class SelectionTool extends BaseTool {
10
10
  private prevSelectionBox;
11
11
  private selectionBox;
12
12
  private lastEvtTarget;
13
+ private startPoint;
13
14
  private expandingSelectionBox;
14
15
  private shiftKeyPressed;
15
16
  private ctrlKeyPressed;
@@ -14,6 +14,7 @@ class SelectionTool extends BaseTool {
14
14
  super(editor.notifier, description);
15
15
  this.editor = editor;
16
16
  this.lastEvtTarget = null;
17
+ this.startPoint = null; // canvas position
17
18
  this.expandingSelectionBox = false;
18
19
  this.shiftKeyPressed = false;
19
20
  this.ctrlKeyPressed = false;
@@ -24,6 +25,9 @@ class SelectionTool extends BaseTool {
24
25
  this.handleOverlay.classList.add('handleOverlay');
25
26
  editor.notifier.on(EditorEventType.ViewportChanged, _data => {
26
27
  var _a;
28
+ // The selection box could be using the wet ink display if its transformation
29
+ // hasn't been finalized yet. Clear before updating the UI.
30
+ this.editor.clearWetInk();
27
31
  (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.updateUI();
28
32
  });
29
33
  this.editor.handleKeyEventsFrom(this.handleOverlay);
@@ -61,6 +65,7 @@ class SelectionTool extends BaseTool {
61
65
  current = current.snappedToGrid(this.editor.viewport);
62
66
  }
63
67
  if (allPointers.length === 1 && current.isPrimary) {
68
+ this.startPoint = current.canvasPos;
64
69
  let transforming = false;
65
70
  if (this.lastEvtTarget && this.selectionBox) {
66
71
  if (snapToGrid) {
@@ -86,6 +91,9 @@ class SelectionTool extends BaseTool {
86
91
  if (!this.selectionBox)
87
92
  return;
88
93
  let currentPointer = event.current;
94
+ if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
95
+ currentPointer = currentPointer.lockedToXYAxes(this.startPoint, this.editor.viewport);
96
+ }
89
97
  if (this.ctrlKeyPressed) {
90
98
  currentPointer = currentPointer.snappedToGrid(this.editor.viewport);
91
99
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Resolves when all given promises have resolved. If no promises are given,
3
+ * does not return a Promise.
4
+ */
5
+ declare const waitForAll: (results: (Promise<void> | void)[]) => Promise<void> | void;
6
+ export default waitForAll;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resolves when all given promises have resolved. If no promises are given,
3
+ * does not return a Promise.
4
+ */
5
+ const waitForAll = (results) => {
6
+ // If any are Promises...
7
+ if (results.some(command => command && command['then'])) {
8
+ // Wait for all commands to finish.
9
+ return Promise.all(results)
10
+ // Ensure we return a Promise<void> and not a Promise<void[]>
11
+ .then(() => { });
12
+ }
13
+ return;
14
+ };
15
+ export default waitForAll;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/src/lib.d.ts",
6
6
  "main": "./dist/cjs/src/lib.js",
@@ -89,25 +89,25 @@
89
89
  "@types/bezier-js": "^4.1.0",
90
90
  "@types/jest": "^29.5.1",
91
91
  "@types/jsdom": "^21.1.1",
92
- "@types/node": "^18.16.2",
93
- "@typescript-eslint/eslint-plugin": "^5.59.1",
94
- "@typescript-eslint/parser": "^5.59.1",
92
+ "@types/node": "^20.1.0",
93
+ "@typescript-eslint/eslint-plugin": "^5.59.2",
94
+ "@typescript-eslint/parser": "^5.59.2",
95
95
  "css-loader": "^6.7.3",
96
- "eslint": "^8.39.0",
96
+ "eslint": "^8.40.0",
97
97
  "husky": "^8.0.3",
98
98
  "jest": "^29.5.0",
99
99
  "jest-environment-jsdom": "^29.5.0",
100
- "jsdom": "^21.1.1",
100
+ "jsdom": "^22.0.0",
101
101
  "lint-staged": "^13.2.2",
102
102
  "pinst": "^3.0.0",
103
103
  "style-loader": "^3.3.2",
104
- "terser-webpack-plugin": "^5.3.7",
104
+ "terser-webpack-plugin": "^5.3.8",
105
105
  "ts-jest": "^29.1.0",
106
106
  "ts-loader": "^9.4.2",
107
107
  "ts-node": "^10.9.1",
108
- "typedoc": "^0.24.6",
108
+ "typedoc": "^0.24.7",
109
109
  "typescript": "^5.0.4",
110
- "webpack": "^5.81.0"
110
+ "webpack": "^5.82.0"
111
111
  },
112
112
  "bugs": {
113
113
  "url": "https://github.com/personalizedrefrigerator/js-draw/issues"
@@ -81,6 +81,7 @@
81
81
 
82
82
  .toolbar-button > label {
83
83
  cursor: inherit;
84
+ user-select: none;
84
85
  }
85
86
 
86
87
  /* Decrease the font size of labels in the main toolbar if they're long. */
@@ -105,6 +106,7 @@
105
106
 
106
107
  .toolbar-root .toolbar-icon {
107
108
  flex-shrink: 1;
109
+ user-select: none;
108
110
 
109
111
  width: 100%;
110
112
  min-width: 30px;
@@ -131,7 +133,8 @@
131
133
  }
132
134
 
133
135
  .toolbar-dropdown.hidden,
134
- .toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-dropdown {
136
+ .toolbar-toolContainer:not(.selected):not(.dropdownShowable)
137
+ > .toolbar-dropdown:not(.hiding) {
135
138
  display: none;
136
139
  }
137
140
 
@@ -153,6 +156,37 @@
153
156
  box-shadow: 0px 3px 3px var(--primary-shadow-color);
154
157
  }
155
158
 
159
+ /* Animate showing/hiding the dropdown. Animations triggered in JavaScript. */
160
+ @keyframes dropdown-transition-in {
161
+ 0% {
162
+ opacity: 0;
163
+ transform: scale(1, 0);
164
+ }
165
+ 100% {
166
+ opacity: 1;
167
+ transform: scale(1, 1);
168
+ }
169
+ }
170
+
171
+ @keyframes dropdown-transition-out {
172
+ 0% {
173
+ opacity: 1;
174
+ transform: scale(1, 1);
175
+ }
176
+ 100% {
177
+ opacity: 0;
178
+ transform: scale(1, 0);
179
+ }
180
+ }
181
+
182
+ .toolbar-dropdown {
183
+ /* Ensure the animation begins from the correct location. */
184
+ transform-origin: top left;
185
+
186
+ --dropdown-show-animation: dropdown-transition-in;
187
+ --dropdown-hide-animation: dropdown-transition-out;
188
+ }
189
+
156
190
  .toolbar-buttonGroup {
157
191
  display: flex;
158
192
  flex-direction: row;