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