js-draw 0.10.3 → 0.11.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 (56) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +72 -0
  2. package/CHANGELOG.md +9 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +12 -3
  5. package/dist/src/Editor.js +72 -25
  6. package/dist/src/EditorImage.js +1 -1
  7. package/dist/src/SVGLoader.js +3 -2
  8. package/dist/src/components/AbstractComponent.d.ts +1 -0
  9. package/dist/src/components/AbstractComponent.js +15 -6
  10. package/dist/src/components/ImageComponent.d.ts +3 -0
  11. package/dist/src/components/ImageComponent.js +12 -1
  12. package/dist/src/localizations/es.js +1 -1
  13. package/dist/src/rendering/renderers/SVGRenderer.js +9 -5
  14. package/dist/src/toolbar/HTMLToolbar.js +2 -1
  15. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  16. package/dist/src/toolbar/IconProvider.js +7 -0
  17. package/dist/src/toolbar/localization.d.ts +8 -0
  18. package/dist/src/toolbar/localization.js +8 -0
  19. package/dist/src/toolbar/widgets/InsertImageWidget.d.ts +19 -0
  20. package/dist/src/toolbar/widgets/InsertImageWidget.js +169 -0
  21. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  22. package/dist/src/toolbar/widgets/lib.js +1 -0
  23. package/dist/src/tools/Eraser.d.ts +1 -1
  24. package/dist/src/tools/Eraser.js +16 -18
  25. package/dist/src/tools/PanZoom.js +10 -0
  26. package/dist/src/tools/PasteHandler.js +1 -39
  27. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -3
  28. package/dist/src/tools/SelectionTool/Selection.js +63 -26
  29. package/dist/src/tools/SelectionTool/SelectionTool.js +9 -0
  30. package/dist/src/util/fileToBase64.d.ts +3 -0
  31. package/dist/src/util/fileToBase64.js +13 -0
  32. package/dist/src/util/waitForTimeout.d.ts +2 -0
  33. package/dist/src/util/waitForTimeout.js +7 -0
  34. package/package.json +1 -1
  35. package/src/Editor.ts +90 -27
  36. package/src/EditorImage.ts +1 -1
  37. package/src/SVGLoader.ts +1 -0
  38. package/src/components/AbstractComponent.ts +18 -4
  39. package/src/components/ImageComponent.ts +15 -0
  40. package/src/localizations/es.ts +3 -0
  41. package/src/rendering/renderers/SVGRenderer.ts +6 -1
  42. package/src/toolbar/HTMLToolbar.ts +3 -1
  43. package/src/toolbar/IconProvider.ts +8 -0
  44. package/src/toolbar/localization.ts +19 -1
  45. package/src/toolbar/toolbar.css +2 -0
  46. package/src/toolbar/widgets/InsertImageWidget.css +44 -0
  47. package/src/toolbar/widgets/InsertImageWidget.ts +222 -0
  48. package/src/toolbar/widgets/lib.ts +2 -0
  49. package/src/tools/Eraser.ts +19 -15
  50. package/src/tools/PanZoom.test.ts +65 -0
  51. package/src/tools/PanZoom.ts +12 -0
  52. package/src/tools/PasteHandler.ts +2 -51
  53. package/src/tools/SelectionTool/Selection.ts +62 -22
  54. package/src/tools/SelectionTool/SelectionTool.ts +12 -0
  55. package/src/util/fileToBase64.ts +18 -0
  56. package/src/util/waitForTimeout.ts +9 -0
