js-draw 0.1.8 → 0.1.11

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +15 -3
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +3 -0
  5. package/dist/src/Editor.js +45 -16
  6. package/dist/src/UndoRedoHistory.js +3 -0
  7. package/dist/src/Viewport.js +2 -0
  8. package/dist/src/bundle/bundled.d.ts +2 -1
  9. package/dist/src/bundle/bundled.js +2 -1
  10. package/dist/src/geometry/LineSegment2.d.ts +1 -0
  11. package/dist/src/geometry/LineSegment2.js +16 -0
  12. package/dist/src/geometry/Rect2.d.ts +1 -0
  13. package/dist/src/geometry/Rect2.js +16 -0
  14. package/dist/src/localizations/en.d.ts +3 -0
  15. package/dist/src/localizations/en.js +4 -0
  16. package/dist/src/localizations/es.d.ts +3 -0
  17. package/dist/src/localizations/es.js +18 -0
  18. package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
  19. package/dist/src/localizations/getLocalizationTable.js +43 -0
  20. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  21. package/dist/src/toolbar/HTMLToolbar.js +10 -8
  22. package/dist/src/toolbar/icons.js +13 -13
  23. package/dist/src/toolbar/localization.d.ts +2 -0
  24. package/dist/src/toolbar/localization.js +2 -0
  25. package/dist/src/toolbar/makeColorInput.js +22 -8
  26. package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
  27. package/dist/src/toolbar/widgets/HandToolWidget.js +8 -1
  28. package/dist/src/tools/BaseTool.d.ts +2 -1
  29. package/dist/src/tools/BaseTool.js +3 -0
  30. package/dist/src/tools/PanZoom.d.ts +2 -1
  31. package/dist/src/tools/PanZoom.js +10 -4
  32. package/dist/src/tools/SelectionTool.d.ts +9 -2
  33. package/dist/src/tools/SelectionTool.js +131 -19
  34. package/dist/src/tools/ToolController.js +6 -2
  35. package/dist/src/tools/localization.d.ts +1 -0
  36. package/dist/src/tools/localization.js +1 -0
  37. package/dist/src/types.d.ts +9 -2
  38. package/dist/src/types.js +1 -0
  39. package/package.json +9 -1
  40. package/src/Editor.ts +54 -14
  41. package/src/UndoRedoHistory.ts +4 -0
  42. package/src/Viewport.ts +2 -0
  43. package/src/bundle/bundled.ts +2 -1
  44. package/src/geometry/LineSegment2.test.ts +15 -0
  45. package/src/geometry/LineSegment2.ts +20 -0
  46. package/src/geometry/Rect2.test.ts +20 -7
  47. package/src/geometry/Rect2.ts +19 -1
  48. package/src/localizations/en.ts +8 -0
  49. package/src/localizations/es.ts +62 -0
  50. package/src/localizations/getLocalizationTable.test.ts +27 -0
  51. package/src/localizations/getLocalizationTable.ts +53 -0
  52. package/src/toolbar/HTMLToolbar.ts +11 -9
  53. package/src/toolbar/icons.ts +13 -13
  54. package/src/toolbar/localization.ts +4 -0
  55. package/src/toolbar/makeColorInput.ts +25 -10
  56. package/src/toolbar/toolbar.css +6 -2
  57. package/src/toolbar/widgets/BaseWidget.ts +34 -0
  58. package/src/toolbar/widgets/HandToolWidget.ts +12 -1
  59. package/src/tools/BaseTool.ts +5 -1
  60. package/src/tools/PanZoom.ts +13 -4
  61. package/src/tools/SelectionTool.test.ts +24 -1
  62. package/src/tools/SelectionTool.ts +158 -23
  63. package/src/tools/ToolController.ts +6 -2
  64. package/src/tools/localization.ts +2 -0
  65. package/src/types.ts +10 -1
  66. package/dist-test/test-dist-bundle.html +0 -42
