js-draw 0.0.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 (156) hide show
  1. package/.eslintrc.js +57 -0
  2. package/.husky/pre-commit +4 -0
  3. package/LICENSE +21 -0
  4. package/README.md +74 -0
  5. package/__mocks__/coloris.ts +8 -0
  6. package/__mocks__/styleMock.js +1 -0
  7. package/dist/__mocks__/coloris.d.ts +2 -0
  8. package/dist/__mocks__/coloris.js +5 -0
  9. package/dist/build_tools/BundledFile.d.ts +12 -0
  10. package/dist/build_tools/BundledFile.js +153 -0
  11. package/dist/scripts/bundle.d.ts +1 -0
  12. package/dist/scripts/bundle.js +19 -0
  13. package/dist/scripts/watchBundle.d.ts +1 -0
  14. package/dist/scripts/watchBundle.js +9 -0
  15. package/dist/src/Color4.d.ts +23 -0
  16. package/dist/src/Color4.js +102 -0
  17. package/dist/src/Display.d.ts +22 -0
  18. package/dist/src/Display.js +93 -0
  19. package/dist/src/Editor.d.ts +55 -0
  20. package/dist/src/Editor.js +366 -0
  21. package/dist/src/EditorImage.d.ts +44 -0
  22. package/dist/src/EditorImage.js +243 -0
  23. package/dist/src/EventDispatcher.d.ts +11 -0
  24. package/dist/src/EventDispatcher.js +39 -0
  25. package/dist/src/Pointer.d.ts +22 -0
  26. package/dist/src/Pointer.js +57 -0
  27. package/dist/src/SVGLoader.d.ts +21 -0
  28. package/dist/src/SVGLoader.js +204 -0
  29. package/dist/src/StrokeBuilder.d.ts +35 -0
  30. package/dist/src/StrokeBuilder.js +275 -0
  31. package/dist/src/UndoRedoHistory.d.ts +17 -0
  32. package/dist/src/UndoRedoHistory.js +46 -0
  33. package/dist/src/Viewport.d.ts +39 -0
  34. package/dist/src/Viewport.js +134 -0
  35. package/dist/src/commands/Command.d.ts +15 -0
  36. package/dist/src/commands/Command.js +29 -0
  37. package/dist/src/commands/Erase.d.ts +11 -0
  38. package/dist/src/commands/Erase.js +37 -0
  39. package/dist/src/commands/localization.d.ts +19 -0
  40. package/dist/src/commands/localization.js +17 -0
  41. package/dist/src/components/AbstractComponent.d.ts +19 -0
  42. package/dist/src/components/AbstractComponent.js +46 -0
  43. package/dist/src/components/Stroke.d.ts +16 -0
  44. package/dist/src/components/Stroke.js +79 -0
  45. package/dist/src/components/UnknownSVGObject.d.ts +15 -0
  46. package/dist/src/components/UnknownSVGObject.js +25 -0
  47. package/dist/src/components/localization.d.ts +5 -0
  48. package/dist/src/components/localization.js +4 -0
  49. package/dist/src/geometry/LineSegment2.d.ts +19 -0
  50. package/dist/src/geometry/LineSegment2.js +100 -0
  51. package/dist/src/geometry/Mat33.d.ts +31 -0
  52. package/dist/src/geometry/Mat33.js +187 -0
  53. package/dist/src/geometry/Path.d.ts +55 -0
  54. package/dist/src/geometry/Path.js +364 -0
  55. package/dist/src/geometry/Rect2.d.ts +47 -0
  56. package/dist/src/geometry/Rect2.js +148 -0
  57. package/dist/src/geometry/Vec2.d.ts +13 -0
  58. package/dist/src/geometry/Vec2.js +13 -0
  59. package/dist/src/geometry/Vec3.d.ts +32 -0
  60. package/dist/src/geometry/Vec3.js +98 -0
  61. package/dist/src/localization.d.ts +12 -0
  62. package/dist/src/localization.js +5 -0
  63. package/dist/src/main.d.ts +3 -0
  64. package/dist/src/main.js +4 -0
  65. package/dist/src/rendering/AbstractRenderer.d.ts +38 -0
  66. package/dist/src/rendering/AbstractRenderer.js +108 -0
  67. package/dist/src/rendering/CanvasRenderer.d.ts +23 -0
  68. package/dist/src/rendering/CanvasRenderer.js +108 -0
  69. package/dist/src/rendering/DummyRenderer.d.ts +25 -0
  70. package/dist/src/rendering/DummyRenderer.js +65 -0
  71. package/dist/src/rendering/SVGRenderer.d.ts +27 -0
  72. package/dist/src/rendering/SVGRenderer.js +122 -0
  73. package/dist/src/testing/loadExpectExtensions.d.ts +17 -0
  74. package/dist/src/testing/loadExpectExtensions.js +27 -0
  75. package/dist/src/toolbar/HTMLToolbar.d.ts +12 -0
  76. package/dist/src/toolbar/HTMLToolbar.js +444 -0
  77. package/dist/src/toolbar/types.d.ts +17 -0
  78. package/dist/src/toolbar/types.js +5 -0
  79. package/dist/src/tools/BaseTool.d.ts +20 -0
  80. package/dist/src/tools/BaseTool.js +44 -0
  81. package/dist/src/tools/Eraser.d.ts +16 -0
  82. package/dist/src/tools/Eraser.js +53 -0
  83. package/dist/src/tools/PanZoom.d.ts +40 -0
  84. package/dist/src/tools/PanZoom.js +191 -0
  85. package/dist/src/tools/Pen.d.ts +25 -0
  86. package/dist/src/tools/Pen.js +97 -0
  87. package/dist/src/tools/SelectionTool.d.ts +49 -0
  88. package/dist/src/tools/SelectionTool.js +437 -0
  89. package/dist/src/tools/ToolController.d.ts +18 -0
  90. package/dist/src/tools/ToolController.js +110 -0
  91. package/dist/src/tools/ToolEnabledGroup.d.ts +6 -0
  92. package/dist/src/tools/ToolEnabledGroup.js +11 -0
  93. package/dist/src/tools/localization.d.ts +10 -0
  94. package/dist/src/tools/localization.js +9 -0
  95. package/dist/src/types.d.ts +88 -0
  96. package/dist/src/types.js +20 -0
  97. package/jest.config.js +22 -0
  98. package/lint-staged.config.js +6 -0
  99. package/package.json +82 -0
  100. package/src/Color4.test.ts +12 -0
  101. package/src/Color4.ts +122 -0
  102. package/src/Display.ts +118 -0
  103. package/src/Editor.css +58 -0
  104. package/src/Editor.ts +469 -0
  105. package/src/EditorImage.test.ts +90 -0
  106. package/src/EditorImage.ts +297 -0
  107. package/src/EventDispatcher.test.ts +123 -0
  108. package/src/EventDispatcher.ts +53 -0
  109. package/src/Pointer.ts +93 -0
  110. package/src/SVGLoader.ts +230 -0
  111. package/src/StrokeBuilder.ts +362 -0
  112. package/src/UndoRedoHistory.ts +61 -0
  113. package/src/Viewport.ts +168 -0
  114. package/src/commands/Command.ts +43 -0
  115. package/src/commands/Erase.ts +52 -0
  116. package/src/commands/localization.ts +38 -0
  117. package/src/components/AbstractComponent.ts +73 -0
  118. package/src/components/Stroke.test.ts +18 -0
  119. package/src/components/Stroke.ts +102 -0
  120. package/src/components/UnknownSVGObject.ts +36 -0
  121. package/src/components/localization.ts +9 -0
  122. package/src/editorStyles.js +3 -0
  123. package/src/geometry/LineSegment2.test.ts +77 -0
  124. package/src/geometry/LineSegment2.ts +127 -0
  125. package/src/geometry/Mat33.test.ts +144 -0
  126. package/src/geometry/Mat33.ts +268 -0
  127. package/src/geometry/Path.fromString.test.ts +146 -0
  128. package/src/geometry/Path.test.ts +96 -0
  129. package/src/geometry/Path.toString.test.ts +31 -0
  130. package/src/geometry/Path.ts +456 -0
  131. package/src/geometry/Rect2.test.ts +121 -0
  132. package/src/geometry/Rect2.ts +215 -0
  133. package/src/geometry/Vec2.test.ts +32 -0
  134. package/src/geometry/Vec2.ts +18 -0
  135. package/src/geometry/Vec3.test.ts +29 -0
  136. package/src/geometry/Vec3.ts +133 -0
  137. package/src/localization.ts +27 -0
  138. package/src/rendering/AbstractRenderer.ts +164 -0
  139. package/src/rendering/CanvasRenderer.ts +141 -0
  140. package/src/rendering/DummyRenderer.ts +80 -0
  141. package/src/rendering/SVGRenderer.ts +159 -0
  142. package/src/testing/loadExpectExtensions.ts +43 -0
  143. package/src/toolbar/HTMLToolbar.ts +551 -0
  144. package/src/toolbar/toolbar.css +110 -0
  145. package/src/toolbar/types.ts +20 -0
  146. package/src/tools/BaseTool.ts +58 -0
  147. package/src/tools/Eraser.ts +67 -0
  148. package/src/tools/PanZoom.ts +253 -0
  149. package/src/tools/Pen.ts +121 -0
  150. package/src/tools/SelectionTool.test.ts +85 -0
  151. package/src/tools/SelectionTool.ts +545 -0
  152. package/src/tools/ToolController.ts +126 -0
  153. package/src/tools/ToolEnabledGroup.ts +14 -0
  154. package/src/tools/localization.ts +22 -0
  155. package/src/types.ts +133 -0
  156. package/tsconfig.json +28 -0