@@ -0,0 +1,169 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import ImageComponent from '../../components/ImageComponent';
11
+ import Erase from '../../commands/Erase';
12
+ import EditorImage from '../../EditorImage';
13
+ import { SelectionTool, uniteCommands } from '../../lib';
14
+ import Mat33 from '../../math/Mat33';
15
+ import fileToBase64 from '../../util/fileToBase64';
16
+ import ActionButtonWidget from './ActionButtonWidget';
17
+ export default class InsertImageWidget extends ActionButtonWidget {
18
+ constructor(editor, localization) {
19
+ localization !== null && localization !== void 0 ? localization : (localization = editor.localization);
20
+ super(editor, 'insert-image-widget', () => editor.icons.makeInsertImageIcon(), localization.image, () => this.onClicked());
21
+ this.imageSelectionOverlay = document.createElement('div');
22
+ this.imageSelectionOverlay.classList.add('toolbar-image-selection-overlay');
23
+ this.fillOverlay();
24
+ this.editor.createHTMLOverlay(this.imageSelectionOverlay);
25
+ this.imageSelectionOverlay.style.display = 'none';
26
+ }
27
+ fillOverlay() {
28
+ const container = document.createElement('div');
29
+ const chooseImageRow = document.createElement('div');
30
+ const altTextRow = document.createElement('div');
31
+ this.imagePreview = document.createElement('img');
32
+ this.statusView = document.createElement('div');
33
+ const actionButtonRow = document.createElement('div');
34
+ actionButtonRow.classList.add('action-button-row');
35
+ this.submitButton = document.createElement('button');
36
+ const cancelButton = document.createElement('button');
37
+ this.imageFileInput = document.createElement('input');
38
+ this.imageAltTextInput = document.createElement('input');
39
+ const imageFileInputLabel = document.createElement('label');
40
+ const imageAltTextLabel = document.createElement('label');
41
+ const fileInputId = `insert-image-file-input-${InsertImageWidget.nextInputId++}`;
42
+ const altTextInputId = `insert-image-alt-text-input-${InsertImageWidget.nextInputId++}`;
43
+ this.imageFileInput.setAttribute('id', fileInputId);
44
+ this.imageAltTextInput.setAttribute('id', altTextInputId);
45
+ imageAltTextLabel.htmlFor = altTextInputId;
46
+ imageFileInputLabel.htmlFor = fileInputId;
47
+ this.imageFileInput.accept = 'image/*';
48
+ imageAltTextLabel.innerText = this.localizationTable.inputAltText;
49
+ imageFileInputLabel.innerText = this.localizationTable.chooseFile;
50
+ this.imageFileInput.type = 'file';
51
+ this.imageAltTextInput.type = 'text';
52
+ this.statusView.setAttribute('aria-live', 'polite');
53
+ cancelButton.innerText = this.localizationTable.cancel;
54
+ this.submitButton.innerText = this.localizationTable.submit;
55
+ this.imageFileInput.onchange = () => __awaiter(this, void 0, void 0, function* () {
56
+ if (this.imageFileInput.value === '' || !this.imageFileInput.files || !this.imageFileInput.files[0]) {
57
+ this.imagePreview.style.display = 'none';
58
+ this.submitButton.disabled = true;
59
+ return;
60
+ }
61
+ this.imagePreview.style.display = 'block';
62
+ const image = this.imageFileInput.files[0];
63
+ let data = null;
64
+ try {
65
+ data = yield fileToBase64(image);
66
+ }
67
+ catch (e) {
68
+ this.statusView.innerText = this.localizationTable.imageLoadError(e);
69
+ }
70
+ this.imageBase64URL = data;
71
+ if (data) {
72
+ this.imagePreview.src = data;
73
+ this.submitButton.disabled = false;
74
+ this.updateImageSizeDisplay();
75
+ }
76
+ else {
77
+ this.submitButton.disabled = true;
78
+ this.statusView.innerText = '';
79
+ }
80
+ });
81
+ cancelButton.onclick = () => {
82
+ this.hideDialog();
83
+ };
84
+ this.imageSelectionOverlay.onclick = (evt) => {
85
+ // If clicking on the backdrop
86
+ if (evt.target === this.imageSelectionOverlay) {
87
+ this.hideDialog();
88
+ }
89
+ };
90
+ chooseImageRow.replaceChildren(imageFileInputLabel, this.imageFileInput);
91
+ altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
92
+ actionButtonRow.replaceChildren(cancelButton, this.submitButton);
93
+ container.replaceChildren(chooseImageRow, altTextRow, this.imagePreview, this.statusView, actionButtonRow);
94
+ this.imageSelectionOverlay.replaceChildren(container);
95
+ }
96
+ hideDialog() {
97
+ this.imageSelectionOverlay.style.display = 'none';
98
+ }
99
+ updateImageSizeDisplay() {
100
+ var _a;
101
+ const imageData = (_a = this.imageBase64URL) !== null && _a !== void 0 ? _a : '';
102
+ const sizeInKiB = imageData.length / 1024;
103
+ const sizeInMiB = sizeInKiB / 1024;
104
+ let units = 'KiB';
105
+ let size = sizeInKiB;
106
+ if (sizeInMiB >= 1) {
107
+ size = sizeInMiB;
108
+ units = 'MiB';
109
+ }
110
+ this.statusView.innerText = this.localizationTable.imageSize(Math.round(size), units);
111
+ }
112
+ clearInputs() {
113
+ this.imageFileInput.value = '';
114
+ this.imageAltTextInput.value = '';
115
+ this.imagePreview.style.display = 'none';
116
+ this.submitButton.disabled = true;
117
+ this.statusView.innerText = '';
118
+ }
119
+ onClicked() {
120
+ var _a;
121
+ this.imageSelectionOverlay.style.display = '';
122
+ this.clearInputs();
123
+ this.imageFileInput.focus();
124
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
125
+ const selectedObjects = selectionTools.map(tool => tool.getSelectedObjects()).flat();
126
+ let editingImage = null;
127
+ if (selectedObjects.length === 1 && selectedObjects[0] instanceof ImageComponent) {
128
+ editingImage = selectedObjects[0];
129
+ this.imageAltTextInput.value = (_a = editingImage.getAltText()) !== null && _a !== void 0 ? _a : '';
130
+ this.imagePreview.style.display = 'block';
131
+ this.submitButton.disabled = false;
132
+ this.imageBase64URL = editingImage.getURL();
133
+ this.imagePreview.src = this.imageBase64URL;
134
+ this.updateImageSizeDisplay();
135
+ }
136
+ else {
137
+ selectionTools.forEach(tool => tool.clearSelection());
138
+ }
139
+ this.submitButton.onclick = () => __awaiter(this, void 0, void 0, function* () {
140
+ var _b;
141
+ if (!this.imageBase64URL) {
142
+ return;
143
+ }
144
+ const image = new Image();
145
+ image.src = this.imageBase64URL;
146
+ image.setAttribute('alt', this.imageAltTextInput.value);
147
+ const component = yield ImageComponent.fromImage(image, Mat33.identity);
148
+ if (component.getBBox().area === 0) {
149
+ this.statusView.innerText = this.localizationTable.errorImageHasZeroSize;
150
+ return;
151
+ }
152
+ this.imageSelectionOverlay.style.display = 'none';
153
+ if (editingImage) {
154
+ const eraseCommand = new Erase([editingImage]);
155
+ yield this.editor.dispatch(uniteCommands([
156
+ EditorImage.addElement(component),
157
+ component.transformBy(editingImage.getTransformation()),
158
+ component.setZIndex(editingImage.getZIndex()),
159
+ eraseCommand,
160
+ ]));
161
+ (_b = selectionTools[0]) === null || _b === void 0 ? void 0 : _b.setSelection([component]);
162
+ }
163
+ else {
164
+ yield this.editor.addAndCenterComponents([component]);
165
+ }
166
+ });
167
+ }
168
+ }
169
+ InsertImageWidget.nextInputId = 0;
@@ -6,3 +6,4 @@ export { default as TextToolWidget } from './TextToolWidget';
6
6
  export { default as HandToolWidget } from './HandToolWidget';