@@ -66,7 +66,7 @@ describe('SelectionTool', () => {
66
66
 
67
67
  // Drag the object
68
68
  selection!.handleBackgroundDrag(Vec2.of(5, 5));
69
- selection!.finishDragging();
69
+ selection!.finalizeTransform();
70
70
 
71
71
  expect(testStroke.getBBox().topLeft).toMatchObject({
72
72
  x: 5,
@@ -80,4 +80,27 @@ describe('SelectionTool', () => {
80
80
  y: 0,
81
81
  });
82
82
  });
83
+
84
+ it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
85
+ const { addTestStrokeCommand } = createSquareStroke();
86
+ const editor = createEditor();
87
+ editor.dispatch(addTestStrokeCommand);
88
+
89
+ // Select the stroke
90
+ const selectionTool = getSelectionTool(editor);
91
+ selectionTool.setEnabled(true);
92
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
93
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
94
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
95
+
96
+ const selection = selectionTool.getSelection();
97
+ if (selection === null) {
98
+ // Throw to allow TypeScript's non-null checker to understand that selection
99
+ // must be non-null after this.
100
+ throw new Error('Selection should be non-null.');
101
+ }
102
+
103
+ selection.handleBackgroundDrag(Vec2.of(0, -1000));
104
+ expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
105
+ });
83
106
  });
@@ -8,7 +8,8 @@ import Mat33 from '../geometry/Mat33';
8
8
  import Rect2 from '../geometry/Rect2';
9
9
  import { Point2, Vec2 } from '../geometry/Vec2';
10
10
  import { EditorLocalization } from '../localization';
11
- import { EditorEventType, PointerEvt } from '../types';
11
+ import { EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
12
+ import Viewport from '../Viewport';
12
13
  import BaseTool from './BaseTool';
13
14
  import { ToolType } from './ToolController';
14
15
 
@@ -163,15 +164,15 @@ class Selection {
163
164
 
164
165
  makeDraggable(draggableBackground, (deltaPosition: Vec2) => {
165
166
  this.handleBackgroundDrag(deltaPosition);
166
- }, () => this.finishDragging());
167
+ }, () => this.finalizeTransform());
167
168
 
168
169
  makeDraggable(resizeCorner, (deltaPosition) => {
169
170
  this.handleResizeCornerDrag(deltaPosition);
170
- }, () => this.finishDragging());
171
+ }, () => this.finalizeTransform());
171
172
 
172
173
  makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
173
174
  this.handleRotateCircleDrag(offset);
174
- }, () => this.finishDragging());
175
+ }, () => this.finalizeTransform());
175
176
  }
176
177
 
177
178
  // Note a small change in the position of this' background while dragging
@@ -186,10 +187,7 @@ class Selection {
186
187
  // Snap position to a multiple of 10 (additional decimal points lead to larger files).
187
188
  deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
188
189
 
189
- this.region = this.region.translatedBy(deltaPosition);
190
- this.transform = this.transform.rightMul(Mat33.translation(deltaPosition));
191
-
192
- this.previewTransformCmds();
190
+ this.transformPreview(Mat33.translation(deltaPosition));
193
191
  }
194
192
 
195
193
  public handleResizeCornerDrag(deltaPosition: Vec2) {
@@ -203,21 +201,13 @@ class Selection {
203
201
  const newSize = this.region.size.plus(deltaPosition);
204
202
 
205
203
  if (newSize.y > 0 && newSize.x > 0) {
206
- this.region = this.region.resizedTo(newSize);
207
- const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight);
204
+ const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight);
208
205
 
209
- const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft);
210
- this.transform = this.transform.rightMul(currentTransfm);
211
- this.previewTransformCmds();
206
+ this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft));
212
207
  }
213
208
  }
214
209
 
215
210
  public handleRotateCircleDrag(offset: Vec2) {
216
- this.boxRotation = this.boxRotation % (2 * Math.PI);
217
- if (this.boxRotation < 0) {
218
- this.boxRotation += 2 * Math.PI;
219
- }
220
-
221
211
  let targetRotation = offset.angle();
222
212
  targetRotation = targetRotation % (2 * Math.PI);
223
213
  if (targetRotation < 0) {
@@ -237,9 +227,7 @@ class Selection {
237
227
  deltaRotation *= rotationDirection;
238
228
  }
239
229
 
240
- this.transform = this.transform.rightMul(Mat33.zRotation(deltaRotation, this.region.center));
241
- this.boxRotation += deltaRotation;
242
- this.previewTransformCmds();
230
+ this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center));
243
231
  }
244
232
 