@@ -0,0 +1,437 @@
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 Mat33 from '../geometry/Mat33';
11
+ // import Mat33 from "../geometry/Mat33";
12
+ import Rect2 from '../geometry/Rect2';
13
+ import { Vec2 } from '../geometry/Vec2';
14
+ import { EditorEventType } from '../types';
15
+ import Viewport from '../Viewport';
16
+ import BaseTool from './BaseTool';
17
+ import { ToolType } from './ToolController';
18
+ const handleScreenSize = 30;
19
+ const styles = `
20
+ .handleOverlay {
21
+ }
22
+
23
+ .handleOverlay > .selectionBox {
24
+ position: fixed;
25
+ z-index: 0;
26
+ transform-origin: center;
27
+ }
28
+
29
+ .handleOverlay > .selectionBox .draggableBackground {
30
+ position: absolute;
31
+ top: 0;
32
+ left: 0;
33
+ right: 0;
34
+ bottom: 0;
35
+
36
+ background-color: var(--secondary-background-color);
37
+ opacity: 0.8;
38
+ border: 1px solid var(--primary-background-color);
39
+ }
40
+
41
+ .handleOverlay .resizeCorner {
42
+ width: ${handleScreenSize}px;
43
+ height: ${handleScreenSize}px;
44
+ margin-right: -${handleScreenSize / 2}px;
45
+ margin-bottom: -${handleScreenSize / 2}px;
46
+
47
+ position: absolute;
48
+ bottom: 0;
49
+ right: 0;
50
+
51
+ opacity: 0.8;
52
+ background-color: var(--primary-background-color);
53
+ border: 1px solid var(--primary-foreground-color);
54
+ }
55
+
56
+ .handleOverlay > .selectionBox .rotateCircleContainer {
57
+ position: absolute;
58
+ top: 50%;
59
+ bottom: 50%;
60
+ left: 50%;
61
+ right: 50%;
62
+ }
63
+
64
+ .handleOverlay .rotateCircle {
65
+ width: ${handleScreenSize}px;
66
+ height: ${handleScreenSize}px;
67
+ margin-left: -${handleScreenSize / 2}px;
68
+ margin-top: -${handleScreenSize / 2}px;
69
+ opacity: 0.8;
70
+
71
+ border: 1px solid var(--primary-foreground-color);
72
+ background-color: var(--primary-background-color);
73
+ border-radius: 100%;
74
+ }
75
+ `;
76
+ const makeDraggable = (element, onDrag, onDragEnd) => {
77
+ element.style.touchAction = 'none';
78
+ let down = false;
79
+ // Work around a Safari bug
80
+ element.addEventListener('touchstart', evt => evt.preventDefault());
81
+ let lastX;
82
+ let lastY;
83
+ element.addEventListener('pointerdown', event => {
84
+ if (event.isPrimary) {
85
+ down = true;
86
+ element.setPointerCapture(event.pointerId);
87
+ lastX = event.pageX;
88
+ lastY = event.pageY;
89
+ return true;
90
+ }
91
+ return false;
92
+ });
93
+ element.addEventListener('pointermove', event => {
94
+ if (event.isPrimary && down) {
95
+ // Safari/iOS doesn't seem to support movementX/movementY on pointer events.
96
+ // Calculate manually:
97
+ const delta = Vec2.of(event.pageX - lastX, event.pageY - lastY);
98
+ onDrag(delta, Vec2.of(event.offsetX, event.offsetY));
99
+ lastX = event.pageX;
100
+ lastY = event.pageY;
101
+ return true;
102
+ }
103
+ return false;
104
+ });
105
+ const onPointerEnd = (event) => {
106
+ if (event.isPrimary) {
107
+ down = false;
108
+ onDragEnd();
109
+ return true;
110
+ }
111
+ return false;
112
+ };
113
+ element.addEventListener('pointerup', onPointerEnd);
114
+ element.addEventListener('pointercancel', onPointerEnd);
115
+ };
116
+ // Maximum number of strokes to transform without a re-render.
117
+ const updateChunkSize = 50;
118
+ class Selection {
119
+ constructor(startPoint, editor) {
120
+ this.startPoint = startPoint;
121
+ this.editor = editor;
122
+ this.boxRotation = this.editor.viewport.getRotationAngle();
123
+ this.selectedElems = [];
124
+ this.region = Rect2.bboxOf([startPoint]);
125
+ // Create draggable rectangles
126
+ this.backgroundBox = document.createElement('div');
127
+ const draggableBackground = document.createElement('div');
128
+ const resizeCorner = document.createElement('div');
129
+ this.rotateCircle = document.createElement('div');
130
+ const rotateCircleContainer = document.createElement('div');
131
+ this.backgroundBox.classList.add('selectionBox');
132
+ draggableBackground.classList.add('draggableBackground');
133
+ resizeCorner.classList.add('resizeCorner');
134
+ this.rotateCircle.classList.add('rotateCircle');
135
+ rotateCircleContainer.classList.add('rotateCircleContainer');
136
+ rotateCircleContainer.appendChild(this.rotateCircle);
137
+ this.backgroundBox.appendChild(draggableBackground);
138
+ this.backgroundBox.appendChild(rotateCircleContainer);
139
+ this.backgroundBox.appendChild(resizeCorner);
140
+ this.transformationCommands = [];
141
+ this.transform = Mat33.identity;
142
+ makeDraggable(draggableBackground, (deltaPosition) => {
143
+ this.handleBackgroundDrag(deltaPosition);
144
+ }, () => this.finishDragging());
145
+ makeDraggable(resizeCorner, (deltaPosition) => {
146
+ this.handleResizeCornerDrag(deltaPosition);
147
+ }, () => this.finishDragging());
148
+ makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
149
+ this.handleRotateCircleDrag(offset);
150
+ }, () => this.finishDragging());
151
+ }
152
+ // Note a small change in the position of this' background while dragging
153
+ // At the end of a drag, changes should be applied by calling this.finishDragging()
154
+ handleBackgroundDrag(deltaPosition) {
155
+ // Re-scale the change in position
156
+ // (use a Vec3 transform to avoid translating deltaPosition)
157
+ deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
158
+ // Snap position to a multiple of 10 (additional decimal points lead to larger files).
159
+ deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
160
+ this.region = this.region.translatedBy(deltaPosition);
161
+ this.transform = this.transform.rightMul(Mat33.translation(deltaPosition));
162
+ this.previewTransformCmds();
163
+ }
164
+ handleResizeCornerDrag(deltaPosition) {
165
+ deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
166
+ deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
167
+ const oldWidth = this.region.w;
168
+ const oldHeight = this.region.h;
169
+ const newSize = this.region.size.plus(deltaPosition);
170
+ if (newSize.y > 0 && newSize.x > 0) {
171
+ this.region = this.region.resizedTo(newSize);
172
+ const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight);
173
+ const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft);
174
+ this.transform = this.transform.rightMul(currentTransfm);
175
+ this.previewTransformCmds();
176
+ }
177
+ }
178
+ handleRotateCircleDrag(offset) {
179
+ this.boxRotation = this.boxRotation % (2 * Math.PI);
180
+ if (this.boxRotation < 0) {
181
+ this.boxRotation += 2 * Math.PI;
182
+ }
183
+ let targetRotation = offset.angle();
184
+ targetRotation = targetRotation % (2 * Math.PI);
185
+ if (targetRotation < 0) {
186
+ targetRotation += 2 * Math.PI;
187
+ }
188
+ let deltaRotation = (targetRotation - this.boxRotation);
189
+ const rotationStep = Math.PI / 12;
190
+ if (Math.abs(deltaRotation) < rotationStep || !isFinite(deltaRotation)) {
191
+ return;
192
+ }
193
+ else {
194
+ const rotationDirection = Math.sign(deltaRotation);
195
+ // Step exactly one rotationStep
196
+ deltaRotation = Math.floor(Math.abs(deltaRotation) / rotationStep) * rotationStep;
197
+ deltaRotation *= rotationDirection;
198
+ }
199
+ this.transform = this.transform.rightMul(Mat33.zRotation(deltaRotation, this.region.center));
200
+ this.boxRotation += deltaRotation;
201
+ this.previewTransformCmds();
202
+ }
203
+ computeTransformCommands() {
204
+ return this.selectedElems.map(elem => {
205
+ return elem.transformBy(this.transform);
206
+ });
207
+ }
208
+ // Applies the current transformation to the selection
209
+ finishDragging() {
210
+ this.transformationCommands.forEach(cmd => {
211
+ cmd.unapply(this.editor);
212
+ });
213
+ const fullTransform = this.transform;
214
+ const inverseTransform = this.transform.inverse();
215
+ const deltaBoxRotation = this.boxRotation;
216
+ const currentTransfmCommands = this.computeTransformCommands();
217
+ // Reset for the next drag
218
+ this.transformationCommands = [];
219
+ this.transform = Mat33.identity;
220
+ this.region = this.region.transformedBoundingBox(inverseTransform);
221
+ // Make the commands undo-able
222
+ this.editor.dispatch({
223
+ apply: (editor) => __awaiter(this, void 0, void 0, function* () {
224
+ // Approximate the new selection
225
+ this.region = this.region.transformedBoundingBox(fullTransform);
226
+ this.boxRotation += deltaBoxRotation;
227
+ this.updateUI();
228
+ yield editor.asyncApplyCommands(currentTransfmCommands, updateChunkSize);
229
+ this.recomputeRegion();
230
+ this.updateUI();
231
+ }),
232
+ unapply: (editor) => __awaiter(this, void 0, void 0, function* () {
233
+ this.region = this.region.transformedBoundingBox(inverseTransform);
234
+ this.boxRotation -= deltaBoxRotation;
235
+ this.updateUI();
236
+ yield editor.asyncUnapplyCommands(currentTransfmCommands, updateChunkSize);
237
+ this.recomputeRegion();
238
+ this.updateUI();
239
+ }),
240
+ description(localizationTable) {
241
+ return localizationTable.transformedElements(currentTransfmCommands.length);
242
+ },
243
+ });
244
+ }
245
+ // Preview the effects of the current transformation on the selection
246
+ previewTransformCmds() {
247
+ // Don't render what we're moving if it's likely to be slow.
248
+ if (this.selectedElems.length > updateChunkSize) {
249
+ this.updateUI();
250
+ return;
251
+ }
252
+ this.transformationCommands.forEach(cmd => cmd.unapply(this.editor));
253
+ this.transformationCommands = this.computeTransformCommands();
254
+ this.transformationCommands.forEach(cmd => cmd.apply(this.editor));
255
+ this.updateUI();
256
+ }
257
+ appendBackgroundBoxTo(elem) {
258
+ if (this.backgroundBox.parentElement) {
259
+ this.backgroundBox.remove();
260
+ }
261
+ elem.appendChild(this.backgroundBox);
262
+ }
263
+ setToPoint(point) {
264
+ this.region = this.region.grownToPoint(point);
265
+ this.recomputeBoxRotation();
266
+ this.updateUI();
267
+ }
268
+ cancelSelection() {
269
+ if (this.backgroundBox.parentElement) {
270
+ this.backgroundBox.remove();
271
+ }
272
+ this.region = Rect2.empty;
273
+ }
274
+ // Find the objects corresponding to this in the document,
275
+ // select them.
276
+ // Returns false iff nothing was selected.
277
+ resolveToObjects() {
278
+ // Grow the rectangle, if necessary
279
+ if (this.region.w === 0 || this.region.h === 0) {
280
+ const padding = this.editor.viewport.visibleRect.maxDimension / 100;
281
+ this.region = Rect2.bboxOf(this.region.corners, padding);
282
+ }
283
+ this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
284
+ if (this.region.containsRect(elem.getBBox())) {
285
+ return true;
286
+ }
287
+ else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
288
+ return true;
289
+ }
290
+ return false;
291
+ });
292
+ // Find the bounding box of all selected elements.
293
+ if (!this.recomputeRegion()) {
294
+ return false;
295
+ }
296
+ this.updateUI();
297
+ return true;
298
+ }
299
+ // Recompute this' region from the selected elements. Resets rotation to zero.
300
+ // Returns false if the selection is empty.
301
+ recomputeRegion() {
302
+ const newRegion = this.selectedElems.reduce((accumulator, elem) => {
303
+ return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
304
+ }, null);
305
+ if (!newRegion) {
306
+ this.cancelSelection();
307
+ return false;
308
+ }
309
+ this.region = newRegion;
310
+ const minSize = this.getMinCanvasSize();
311
+ if (this.region.w < minSize || this.region.h < minSize) {
312
+ // Add padding
313
+ const padding = minSize / 2;
314
+ this.region = Rect2.bboxOf(this.region.corners, padding);
315
+ }
316
+ this.recomputeBoxRotation();
317
+ return true;
318
+ }
319
+ getMinCanvasSize() {
320
+ const canvasHandleSize = handleScreenSize / this.editor.viewport.getScaleFactor();
321
+ return canvasHandleSize * 2;
322
+ }
323
+ recomputeBoxRotation() {
324
+ this.boxRotation = this.editor.viewport.getRotationAngle();
325
+ }
326
+ getSelectedItemCount() {
327
+ return this.selectedElems.length;
328
+ }
329
+ updateUI() {
330
+ if (!this.backgroundBox) {
331
+ return;
332
+ }
333
+ const rightSideDirection = this.region.topRight.minus(this.region.bottomRight);
334
+ const topSideDirection = this.region.topLeft.minus(this.region.topRight);
335
+ const toScreen = this.editor.viewport.canvasToScreenTransform;
336
+ const centerOnScreen = toScreen.transformVec2(this.region.center);
337
+ const heightOnScreen = toScreen.transformVec3(rightSideDirection).magnitude();
338
+ const widthOnScreen = toScreen.transformVec3(topSideDirection).magnitude();
339
+ this.backgroundBox.style.marginLeft = `${centerOnScreen.x - widthOnScreen / 2}px`;
340
+ this.backgroundBox.style.marginTop = `${centerOnScreen.y - heightOnScreen / 2}px`;
341
+ this.backgroundBox.style.width = `${widthOnScreen}px`;
342
+ this.backgroundBox.style.height = `${heightOnScreen}px`;
343
+ const rotationDeg = this.boxRotation * 180 / Math.PI;
344
+ this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
345
+ this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
346
+ }
347
+ }
348
+ export default class SelectionTool extends BaseTool {
349
+ constructor(editor, description) {
350
+ super(editor.notifier, description);
351
+ this.editor = editor;
352
+ this.kind = ToolType.Selection;
353
+ this.handleOverlay = document.createElement('div');
354
+ editor.createHTMLOverlay(this.handleOverlay);
355
+ editor.addStyleSheet(styles);
356
+ this.handleOverlay.style.display = 'none';
357
+ this.handleOverlay.classList.add('handleOverlay');
358
+ editor.notifier.on(EditorEventType.ViewportChanged, _data => {
359
+ var _a, _b;
360
+ (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.recomputeRegion();
361
+ (_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.updateUI();
362
+ });
363
+ }
364
+ onPointerDown(event) {
365
+ if (event.allPointers.length === 1 && event.current.isPrimary) {
366
+ this.prevSelectionBox = this.selectionBox;
367
+ this.selectionBox = new Selection(event.current.canvasPos, this.editor);
368
+ // Remove any previous selection rects
369
+ this.handleOverlay.replaceChildren();
370
+ this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
371
+ return true;
372
+ }
373
+ return false;
374
+ }
375
+ onPointerMove(event) {
376
+ if (!this.selectionBox)
377
+ return;
378
+ this.selectionBox.setToPoint(event.current.canvasPos);
379
+ }
380
+ onGestureEnd() {
381
+ if (!this.selectionBox)
382
+ return;
383
+ // Expand/shrink the selection rectangle, if applicable
384
+ const hasSelection = this.selectionBox.resolveToObjects();
385
+ // Note that the selection has changed
386
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
387
+ kind: EditorEventType.ToolUpdated,
388
+ tool: this,
389
+ });
390
+ if (hasSelection) {
391
+ const visibleRect = this.editor.viewport.visibleRect;
392
+ const selectionRect = this.selectionBox.region;
393
+ this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
394
+ // Try to move the selection within the center 2/3rds of the viewport.
395
+ const targetRect = visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
396
+ // Ensure that the selection fits within the target
397
+ if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
398
+ const multiplier = Math.max(selectionRect.w / targetRect.w, selectionRect.h / targetRect.h);
399
+ const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
400
+ const viewportContentTransform = visibleRectTransform.inverse();
401
+ (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
402
+ }
403
+ // Ensure that the top left is visible
404
+ if (!targetRect.containsRect(selectionRect)) {
405
+ // target position - current position
406
+ const translation = selectionRect.center.minus(targetRect.center);
407
+ const visibleRectTransform = Mat33.translation(translation);
408
+ const viewportContentTransform = visibleRectTransform.inverse();
409
+ (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
410
+ }
411
+ }
412
+ }
413
+ onPointerUp(event) {
414
+ if (!this.selectionBox)
415
+ return;
416
+ this.selectionBox.setToPoint(event.current.canvasPos);
417
+ this.onGestureEnd();
418
+ }
419
+ onGestureCancel() {
420
+ var _a, _b;
421
+ // Revert to the previous selection, if any.
422
+ (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection();
423
+ this.selectionBox = this.prevSelectionBox;
424
+ (_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.appendBackgroundBoxTo(this.handleOverlay);
425
+ }
426
+ setEnabled(enabled) {
427
+ super.setEnabled(enabled);
428
+ // Clear the selection
429
+ this.handleOverlay.replaceChildren();
430
+ this.selectionBox = null;
431
+ this.handleOverlay.style.display = enabled ? 'block' : 'none';
432
+ }
433
+ // Get the object responsible for displaying this' selection.
434
+ getSelection() {
435
+ return this.selectionBox;
436
+ }
437
+ }
@@ -0,0 +1,18 @@
1
+ import { InputEvt } from '../types';
2
+ import Editor from '../Editor';
3
+ import BaseTool from './BaseTool';
4
+ import { ToolLocalization } from './localization';
5
+ export declare enum ToolType {
6
+ TouchPanZoom = 0,
7
+ Pen = 1,
8
+ Selection = 2,
9
+ Eraser = 3,
10
+ PanZoom = 4
11
+ }
12
+ export default class ToolController {
13
+ private tools;
14
+ private activeTool;
15
+ constructor(editor: Editor, localization: ToolLocalization);
16
+ dispatchInputEvent(event: InputEvt): boolean;
17
+ getMatchingTools(kind: ToolType): BaseTool[];
18
+ }
@@ -0,0 +1,110 @@
1
+ import { InputEvtType, EditorEventType } from '../types';
2
+ import PanZoom, { PanZoomMode } from './PanZoom';
3
+ import Pen from './Pen';
4
+ import ToolEnabledGroup from './ToolEnabledGroup';
5
+ import Eraser from './Eraser';
6
+ import SelectionTool from './SelectionTool';
7
+ import Color4 from '../Color4';
8
+ export var ToolType;
9
+ (function (ToolType) {
10
+ ToolType[ToolType["TouchPanZoom"] = 0] = "TouchPanZoom";
11
+ ToolType[ToolType["Pen"] = 1] = "Pen";
12
+ ToolType[ToolType["Selection"] = 2] = "Selection";
13
+ ToolType[ToolType["Eraser"] = 3] = "Eraser";
14
+ ToolType[ToolType["PanZoom"] = 4] = "PanZoom";
15
+ })(ToolType || (ToolType = {}));
16
+ export default class ToolController {
17
+ constructor(editor, localization) {
18
+ const primaryToolEnabledGroup = new ToolEnabledGroup();
19
+ const touchPanZoom = new PanZoom(editor, PanZoomMode.OneFingerGestures, localization.touchPanTool);
20
+ const primaryPenTool = new Pen(editor, localization.penTool(1));
21
+ const primaryTools = [
22
+ new SelectionTool(editor, localization.selectionTool),
23
+ new Eraser(editor, localization.eraserTool),
24
+ // Three pens
25
+ primaryPenTool,
26
+ new Pen(editor, localization.penTool(2), Color4.clay, 8),
27
+ // Highlighter-like pen with width=64
28
+ new Pen(editor, localization.penTool(3), Color4.ofRGBA(1, 1, 0, 0.5), 64),
29
+ ];
30
+ this.tools = [
31
+ touchPanZoom,
32
+ ...primaryTools,
33
+ new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
34
+ ];
35
+ primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
36
+ touchPanZoom.setEnabled(false);
37
+ primaryPenTool.setEnabled(true);
38
+ editor.notifier.on(EditorEventType.ToolEnabled, event => {
39
+ if (event.kind === EditorEventType.ToolEnabled) {
40
+ editor.announceForAccessibility(localization.toolEnabledAnnouncement(event.tool.description));
41
+ }
42
+ });
43
+ editor.notifier.on(EditorEventType.ToolDisabled, event => {
44
+ if (event.kind === EditorEventType.ToolDisabled) {
45
+ editor.announceForAccessibility(localization.toolDisabledAnnouncement(event.tool.description));
46
+ }
47
+ });
48
+ this.activeTool = null;
49
+ }
50
+ // Returns true if the event was handled
51
+ dispatchInputEvent(event) {
52
+ var _a, _b;
53
+ let handled = false;
54
+ if (event.kind === InputEvtType.PointerDownEvt) {
55
+ for (const tool of this.tools) {
56
+ if (tool.isEnabled() && tool.onPointerDown(event)) {
57
+ if (this.activeTool !== tool) {
58
+ (_a = this.activeTool) === null || _a === void 0 ? void 0 : _a.onGestureCancel();
59
+ }
60
+ this.activeTool = tool;
61
+ handled = true;
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ else if (event.kind === InputEvtType.PointerUpEvt) {
67
+ (_b = this.activeTool) === null || _b === void 0 ? void 0 : _b.onPointerUp(event);
68
+ this.activeTool = null;
69
+ handled = true;
70
+ }
71
+ else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent) {
72
+ const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
73
+ const isWheelEvt = event.kind === InputEvtType.WheelEvt;
74
+ for (const tool of this.tools) {
75
+ if (!tool.isEnabled()) {
76
+ continue;
77
+ }
78
+ const wheelResult = isWheelEvt && tool.onWheel(event);
79
+ const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
80
+ handled = keyPressResult || wheelResult;
81
+ if (handled) {
82
+ break;
83
+ }
84
+ }
85
+ }
86
+ else if (this.activeTool !== null) {
87
+ let allCasesHandledGuard;
88
+ switch (event.kind) {
89
+ case InputEvtType.PointerMoveEvt:
90
+ this.activeTool.onPointerMove(event);
91
+ break;
92
+ case InputEvtType.GestureCancelEvt:
93
+ this.activeTool.onGestureCancel();
94
+ this.activeTool = null;
95
+ break;
96
+ default:
97
+ allCasesHandledGuard = event;
98
+ return allCasesHandledGuard;
99
+ }
100
+ handled = true;
101
+ }
102
+ else {
103
+ handled = false;
104
+ }
105
+ return handled;
106
+ }
107
+ getMatchingTools(kind) {
108
+ return this.tools.filter(tool => tool.kind === kind);
109
+ }
110
+ }
@@ -0,0 +1,6 @@
1
+ import BaseTool from './BaseTool';
2
+ export default class ToolEnabledGroup {
3
+ private activeTool;
4
+ constructor();
5
+ notifyEnabled(tool: BaseTool): void;
6
+ }
@@ -0,0 +1,11 @@
1
+ // Connects a group of tools -- at most one tool in the group must be enabled.
2
+ export default class ToolEnabledGroup {
3
+ constructor() { }
4
+ notifyEnabled(tool) {
5
+ var _a;
6
+ if (tool !== this.activeTool) {
7
+ (_a = this.activeTool) === null || _a === void 0 ? void 0 : _a.setEnabled(false);
8
+ this.activeTool = tool;
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ export interface ToolLocalization {
2
+ penTool: (penId: number) => string;
3
+ selectionTool: string;
4
+ eraserTool: string;
5
+ touchPanTool: string;
6
+ twoFingerPanZoomTool: string;
7
+ toolEnabledAnnouncement: (toolName: string) => string;
8
+ toolDisabledAnnouncement: (toolName: string) => string;
9
+ }
10
+ export declare const defaultToolLocalization: ToolLocalization;
@@ -0,0 +1,9 @@
1
+ export const defaultToolLocalization = {
2
+ penTool: (penId) => `Pen ${penId}`,
3
+ selectionTool: 'Selection',
4
+ eraserTool: 'Eraser',
5
+ touchPanTool: 'Touch Panning',
6
+ twoFingerPanZoomTool: 'Panning and Zooming',
7
+ toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
8
+ toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
9
+ };