7
7
  export { default as SelectionToolWidget } from './SelectionToolWidget';
8
8
  export { default as EraserToolWidget } from './EraserToolWidget';
9
+ export { default as InsertImageWidget } from './InsertImageWidget';
@@ -6,3 +6,4 @@ export { default as TextToolWidget } from './TextToolWidget';
6
6
  export { default as HandToolWidget } from './HandToolWidget';
7
7
  export { default as SelectionToolWidget } from './SelectionToolWidget';
8
8
  export { default as EraserToolWidget } from './EraserToolWidget';
9
+ export { default as InsertImageWidget } from './InsertImageWidget';
@@ -4,8 +4,8 @@ import Editor from '../Editor';
4
4
  export default class Eraser extends BaseTool {
5
5
  private editor;
6
6
  private lastPoint;
7
- private command;
8
7
  private toRemove;
8
+ private partialCommands;
9
9
  constructor(editor: Editor, description: string);
10
10
  onPointerDown(event: PointerEvt): boolean;
11
11
  onPointerMove(event: PointerEvt): void;
@@ -6,7 +6,8 @@ export default class Eraser extends BaseTool {
6
6
  constructor(editor, description) {
7
7
  super(editor.notifier, description);
8
8
  this.editor = editor;
9
- this.command = null;
9
+ // Commands that each remove one element
10
+ this.partialCommands = [];
10
11
  }
11
12
  onPointerDown(event) {
12
13
  if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
@@ -17,35 +18,32 @@ export default class Eraser extends BaseTool {
17
18
  return false;
18
19
  }
19
20
  onPointerMove(event) {
20
- var _a;
21
21
  const currentPoint = event.current.canvasPos;
22
22
  if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
23
23
  return;
24
24
  }
25
25
  const line = new LineSegment2(this.lastPoint, currentPoint);
26
26
  const region = line.bbox;
27
- // Remove any intersecting elements.
28
- this.toRemove.push(...this.editor.image
29
- .getElementsIntersectingRegion(region).filter(component => {
27
+ const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
30
28
  return component.intersects(line);
31
- }));
32
- (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
33
- this.command = new Erase(this.toRemove);
34
- this.command.apply(this.editor);
29
+ });
30
+ // Remove any intersecting elements.
31
+ this.toRemove.push(...intersectingElems);
32
+ // Create new Erase commands for the now-to-be-erased elements and apply them.
33
+ const newPartialCommands = intersectingElems.map(elem => new Erase([elem]));
34
+ newPartialCommands.forEach(cmd => cmd.apply(this.editor));
35
+ this.partialCommands.push(...newPartialCommands);
35
36
  this.lastPoint = currentPoint;
36
37
  }
37
38
  onPointerUp(_event) {
38
- var _a;
39
- if (this.command && this.toRemove.length > 0) {
40
- (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
41
- // Dispatch the command to make it undo-able
42
- this.editor.dispatch(this.command);
39
+ if (this.toRemove.length > 0) {
40
+ // Undo commands for each individual component and unite into a single command.
41
+ this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
42
+ const command = new Erase(this.toRemove);
43
+ this.editor.dispatch(command); // dispatch: Makes undo-able.
43
44
  }
44
- this.command = null;
45
45
  }
46
46
  onGestureCancel() {
47
- var _a;
48
- (_a = this.command) === null || _a === void 0 ? void 0 : _a.unapply(this.editor);
49
- this.command = null;
47
+ this.partialCommands.forEach(cmd => cmd.unapply(this.editor));
50
48
  }
51
49
  }
@@ -202,10 +202,20 @@ export default class PanZoom extends BaseTool {
202
202
  && this.velocity !== null
203
203
  && event.current.timeStamp - this.lastPointerDownTimestamp > minInertialScrollDt;
204
204
  if (shouldInertialScroll && this.velocity !== null) {
205
+ const oldVelocity = this.velocity;
205
206
  // If the user drags the screen, then stops, then lifts the pointer,
206
207
  // we want the final velocity to reflect the stop at the end (so the velocity
207
208
  // should be near zero). Handle this:
208
209
  this.updateVelocity(event.current.screenPos);
210
+ // Work around an input issue. Some devices that disable the touchscreen when a stylus
211
+ // comes near the screen fire a touch-end event at the position of the stylus when a
212
+ // touch gesture is canceled. Because the stylus is often far away from the last touch,
213
+ // this causes a great displacement between the second-to-last (from the touchscreen) and
214
+ // last (from the pen that is now near the screen) events. Only allow velocity to decrease
215
+ // to work around this:
216
+ if (oldVelocity.magnitude() < this.velocity.magnitude()) {
217
+ this.velocity = oldVelocity;
218
+ }
209
219
  // Cancel any ongoing inertial scrolling.
210
220
  (_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
211
221
  this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
@@ -12,16 +12,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  });
13
13
  };
14
14
  import { TextComponent } from '../components/lib';
15
- import { uniteCommands } from '../commands/lib';
16
15
  import SVGLoader from '../SVGLoader';
17
16
  import { Mat33 } from '../math/lib';
18
17
  import BaseTool from './BaseTool';
19
- import EditorImage from '../EditorImage';
20
- import SelectionTool from './SelectionTool/SelectionTool';
21
18
  import TextTool from './TextTool';
22
19
  import Color4 from '../Color4';
23
20
  import ImageComponent from '../components/ImageComponent';
24
- import Viewport from '../Viewport';
25
21
  // { @inheritDoc PasteHandler! }
26
22
  export default class PasteHandler extends BaseTool {
27
23
  constructor(editor) {
@@ -46,41 +42,7 @@ export default class PasteHandler extends BaseTool {
46
42
  }
47
43
  addComponentsFromPaste(components) {
48
44
  return __awaiter(this, void 0, void 0, function* () {
49
- let bbox = null;
50
- for (const component of components) {
51
- if (bbox) {
52
- bbox = bbox.union(component.getBBox());
53
- }
54
- else {
55
- bbox = component.getBBox();
56
- }
57
- }
58
- if (!bbox) {
59
- return;
60
- }
61
- // Find a transform that scales/moves bbox onto the screen.
62
- const visibleRect = this.editor.viewport.visibleRect;
63
- const scaleRatioX = visibleRect.width / bbox.width;
64
- const scaleRatioY = visibleRect.height / bbox.height;
65
- let scaleRatio = scaleRatioX;
66
- if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
67
- scaleRatio = scaleRatioY;
68
- }
69
- scaleRatio *= 2 / 3;
70
- scaleRatio = Viewport.roundScaleRatio(scaleRatio);
71
- const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
72
- const commands = [];
73
- for (const component of components) {
74
- // To allow deserialization, we need to add first, then transform.
75
- commands.push(EditorImage.addElement(component));
76
- commands.push(component.transformBy(transfm));
77
- }
78
- const applyChunkSize = 100;
79
- this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
80
- for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
81
- selectionTool.setEnabled(true);
82
- selectionTool.setSelection(components);
83
- }
45
+ yield this.editor.addAndCenterComponents(components);
84
46
  });
85
47
  }
86
48
  doSVGPaste(data) {
@@ -14,7 +14,6 @@ export default class Selection {
14
14
  private originalRegion;
15
15
  private transformers;
16
16
  private transform;
17
- private transformCommands;
18
17
  private selectedElems;
19
18
  private container;
20
19
  private backgroundElem;
@@ -28,7 +27,6 @@ export default class Selection {
28
27
  get preTransformedScreenRegionRotation(): number;
29
28
  get screenRegion(): Rect2;
30
29
  get screenRegionRotation(): number;
31
- private computeTransformCommands;
32
30
  setTransform(transform: Mat33, preview?: boolean): void;
33
31
  finalizeTransform(): void;
34
32
  private static ApplyTransformationCommand;
@@ -38,13 +36,14 @@ export default class Selection {
38
36
  getMinCanvasSize(): number;
39
37
  getSelectedItemCount(): number;
40
38
  updateUI(): void;
39
+ private addRemoveSelectionFromImage;
41
40
  private targetHandle;
42
41
  private backgroundDragging;
43
42
  onDragStart(pointer: Pointer, target: EventTarget): boolean;
44
43
  onDragUpdate(pointer: Pointer): void;
45
44
  onDragEnd(): void;
46
45
  onDragCancel(): void;
47
- scrollTo(): void;
46
+ scrollTo(): Promise<void>;
48
47
  deleteSelectedObjects(): Command;
49
48
  duplicateSelectedObjects(): Command;
50
49
  addTo(elem: HTMLElement): void;
@@ -22,13 +22,14 @@ import Erase from '../../commands/Erase';
22
22
  import Duplicate from '../../commands/Duplicate';
23
23
  import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode';
24
24
  import { ResizeMode } from './types';
25
+ import EditorImage from '../../EditorImage';
25
26
  const updateChunkSize = 100;
27
+ const maxPreviewElemCount = 500;
26
28
  // @internal
27
29
  export default class Selection {
28
30
  constructor(startPoint, editor) {
29
31
  this.editor = editor;
30
32
  this.transform = Mat33.identity;
31
- this.transformCommands = [];
32
33
  this.selectedElems = [];
33
34
  this.hasParent = true;
34
35
  this.targetHandle = null;
@@ -89,28 +90,19 @@ export default class Selection {
89
90
  get screenRegionRotation() {
90
91
  return this.regionRotation + this.editor.viewport.getRotationAngle();
91
92
  }
92
- computeTransformCommands() {
93
- return this.selectedElems.map(elem => {
94
- return elem.transformBy(this.transform);
95
- });
96
- }
97
93
  // Applies, previews, but doesn't finalize the given transformation.
98
94
  setTransform(transform, preview = true) {
99
95
  this.transform = transform;
100
96
  if (preview && this.hasParent) {
101
- this.previewTransformCmds();
102
97
  this.scrollTo();
98
+ this.previewTransformCmds();
103
99
  }
104
100
  }
105
101
  // Applies the current transformation to the selection
106
102
  finalizeTransform() {
107
- this.transformCommands.forEach(cmd => {
108
- cmd.unapply(this.editor);
109
- });
110
103
  const fullTransform = this.transform;
111
104
  const selectedElems = this.selectedElems;
112
105
  // Reset for the next drag
113
- this.transformCommands = [];
114
106
  this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
115
107
  this.transform = Mat33.identity;
116
108
  // Make the commands undo-able
@@ -119,13 +111,19 @@ export default class Selection {
119
111
  // Preview the effects of the current transformation on the selection
120
112
  previewTransformCmds() {
121
113
  // Don't render what we're moving if it's likely to be slow.
122
- if (this.selectedElems.length > updateChunkSize) {
114
+ if (this.selectedElems.length > maxPreviewElemCount) {
123
115
  this.updateUI();
124
116
  return;
125
117
  }
126
- this.transformCommands.forEach(cmd => cmd.unapply(this.editor));
127
- this.transformCommands = this.computeTransformCommands();
128
- this.transformCommands.forEach(cmd => cmd.apply(this.editor));
118
+ const wetInkRenderer = this.editor.display.getWetInkRenderer();
119
+ wetInkRenderer.clear();
120
+ wetInkRenderer.pushTransform(this.transform);
121
+ const viewportVisibleRect = this.editor.viewport.visibleRect;
122
+ const visibleRect = viewportVisibleRect.transformedBoundingBox(this.transform.inverse());
123
+ for (const elem of this.selectedElems) {
124
+ elem.render(wetInkRenderer, visibleRect);
125
+ }
126
+ wetInkRenderer.popTransform();
129
127
  this.updateUI();
130
128
  }
131
129
  // Find the objects corresponding to this in the document,
@@ -208,7 +206,39 @@ export default class Selection {
208
206
  handle.updatePosition();
209
207
  }
210
208
  }
209
+ // Add/remove the contents of this' seleciton from the editor.
210
+ // Used to prevent previewed content from looking like duplicate content
211
+ // while dragging.
212
+ //
213
+ // Does nothing if a large number of elements are selected (and so modifying
214
+ // the editor image is likely to be slow.)
215
+ //
216
+ // If removed from the image, selected elements are drawn as wet ink.
217
+ addRemoveSelectionFromImage(inImage) {
218
+ return __awaiter(this, void 0, void 0, function* () {
219
+ // Don't hide elements if doing so will be slow.
220
+ if (!inImage && this.selectedElems.length > maxPreviewElemCount) {
221
+ return;
222
+ }
223
+ for (const elem of this.selectedElems) {
224
+ const parent = this.editor.image.findParent(elem);
225
+ if (!inImage) {
226
+ parent === null || parent === void 0 ? void 0 : parent.remove();
227
+ }
228
+ // If we're making things visible and the selected object wasn't previously
229
+ // visible,
230
+ else if (!parent) {
231
+ EditorImage.addElement(elem).apply(this.editor);
232
+ }
233
+ }
234
+ yield this.editor.queueRerender();
235
+ if (!inImage) {
236
+ this.previewTransformCmds();
237
+ }
238
+ });
239
+ }
211
240
  onDragStart(pointer, target) {
241
+ void this.addRemoveSelectionFromImage(false);
212
242
  for (const handle of this.handles) {
213
243
  if (handle.isTarget(target)) {
214
244
  handle.handleDragStart(pointer);
@@ -230,7 +260,6 @@ export default class Selection {
230
260
  if (this.targetHandle) {
231
261
  this.targetHandle.handleDragUpdate(pointer);
232
262
  }
233
- this.updateUI();
234
263
  }
235
264
  onDragEnd() {
236
265
  if (this.backgroundDragging) {
@@ -239,6 +268,7 @@ export default class Selection {
239
268
  else if (this.targetHandle) {
240
269
  this.targetHandle.handleDragEnd();
241
270
  }
271
+ this.addRemoveSelectionFromImage(true);
242
272
  this.backgroundDragging = false;
243
273
  this.targetHandle = null;
244
274
  this.updateUI();
@@ -247,19 +277,26 @@ export default class Selection {
247
277
  this.backgroundDragging = false;
248
278
  this.targetHandle = null;
249
279
  this.setTransform(Mat33.identity);
280
+ this.addRemoveSelectionFromImage(true);
250
281
  }
251
282
  // Scroll the viewport to this. Does not zoom
252
283
  scrollTo() {
253
- if (this.selectedElems.length === 0) {
254
- return;
255
- }
256
- const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
257
- if (!screenRect.containsPoint(this.screenRegion.center)) {
258
- const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
259
- const screenDelta = this.screenRegion.center.minus(closestPoint);
260
- const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
261
- this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
262
- }
284
+ return __awaiter(this, void 0, void 0, function* () {
285
+ if (this.selectedElems.length === 0) {
286
+ return;
287
+ }
288
+ const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
289
+ if (!screenRect.containsPoint(this.screenRegion.center)) {
290
+ const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
291
+ const screenDelta = this.screenRegion.center.minus(closestPoint);
292
+ const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
293
+ yield this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
294
+ // Re-renders clear wet ink, so we need to re-draw the preview
295
+ // after the full re-render.
296
+ yield this.editor.queueRerender();
297
+ this.previewTransformCmds();
298
+ }
299
+ });
263
300
  }
264
301
  deleteSelectedObjects() {
265
302
  return new Erase(this.selectedElems);
@@ -311,6 +311,15 @@ export default class SelectionTool extends BaseTool {
311
311
  setSelection(objects) {
312
312
  // Only select selectable objects.
313
313
  objects = objects.filter(obj => obj.isSelectable());
314
+ // Sort by z-index
315
+ objects.sort((a, b) => a.getZIndex() - b.getZIndex());
316
+ // Remove duplicates
317
+ objects = objects.filter((current, idx) => {
318
+ if (idx > 0) {
319
+ return current !== objects[idx - 1];
320
+ }
321
+ return true;
322
+ });
314
323
  let bbox = null;
315
324
  for (const object of objects) {
316
325
  if (bbox) {
@@ -0,0 +1,3 @@
1
+ type ProgressListener = (evt: ProgressEvent<FileReader>) => void;
2
+ declare const fileToBase64: (file: File, onprogress?: ProgressListener) => Promise<string | null>;
3
+ export default fileToBase64;
@@ -0,0 +1,13 @@
1
+ const fileToBase64 = (file, onprogress) => {
2
+ const reader = new FileReader();
3
+ return new Promise((resolve, reject) => {
4
+ reader.onload = () => resolve(reader.result);
5
+ reader.onerror = reject;
6
+ reader.onabort = reject;
7
+ reader.onprogress = (evt) => {
8
+ onprogress === null || onprogress === void 0 ? void 0 : onprogress(evt);
9
+ };
10
+ reader.readAsDataURL(file);
11
+ });
12
+ };
13
+ export default fileToBase64;
@@ -0,0 +1,2 @@
1
+ declare const waitForTimeout: (timeout: number) => Promise<void>;
2
+ export default waitForTimeout;
@@ -0,0 +1,7 @@
1
+ // Returns a promise that resolves after `timeout` milliseconds.
2
+ const waitForTimeout = (timeout) => {
3
+ return new Promise(resolve => {
4
+ setTimeout(() => resolve(), timeout);
5
+ });
6
+ };
7
+ export default waitForTimeout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.10.3",
3
+ "version": "0.11.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",