245
233
  private computeTransformCommands() {
@@ -248,8 +236,29 @@ class Selection {
248
236
  });
249
237
  }
250
238
 
239
+ // Applies, previews, but doesn't finalize the given transformation.
240
+ public transformPreview(transform: Mat33) {
241
+ this.transform = this.transform.rightMul(transform);
242
+ const deltaRotation = transform.transformVec3(Vec2.unitX).angle();
243
+ transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center));
244
+
245
+ this.boxRotation += deltaRotation;
246
+ this.boxRotation = this.boxRotation % (2 * Math.PI);
247
+ if (this.boxRotation < 0) {
248
+ this.boxRotation += 2 * Math.PI;
249
+ }
250
+
251
+ const newSize = transform.transformVec3(this.region.size);
252
+ const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft);
253
+ this.region = this.region.resizedTo(newSize);
254
+ this.region = this.region.translatedBy(translation);
255
+
256
+ this.previewTransformCmds();
257
+ this.scrollTo();
258
+ }
259
+
251
260
  // Applies the current transformation to the selection
252
- public finishDragging() {
261
+ public finalizeTransform() {
253
262
  this.transformationCommands.forEach(cmd => {
254
263
  cmd.unapply(this.editor);
255
264
  });
@@ -321,7 +330,6 @@ class Selection {
321
330
  this.updateUI();
322
331
  }
323
332
 
324
-
325
333
  public appendBackgroundBoxTo(elem: HTMLElement) {
326
334
  if (this.backgroundBox.parentElement) {
327
335
  this.backgroundBox.remove();
@@ -444,6 +452,19 @@ class Selection {
444
452
  this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
445
453
  }
446
454
 
455
+ // Scroll the viewport to this. Does not zoom
456
+ public scrollTo() {
457
+ const viewport = this.editor.viewport;
458
+ const visibleRect = viewport.visibleRect;
459
+ if (!visibleRect.containsPoint(this.region.center)) {
460
+ const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center);
461
+ const delta = this.region.center.minus(closestPoint);
462
+ this.editor.dispatchNoAnnounce(
463
+ new Viewport.ViewportTransform(Mat33.translation(delta.times(-1))), false
464
+ );
465
+ }
466
+ }
467
+
447
468
  public deleteSelectedObjects(): Command {
448
469
  return new Erase(this.selectedElems);
449
470
  }
@@ -473,6 +494,8 @@ export default class SelectionTool extends BaseTool {
473
494
  this.selectionBox?.recomputeRegion();
474
495
  this.selectionBox?.updateUI();
475
496
  });
497
+
498
+ this.editor.handleKeyEventsFrom(this.handleOverlay);
476
499
  }
477
500
 
478
501
  public onPointerDown(event: PointerEvt): boolean {
@@ -512,7 +535,12 @@ export default class SelectionTool extends BaseTool {
512
535
  this.editor.announceForAccessibility(
513
536
  this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount())
514
537
  );
538
+ this.zoomToSelection();
539
+ }
540
+ }
515
541
 
542
+ private zoomToSelection() {
543
+ if (this.selectionBox) {
516
544
  const selectionRect = this.selectionBox.region;
517
545
  this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
518
546
  }
@@ -532,6 +560,106 @@ export default class SelectionTool extends BaseTool {
532
560
  this.selectionBox?.appendBackgroundBoxTo(this.handleOverlay);
533
561
  }
534
562
 
563
+ private static handleableKeys = [
564
+ 'a', 'h', 'ArrowLeft',
565
+ 'd', 'l', 'ArrowRight',
566
+ 'q', 'k', 'ArrowUp',
567
+ 'e', 'j', 'ArrowDown',
568
+ 'r', 'R',
569
+ 'i', 'I', 'o', 'O',
570
+ ];
571
+ public onKeyPress(event: KeyPressEvent): boolean {
572
+ let rotationSteps = 0;
573
+ let xTranslateSteps = 0;
574
+ let yTranslateSteps = 0;
575
+ let xScaleSteps = 0;
576
+ let yScaleSteps = 0;
577
+
578
+ switch (event.key) {
579
+ case 'a':
580
+ case 'h':
581
+ case 'ArrowLeft':
582
+ xTranslateSteps -= 1;
583
+ break;
584
+ case 'd':
585
+ case 'l':
586
+ case 'ArrowRight':
587
+ xTranslateSteps += 1;
588
+ break;
589
+ case 'q':
590
+ case 'k':
591
+ case 'ArrowUp':
592
+ yTranslateSteps -= 1;
593
+ break;
594
+ case 'e':
595
+ case 'j':
596
+ case 'ArrowDown':
597
+ yTranslateSteps += 1;
598
+ break;
599
+ case 'r':
600
+ rotationSteps += 1;
601
+ break;
602
+ case 'R':
603
+ rotationSteps -= 1;
604
+ break;
605
+ case 'i':
606
+ xScaleSteps -= 1;
607
+ break;
608
+ case 'I':
609
+ xScaleSteps += 1;
610
+ break;
611
+ case 'o':
612
+ yScaleSteps -= 1;
613
+ break;
614
+ case 'O':
615
+ yScaleSteps += 1;
616
+ break;
617
+ }
618
+
619
+ let handled = xTranslateSteps !== 0
620
+ || yTranslateSteps !== 0
621
+ || rotationSteps !== 0
622
+ || xScaleSteps !== 0
623
+ || yScaleSteps !== 0;
624
+
625
+ if (!this.selectionBox) {
626
+ handled = false;
627
+ } else if (handled) {
628
+ const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
629
+ const rotateStepSize = Math.PI / 8;
630
+ const scaleStepSize = translateStepSize / 2;
631
+
632
+ const region = this.selectionBox.region;
633
+ const scaledSize = this.selectionBox.region.size.plus(
634
+ Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize)
635
+ );
636
+
637
+ const transform = Mat33.scaling2D(
638
+ Vec2.of(
639
+ // Don't more-than-half the size of the selection
640
+ Math.max(0.5, scaledSize.x / region.size.x),
641
+ Math.max(0.5, scaledSize.y / region.size.y)
642
+ ),
643
+ region.topLeft
644
+ ).rightMul(Mat33.zRotation(
645
+ rotationSteps * rotateStepSize, region.center
646
+ )).rightMul(Mat33.translation(
647
+ Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)
648
+ ));
649
+ this.selectionBox.transformPreview(transform);
650
+ }
651
+
652
+ return handled;
653
+ }
654
+
655
+ public onKeyUp(evt: KeyUpEvent) {
656
+ if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
657
+ this.selectionBox.finalizeTransform();
658
+ return true;
659
+ }
660
+ return false;
661
+ }
662
+
535
663
  public setEnabled(enabled: boolean) {
536
664
  super.setEnabled(enabled);
537
665
 
@@ -540,6 +668,13 @@ export default class SelectionTool extends BaseTool {
540
668
  this.selectionBox = null;
541
669
 
542
670
  this.handleOverlay.style.display = enabled ? 'block' : 'none';
671
+
672
+ if (enabled) {
673
+ this.handleOverlay.tabIndex = 0;
674
+ this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts;
675
+ } else {
676
+ this.handleOverlay.tabIndex = -1;
677
+ }
543
678
  }
544
679
 
545
680
  // Get the object responsible for displaying this' selection.
@@ -30,6 +30,7 @@ export default class ToolController {
30
30
  public constructor(editor: Editor, localization: ToolLocalization) {
31
31
  const primaryToolEnabledGroup = new ToolEnabledGroup();
32
32
  const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
33
+ const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
33
34
  const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
34
35
  const primaryTools = [
35
36
  new SelectionTool(editor, localization.selectionTool),
@@ -48,6 +49,7 @@ export default class ToolController {
48
49
  new PipetteTool(editor, localization.pipetteTool),
49
50
  panZoomTool,
50
51
  ...primaryTools,
52
+ keyboardPanZoomTool,
51
53
  new UndoRedoShortcut(editor),
52
54
  ];
53
55
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
@@ -88,9 +90,10 @@ export default class ToolController {
88
90
  this.activeTool = null;
89
91
  handled = true;
90
92
  } else if (
91
- event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent
93
+ event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent
92
94
  ) {
93
95
  const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
96
+ const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent;
94
97
  const isWheelEvt = event.kind === InputEvtType.WheelEvt;
95
98
  for (const tool of this.tools) {
96
99
  if (!tool.isEnabled()) {
@@ -99,7 +102,8 @@ export default class ToolController {
99
102
 
100
103
  const wheelResult = isWheelEvt && tool.onWheel(event);
101
104
  const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
102
- handled = keyPressResult || wheelResult;
105
+ const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event);
106
+ handled = keyPressResult || wheelResult || keyReleaseResult;
103
107
 
104
108
  if (handled) {
105
109
  break;
@@ -1,5 +1,6 @@
1
1
 
2
2
  export interface ToolLocalization {
3
+ keyboardPanZoom: string;
3
4
  penTool: (penId: number)=>string;
4
5
  selectionTool: string;
5
6
  eraserTool: string;
@@ -25,6 +26,7 @@ export const defaultToolLocalization: ToolLocalization = {
25
26
  undoRedoTool: 'Undo/Redo',
26
27
  rightClickDragPanTool: 'Right-click drag',
27
28
  pipetteTool: 'Pick color from screen',
29
+ keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
28
30
 
29
31
  textTool: 'Text',
30
32
  enterTextToInsert: 'Text to insert',
package/src/types.ts CHANGED
@@ -24,6 +24,7 @@ export interface PointerEvtListener {
24
24
  onGestureCancel(): void;
25
25
  }
26
26
 
27
+
27
28
  export enum InputEvtType {
28
29
  PointerDownEvt,
29
30
  PointerMoveEvt,
@@ -32,6 +33,7 @@ export enum InputEvtType {
32
33
 
33
34
  WheelEvt,
34
35
  KeyPressEvent,
36
+ KeyUpEvent
35
37
  }
36
38
 
37
39
  // [delta.x] is horizontal scroll,
@@ -49,6 +51,12 @@ export interface KeyPressEvent {
49
51
  readonly ctrlKey: boolean;
50
52
  }
51
53
 
54
+ export interface KeyUpEvent {
55
+ readonly kind: InputEvtType.KeyUpEvent;
56
+ readonly key: string;
57
+ readonly ctrlKey: boolean;
58
+ }
59
+
52
60
  // Event triggered when pointer capture is taken by a different [PointerEvtListener].
53
61
  export interface GestureCancelEvt {
54
62
  readonly kind: InputEvtType.GestureCancelEvt;
@@ -72,7 +80,7 @@ export interface PointerUpEvt extends PointerEvtBase {
72
80
  }
73
81
 
74
82
  export type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
75
- export type InputEvt = KeyPressEvent | WheelEvt | GestureCancelEvt | PointerEvt;
83
+ export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt;
76
84
 
77
85
  export type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
78
86
 
@@ -109,6 +117,7 @@ export interface EditorViewportChangedEvent {
109
117
 
110
118
  // Canvas -> screen transform
111
119
  readonly newTransform: Mat33;
120
+ readonly oldTransform: Mat33;
112
121
  }
113
122
 
114
123
  export interface DisplayResizedEvent {
@@ -1,42 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
5
- <meta charset="utf-8"/>
6
- <title>Editor from a bundle</title>
7
- <style>
8
- body .imageEditorContainer {
9
- height: 800px;
10
-
11
- --primary-background-color: green;
12
- --primary-background-color-transparent: rgba(255, 240, 200, 0.5);
13
- --secondary-background-color: yellow;
14
- --primary-foreground-color: black;
15
- --secondary-foreground-color: black;
16
- }
17
- </style>
18
- </head>
19
- <body>
20
- <p>
21
- This file tests the bundled version of <code>js-draw</code>.
22
- Be sure to run <code>yarn build</code> before opening this!
23
- </p>
24
- <script src="../dist/bundle.js"></script>
25
- <script>
26
- const editor1 = new jsdraw.Editor(document.body, {
27
- wheelEventsEnabled: false,
28
- });
29
- editor1.addToolbar();
30
- editor1.loadFromSVG('<svg><text>Wheel events disabled.</text></svg>');
31
-
32
- const editor2 = new jsdraw.Editor(document.body, {
33
- wheelEventsEnabled: 'only-if-focused',
34
- });
35
- editor2.addToolbar();
36
- editor2.loadFromSVG('<svg><text>Wheel events enabled, only if focused.</text></svg>');
37
-
38
- const editor3 = new jsdraw.Editor(document.body);
39
- editor3.addToolbar();
40
- </script>
41
- </body>
42
- </html>