js-draw 0.1.7 → 0.1.10

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 (68) 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 +1 -0
  5. package/dist/src/Editor.js +32 -15
  6. package/dist/src/UndoRedoHistory.js +3 -0
  7. package/dist/src/bundle/bundled.d.ts +2 -1
  8. package/dist/src/bundle/bundled.js +2 -1
  9. package/dist/src/components/builders/RectangleBuilder.d.ts +3 -1
  10. package/dist/src/components/builders/RectangleBuilder.js +17 -8
  11. package/dist/src/geometry/LineSegment2.d.ts +1 -0
  12. package/dist/src/geometry/LineSegment2.js +16 -0
  13. package/dist/src/geometry/Rect2.d.ts +1 -0
  14. package/dist/src/geometry/Rect2.js +16 -0
  15. package/dist/src/localizations/en.d.ts +3 -0
  16. package/dist/src/localizations/en.js +4 -0
  17. package/dist/src/localizations/es.d.ts +3 -0
  18. package/dist/src/localizations/es.js +18 -0
  19. package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
  20. package/dist/src/localizations/getLocalizationTable.js +43 -0
  21. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -1
  22. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  23. package/dist/src/toolbar/HTMLToolbar.js +10 -8
  24. package/dist/src/toolbar/icons.js +13 -13
  25. package/dist/src/toolbar/localization.d.ts +1 -0
  26. package/dist/src/toolbar/localization.js +1 -0
  27. package/dist/src/toolbar/makeColorInput.js +22 -8
  28. package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
  29. package/dist/src/tools/BaseTool.d.ts +2 -1
  30. package/dist/src/tools/BaseTool.js +3 -0
  31. package/dist/src/tools/PanZoom.d.ts +2 -1
  32. package/dist/src/tools/PanZoom.js +4 -0
  33. package/dist/src/tools/SelectionTool.d.ts +9 -2
  34. package/dist/src/tools/SelectionTool.js +131 -19
  35. package/dist/src/tools/ToolController.js +6 -2
  36. package/dist/src/tools/localization.d.ts +1 -0
  37. package/dist/src/tools/localization.js +1 -0
  38. package/dist/src/types.d.ts +8 -2
  39. package/dist/src/types.js +1 -0
  40. package/package.json +9 -1
  41. package/src/Editor.ts +36 -14
  42. package/src/EditorImage.test.ts +1 -1
  43. package/src/UndoRedoHistory.ts +4 -0
  44. package/src/bundle/bundled.ts +2 -1
  45. package/src/components/builders/RectangleBuilder.ts +23 -8
  46. package/src/geometry/LineSegment2.test.ts +15 -0
  47. package/src/geometry/LineSegment2.ts +20 -0
  48. package/src/geometry/Rect2.test.ts +20 -7
  49. package/src/geometry/Rect2.ts +19 -1
  50. package/src/localizations/en.ts +8 -0
  51. package/src/localizations/es.ts +60 -0
  52. package/src/localizations/getLocalizationTable.test.ts +27 -0
  53. package/src/localizations/getLocalizationTable.ts +53 -0
  54. package/src/rendering/caching/RenderingCacheNode.ts +6 -1
  55. package/src/toolbar/HTMLToolbar.ts +11 -9
  56. package/src/toolbar/icons.ts +13 -13
  57. package/src/toolbar/localization.ts +2 -0
  58. package/src/toolbar/makeColorInput.ts +25 -10
  59. package/src/toolbar/toolbar.css +5 -0
  60. package/src/toolbar/widgets/BaseWidget.ts +34 -0
  61. package/src/tools/BaseTool.ts +5 -1
  62. package/src/tools/PanZoom.ts +6 -0
  63. package/src/tools/SelectionTool.test.ts +24 -1
  64. package/src/tools/SelectionTool.ts +158 -23
  65. package/src/tools/ToolController.ts +6 -2
  66. package/src/tools/localization.ts +2 -0
  67. package/src/types.ts +9 -1
  68. package/dist-test/test-dist-bundle.html +0 -42
@@ -16,6 +16,9 @@ export default class BaseTool {
16
16
  onKeyPress(_event) {
17
17
  return false;
18
18
  }
19
+ onKeyUp(_event) {
20
+ return false;
21
+ }
19
22
  setEnabled(enabled) {
20
23
  var _a;
21
24
  this.enabled = enabled;
@@ -14,7 +14,8 @@ export declare enum PanZoomMode {
14
14
  OneFingerTouchGestures = 1,
15
15
  TwoFingerTouchGestures = 2,
16
16
  RightClickDrags = 4,
17
- SinglePointerGestures = 8
17
+ SinglePointerGestures = 8,
18
+ Keyboard = 16
18
19
  }
19
20
  export default class PanZoom extends BaseTool {
20
21
  private editor;
@@ -12,6 +12,7 @@ export var PanZoomMode;
12
12
  PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
13
13
  PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
14
14
  PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
15
+ PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
15
16
  })(PanZoomMode || (PanZoomMode = {}));
16
17
  export default class PanZoom extends BaseTool {
17
18
  constructor(editor, mode, description) {
@@ -133,6 +134,9 @@ export default class PanZoom extends BaseTool {
133
134
  return true;
134
135
  }
135
136
  onKeyPress({ key }) {
137
+ if (!(this.mode & PanZoomMode.Keyboard)) {
138
+ return false;
139
+ }
136
140
  let translation = Vec2.zero;
137
141
  let scale = 1;
138
142
  let rotation = 0;
@@ -1,8 +1,9 @@
1
1
  import Command from '../commands/Command';
2
2
  import Editor from '../Editor';
3
+ import Mat33 from '../geometry/Mat33';
3
4
  import Rect2 from '../geometry/Rect2';
4
5
  import { Point2, Vec2 } from '../geometry/Vec2';
5
- import { PointerEvt } from '../types';
6
+ import { KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
6
7
  import BaseTool from './BaseTool';
7
8
  import { ToolType } from './ToolController';
8
9
  declare class Selection {
@@ -20,7 +21,8 @@ declare class Selection {
20
21
  handleResizeCornerDrag(deltaPosition: Vec2): void;
21
22
  handleRotateCircleDrag(offset: Vec2): void;
22
23
  private computeTransformCommands;
23
- finishDragging(): void;
24
+ transformPreview(transform: Mat33): void;
25
+ finalizeTransform(): void;
24
26
  private static ApplyTransformationCommand;
25
27
  private previewTransformCmds;
26
28
  appendBackgroundBoxTo(elem: HTMLElement): void;
@@ -32,6 +34,7 @@ declare class Selection {
32
34
  private recomputeBoxRotation;
33
35
  getSelectedItemCount(): number;
34
36
  updateUI(): void;
37
+ scrollTo(): void;
35
38
  deleteSelectedObjects(): Command;
36
39
  duplicateSelectedObjects(): Command;
37
40
  }
@@ -45,8 +48,12 @@ export default class SelectionTool extends BaseTool {
45
48
  onPointerDown(event: PointerEvt): boolean;
46
49
  onPointerMove(event: PointerEvt): void;
47
50
  private onGestureEnd;
51
+ private zoomToSelection;
48
52
  onPointerUp(event: PointerEvt): void;
49
53
  onGestureCancel(): void;
54
+ private static handleableKeys;
55
+ onKeyPress(event: KeyPressEvent): boolean;
56
+ onKeyUp(evt: KeyUpEvent): boolean;
50
57
  setEnabled(enabled: boolean): void;
51
58
  getSelection(): Selection | null;
52
59
  clearSelection(): void;
@@ -15,6 +15,7 @@ import Mat33 from '../geometry/Mat33';
15
15
  import Rect2 from '../geometry/Rect2';
16
16
  import { Vec2 } from '../geometry/Vec2';
17
17
  import { EditorEventType } from '../types';
18
+ import Viewport from '../Viewport';
18
19
  import BaseTool from './BaseTool';
19
20
  import { ToolType } from './ToolController';
20
21
  const handleScreenSize = 30;
@@ -143,13 +144,13 @@ class Selection {
143
144
  this.transform = Mat33.identity;
144
145
  makeDraggable(draggableBackground, (deltaPosition) => {
145
146
  this.handleBackgroundDrag(deltaPosition);
146
- }, () => this.finishDragging());
147
+ }, () => this.finalizeTransform());
147
148
  makeDraggable(resizeCorner, (deltaPosition) => {
148
149
  this.handleResizeCornerDrag(deltaPosition);
149
- }, () => this.finishDragging());
150
+ }, () => this.finalizeTransform());
150
151
  makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
151
152
  this.handleRotateCircleDrag(offset);
152
- }, () => this.finishDragging());
153
+ }, () => this.finalizeTransform());
153
154
  }
154
155
  // Note a small change in the position of this' background while dragging
155
156
  // At the end of a drag, changes should be applied by calling this.finishDragging()
@@ -159,9 +160,7 @@ class Selection {
159
160
  deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
160
161
  // Snap position to a multiple of 10 (additional decimal points lead to larger files).
161
162
  deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
162
- this.region = this.region.translatedBy(deltaPosition);
163
- this.transform = this.transform.rightMul(Mat33.translation(deltaPosition));
164
- this.previewTransformCmds();
163
+ this.transformPreview(Mat33.translation(deltaPosition));
165
164
  }
166
165
  handleResizeCornerDrag(deltaPosition) {
167
166
  deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
@@ -170,18 +169,11 @@ class Selection {
170
169
  const oldHeight = this.region.h;
171
170
  const newSize = this.region.size.plus(deltaPosition);
172
171
  if (newSize.y > 0 && newSize.x > 0) {
173
- this.region = this.region.resizedTo(newSize);
174
- const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight);
175
- const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft);
176
- this.transform = this.transform.rightMul(currentTransfm);
177
- this.previewTransformCmds();
172
+ const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight);
173
+ this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft));
178
174
  }
179
175
  }
180
176
  handleRotateCircleDrag(offset) {
181
- this.boxRotation = this.boxRotation % (2 * Math.PI);
182
- if (this.boxRotation < 0) {
183
- this.boxRotation += 2 * Math.PI;
184
- }
185
177
  let targetRotation = offset.angle();
186
178
  targetRotation = targetRotation % (2 * Math.PI);
187
179
  if (targetRotation < 0) {
@@ -198,17 +190,32 @@ class Selection {
198
190
  deltaRotation = Math.floor(Math.abs(deltaRotation) / rotationStep) * rotationStep;
199
191
  deltaRotation *= rotationDirection;
200
192
  }
201
- this.transform = this.transform.rightMul(Mat33.zRotation(deltaRotation, this.region.center));
202
- this.boxRotation += deltaRotation;
203
- this.previewTransformCmds();
193
+ this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center));
204
194
  }
205
195
  computeTransformCommands() {
206
196
  return this.selectedElems.map(elem => {
207
197
  return elem.transformBy(this.transform);
208
198
  });
209
199
  }
200
+ // Applies, previews, but doesn't finalize the given transformation.
201
+ transformPreview(transform) {
202
+ this.transform = this.transform.rightMul(transform);
203
+ const deltaRotation = transform.transformVec3(Vec2.unitX).angle();
204
+ transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center));
205
+ this.boxRotation += deltaRotation;
206
+ this.boxRotation = this.boxRotation % (2 * Math.PI);
207
+ if (this.boxRotation < 0) {
208
+ this.boxRotation += 2 * Math.PI;
209
+ }
210
+ const newSize = transform.transformVec3(this.region.size);
211
+ const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft);
212
+ this.region = this.region.resizedTo(newSize);
213
+ this.region = this.region.translatedBy(translation);
214
+ this.previewTransformCmds();
215
+ this.scrollTo();
216
+ }
210
217
  // Applies the current transformation to the selection
211
- finishDragging() {
218
+ finalizeTransform() {
212
219
  this.transformationCommands.forEach(cmd => {
213
220
  cmd.unapply(this.editor);
214
221
  });
@@ -328,6 +335,16 @@ class Selection {
328
335
  this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
329
336
  this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
330
337
  }
338
+ // Scroll the viewport to this. Does not zoom
339
+ scrollTo() {
340
+ const viewport = this.editor.viewport;
341
+ const visibleRect = viewport.visibleRect;
342
+ if (!visibleRect.containsPoint(this.region.center)) {
343
+ const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center);
344
+ const delta = this.region.center.minus(closestPoint);
345
+ this.editor.dispatchNoAnnounce(new Viewport.ViewportTransform(Mat33.translation(delta.times(-1))), false);
346
+ }
347
+ }
331
348
  deleteSelectedObjects() {
332
349
  return new Erase(this.selectedElems);
333
350
  }
@@ -384,6 +401,7 @@ export default class SelectionTool extends BaseTool {
384
401
  (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.recomputeRegion();
385
402
  (_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.updateUI();
386
403
  });
404
+ this.editor.handleKeyEventsFrom(this.handleOverlay);
387
405
  }
388
406
  onPointerDown(event) {
389
407
  if (event.allPointers.length === 1 && event.current.isPrimary) {
@@ -413,6 +431,11 @@ export default class SelectionTool extends BaseTool {
413
431
  });
414
432
  if (hasSelection) {
415
433
  this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
434
+ this.zoomToSelection();
435
+ }
436
+ }
437
+ zoomToSelection() {
438
+ if (this.selectionBox) {
416
439
  const selectionRect = this.selectionBox.region;
417
440
  this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
418
441
  }
@@ -430,12 +453,93 @@ export default class SelectionTool extends BaseTool {
430
453
  this.selectionBox = this.prevSelectionBox;
431
454
  (_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.appendBackgroundBoxTo(this.handleOverlay);
432
455
  }
456
+ onKeyPress(event) {
457
+ let rotationSteps = 0;
458
+ let xTranslateSteps = 0;
459
+ let yTranslateSteps = 0;
460
+ let xScaleSteps = 0;
461
+ let yScaleSteps = 0;
462
+ switch (event.key) {
463
+ case 'a':
464
+ case 'h':
465
+ case 'ArrowLeft':
466
+ xTranslateSteps -= 1;
467
+ break;
468
+ case 'd':
469
+ case 'l':
470
+ case 'ArrowRight':
471
+ xTranslateSteps += 1;
472
+ break;
473
+ case 'q':
474
+ case 'k':
475
+ case 'ArrowUp':
476
+ yTranslateSteps -= 1;
477
+ break;
478
+ case 'e':
479
+ case 'j':
480
+ case 'ArrowDown':
481
+ yTranslateSteps += 1;
482
+ break;
483
+ case 'r':
484
+ rotationSteps += 1;
485
+ break;
486
+ case 'R':
487
+ rotationSteps -= 1;
488
+ break;
489
+ case 'i':
490
+ xScaleSteps -= 1;
491
+ break;
492
+ case 'I':
493
+ xScaleSteps += 1;
494
+ break;
495
+ case 'o':
496
+ yScaleSteps -= 1;
497
+ break;
498
+ case 'O':
499
+ yScaleSteps += 1;
500
+ break;
501
+ }
502
+ let handled = xTranslateSteps !== 0
503
+ || yTranslateSteps !== 0
504
+ || rotationSteps !== 0
505
+ || xScaleSteps !== 0
506
+ || yScaleSteps !== 0;
507
+ if (!this.selectionBox) {
508
+ handled = false;
509
+ }
510
+ else if (handled) {
511
+ const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
512
+ const rotateStepSize = Math.PI / 8;
513
+ const scaleStepSize = translateStepSize / 2;
514
+ const region = this.selectionBox.region;
515
+ const scaledSize = this.selectionBox.region.size.plus(Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize));
516
+ const transform = Mat33.scaling2D(Vec2.of(
517
+ // Don't more-than-half the size of the selection
518
+ Math.max(0.5, scaledSize.x / region.size.x), Math.max(0.5, scaledSize.y / region.size.y)), region.topLeft).rightMul(Mat33.zRotation(rotationSteps * rotateStepSize, region.center)).rightMul(Mat33.translation(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)));
519
+ this.selectionBox.transformPreview(transform);
520
+ }
521
+ return handled;
522
+ }
523
+ onKeyUp(evt) {
524
+ if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
525
+ this.selectionBox.finalizeTransform();
526
+ return true;
527
+ }
528
+ return false;
529
+ }
433
530
  setEnabled(enabled) {
434
531
  super.setEnabled(enabled);
435
532
  // Clear the selection
436
533
  this.handleOverlay.replaceChildren();
437
534
  this.selectionBox = null;
438
535
  this.handleOverlay.style.display = enabled ? 'block' : 'none';
536
+ if (enabled) {
537
+ this.handleOverlay.tabIndex = 0;
538
+ this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts;
539
+ }
540
+ else {
541
+ this.handleOverlay.tabIndex = -1;
542
+ }
439
543
  }
440
544
  // Get the object responsible for displaying this' selection.
441
545
  getSelection() {
@@ -451,3 +555,11 @@ export default class SelectionTool extends BaseTool {
451
555
  });
452
556
  }
453
557
  }
558
+ SelectionTool.handleableKeys = [
559
+ 'a', 'h', 'ArrowLeft',
560
+ 'd', 'l', 'ArrowRight',
561
+ 'q', 'k', 'ArrowUp',
562
+ 'e', 'j', 'ArrowDown',
563
+ 'r', 'R',
564
+ 'i', 'I', 'o', 'O',
565
+ ];
@@ -23,6 +23,7 @@ export default class ToolController {
23
23
  constructor(editor, localization) {
24
24
  const primaryToolEnabledGroup = new ToolEnabledGroup();
25
25
  const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
26
+ const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
26
27
  const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
27
28
  const primaryTools = [
28
29
  new SelectionTool(editor, localization.selectionTool),
@@ -38,6 +39,7 @@ export default class ToolController {
38
39
  new PipetteTool(editor, localization.pipetteTool),
39
40
  panZoomTool,
40
41
  ...primaryTools,
42
+ keyboardPanZoomTool,
41
43
  new UndoRedoShortcut(editor),
42
44
  ];
43
45
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
@@ -76,8 +78,9 @@ export default class ToolController {
76
78
  this.activeTool = null;
77
79
  handled = true;
78
80
  }
79
- else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent) {
81
+ else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent) {
80
82
  const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
83
+ const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent;
81
84
  const isWheelEvt = event.kind === InputEvtType.WheelEvt;
82
85
  for (const tool of this.tools) {
83
86
  if (!tool.isEnabled()) {
@@ -85,7 +88,8 @@ export default class ToolController {
85
88
  }
86
89
  const wheelResult = isWheelEvt && tool.onWheel(event);
87
90
  const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
88
- handled = keyPressResult || wheelResult;
91
+ const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event);
92
+ handled = keyPressResult || wheelResult || keyReleaseResult;
89
93
  if (handled) {
90
94
  break;
91
95
  }
@@ -1,4 +1,5 @@
1
1
  export interface ToolLocalization {
2
+ keyboardPanZoom: string;
2
3
  penTool: (penId: number) => string;
3
4
  selectionTool: string;
4
5
  eraserTool: string;
@@ -7,6 +7,7 @@ export const defaultToolLocalization = {
7
7
  undoRedoTool: 'Undo/Redo',
8
8
  rightClickDragPanTool: 'Right-click drag',
9
9
  pipetteTool: 'Pick color from screen',
10
+ keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
10
11
  textTool: 'Text',
11
12
  enterTextToInsert: 'Text to insert',
12
13
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
@@ -19,7 +19,8 @@ export declare enum InputEvtType {
19
19
  PointerUpEvt = 2,
20
20
  GestureCancelEvt = 3,
21
21
  WheelEvt = 4,
22
- KeyPressEvent = 5
22
+ KeyPressEvent = 5,
23
+ KeyUpEvent = 6
23
24
  }
24
25
  export interface WheelEvt {
25
26
  readonly kind: InputEvtType.WheelEvt;
@@ -31,6 +32,11 @@ export interface KeyPressEvent {
31
32
  readonly key: string;
32
33
  readonly ctrlKey: boolean;
33
34
  }
35
+ export interface KeyUpEvent {
36
+ readonly kind: InputEvtType.KeyUpEvent;
37
+ readonly key: string;
38
+ readonly ctrlKey: boolean;
39
+ }
34
40
  export interface GestureCancelEvt {
35
41
  readonly kind: InputEvtType.GestureCancelEvt;
36
42
  }
@@ -48,7 +54,7 @@ export interface PointerUpEvt extends PointerEvtBase {
48
54
  readonly kind: InputEvtType.PointerUpEvt;
49
55
  }
50
56
  export declare type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
51
- export declare type InputEvt = KeyPressEvent | WheelEvt | GestureCancelEvt | PointerEvt;
57
+ export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt;
52
58
  export declare type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
53
59
  export declare enum EditorEventType {
54
60
  ToolEnabled = 0,
package/dist/src/types.js CHANGED
@@ -7,6 +7,7 @@ export var InputEvtType;
7
7
  InputEvtType[InputEvtType["GestureCancelEvt"] = 3] = "GestureCancelEvt";
8
8
  InputEvtType[InputEvtType["WheelEvt"] = 4] = "WheelEvt";
9
9
  InputEvtType[InputEvtType["KeyPressEvent"] = 5] = "KeyPressEvent";
10
+ InputEvtType[InputEvtType["KeyUpEvent"] = 6] = "KeyUpEvent";
10
11
  })(InputEvtType || (InputEvtType = {}));
11
12
  export var EditorEventType;
12
13
  (function (EditorEventType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
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/Editor.js",
6
6
  "types": "dist/src/Editor.d.ts",
@@ -9,6 +9,14 @@
9
9
  "types": "./dist/src/Editor.d.ts",
10
10
  "default": "./dist/src/Editor.js"
11
11
  },
12
+ "./localizations/getLocalizationTable": {
13
+ "types": "./dist/src/localizations/getLocalizationTable.d.ts",
14
+ "default": "./dist/src/localizations/getLocalizationTable.js"
15
+ },
16
+ "./getLocalizationTable": {
17
+ "types": "./dist/src/localizations/getLocalizationTable.d.ts",
18
+ "default": "./dist/src/localizations/getLocalizationTable.js"
19
+ },
12
20
  "./styles": {
13
21
  "default": "./src/styles.js"
14
22
  },
package/src/Editor.ts CHANGED
@@ -17,7 +17,8 @@ import SVGLoader from './SVGLoader';
17
17
  import Pointer from './Pointer';
18
18
  import Mat33 from './geometry/Mat33';
19
19
  import Rect2 from './geometry/Rect2';
20
- import { defaultEditorLocalization, EditorLocalization } from './localization';
20
+ import { EditorLocalization } from './localization';
21
+ import getLocalizationTable from './localizations/getLocalizationTable';
21
22
 
22
23
  export interface EditorSettings {
23
24
  // Defaults to RenderingMode.CanvasRenderer
@@ -43,7 +44,7 @@ export class Editor {
43
44
 
44
45
  // Viewport for the exported/imported image
45
46
  private importExportViewport: Viewport;
46
- public localization: EditorLocalization = defaultEditorLocalization;
47
+ public localization: EditorLocalization;
47
48
 
48
49
  public viewport: Viewport;
49
50
  public toolController: ToolController;
@@ -59,7 +60,7 @@ export class Editor {
59
60
  settings: Partial<EditorSettings> = {},
60
61
  ) {
61
62
  this.localization = {
62
- ...this.localization,
63
+ ...getLocalizationTable(),
63
64
  ...settings.localization,
64
65
  };
65
66
 
@@ -238,17 +239,7 @@ export class Editor {
238
239
  pointerEnd(evt);
239
240
  });
240
241
 
241
- this.renderingRegion.addEventListener('keydown', evt => {
242
- if (this.toolController.dispatchInputEvent({
243
- kind: InputEvtType.KeyPressEvent,
244
- key: evt.key,
245
- ctrlKey: evt.ctrlKey,
246
- })) {
247
- evt.preventDefault();
248
- } else if (evt.key === 'Escape') {
249
- this.renderingRegion.blur();
250
- }
251
- });
242
+ this.handleKeyEventsFrom(this.renderingRegion);
252
243
 
253
244
  this.container.addEventListener('wheel', evt => {
254
245
  let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
@@ -307,6 +298,32 @@ export class Editor {
307
298
  });
308
299
  }
309
300
 
301
+ // Adds event listners for keypresses to [elem] and forwards those events to the
302
+ // editor.
303
+ public handleKeyEventsFrom(elem: HTMLElement) {
304
+ elem.addEventListener('keydown', evt => {
305
+ if (this.toolController.dispatchInputEvent({
306
+ kind: InputEvtType.KeyPressEvent,
307
+ key: evt.key,
308
+ ctrlKey: evt.ctrlKey,
309
+ })) {
310
+ evt.preventDefault();
311
+ } else if (evt.key === 'Escape') {
312
+ this.renderingRegion.blur();
313
+ }
314
+ });
315
+
316
+ elem.addEventListener('keyup', evt => {
317
+ if (this.toolController.dispatchInputEvent({
318
+ kind: InputEvtType.KeyUpEvent,
319
+ key: evt.key,
320
+ ctrlKey: evt.ctrlKey,
321
+ })) {
322
+ evt.preventDefault();
323
+ }
324
+ });
325
+ }
326
+
310
327
  // Adds to history by default
311
328
  public dispatch(command: Command, addToHistory: boolean = true) {
312
329
  if (addToHistory) {
@@ -391,6 +408,11 @@ export class Editor {
391
408
  public rerender(showImageBounds: boolean = true) {
392
409
  this.display.startRerender();
393
410
 
411
+ // Don't render if the display has zero size.
412
+ if (this.display.width === 0 || this.display.height === 0) {
413
+ return;
414
+ }
415
+
394
416
  // Draw a rectangle around the region that will be visible on save
395
417
  const renderer = this.display.getDryInkRenderer();
396
418
 
@@ -48,7 +48,7 @@ describe('EditorImage', () => {
48
48
  expect(renderer.objectNestingLevel).toBe(0);
49
49
  editor.dispatch(addTestStrokeCommand);
50
50
  editor.rerender();
51
- expect(renderer.renderedPathCount - emptyDocumentPathCount).toBe(1);
51
+ expect(renderer.renderedPathCount - emptyDocumentPathCount).toBeGreaterThanOrEqual(1);
52
52
 
53
53
  // Should not be within objects after finished rendering
54
54
  expect(renderer.objectNestingLevel).toBe(0);
@@ -32,6 +32,10 @@ class UndoRedoHistory {
32
32
  command.apply(this.editor);
33
33
  }
34
34
  this.undoStack.push(command);
35
+
36
+ for (const elem of this.redoStack) {
37
+ elem.onDrop(this.editor);
38
+ }
35
39
  this.redoStack = [];
36
40
  this.fireUpdateEvent();
37
41
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import '../styles';
4
4
  import Editor from '../Editor';
5
+ import getLocalizationTable from '../localizations/getLocalizationTable';
5
6
 
6
7
  export default Editor;
7
- export { Editor };
8
+ export { Editor, getLocalizationTable };
@@ -1,3 +1,4 @@
1
+ import Mat33 from '../../geometry/Mat33';
1
2
  import Path from '../../geometry/Path';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
@@ -7,18 +8,22 @@ import AbstractComponent from '../AbstractComponent';
7
8
  import Stroke from '../Stroke';
8
9
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
9
10
 
10
- export const makeFilledRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
11
- return new RectangleBuilder(initialPoint, true);
11
+ export const makeFilledRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
12
+ return new RectangleBuilder(initialPoint, true, viewport);
12
13
  };
13
14
 
14
- export const makeOutlinedRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, _viewport: Viewport) => {
15
- return new RectangleBuilder(initialPoint, false);
15
+ export const makeOutlinedRectangleBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
16
+ return new RectangleBuilder(initialPoint, false, viewport);
16
17
  };
17
18
 
18
19
  export default class RectangleBuilder implements ComponentBuilder {
19
20
  private endPoint: StrokeDataPoint;
20
21
 
21
- public constructor(private readonly startPoint: StrokeDataPoint, private filled: boolean) {
22
+ public constructor(
23
+ private readonly startPoint: StrokeDataPoint,
24
+ private filled: boolean,
25
+ private viewport: Viewport,
26
+ ) {
22
27
  // Initially, the start and end points are the same.
23
28
  this.endPoint = startPoint;
24
29
  }
@@ -29,11 +34,21 @@ export default class RectangleBuilder implements ComponentBuilder {
29
34
  }
30
35
 
31
36
  private buildPreview(): Stroke {
32
- const startPoint = this.startPoint.pos;
33
- const endPoint = this.endPoint.pos;
37
+ const canvasAngle = this.viewport.getRotationAngle();
38
+ const rotationMat = Mat33.zRotation(-canvasAngle);
39
+
40
+ // Adjust startPoint and endPoint such that applying [rotationMat] to them
41
+ // brings them to this.startPoint and this.endPoint.
42
+ const startPoint = rotationMat.inverse().transformVec2(this.startPoint.pos);
43
+ const endPoint = rotationMat.inverse().transformVec2(this.endPoint.pos);
44
+
45
+ const rect = Rect2.fromCorners(startPoint, endPoint);
34
46
  const path = Path.fromRect(
35
- Rect2.fromCorners(startPoint, endPoint),
47
+ rect,
36
48
  this.filled ? null : this.endPoint.width,
49
+ ).transformedBy(
50
+ // Rotate the canvas rectangle so that its rotation matches the screen
51
+ rotationMat
37
52
  );
38
53
 
39
54
  const preview = new Stroke([
@@ -74,4 +74,19 @@ describe('Line2', () => {
74
74
  expect(line1.intersection(line2)).toBeNull();
75
75
  expect(line2.intersection(line1)).toBeNull();
76
76
  });
77
+
78
+ it('Closest point to (0,0) on the line x = 1 should be (1,0)', () => {
79
+ const line = new LineSegment2(Vec2.of(1, 100), Vec2.of(1, -100));
80
+ expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
81
+ });
82
+
83
+ it('Closest point from (-1,2) to segment((1,1) -> (2,4)) should be (1,1)', () => {
84
+ const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
85
+ expect(line.closestPointTo(Vec2.of(-1, 2))).objEq(Vec2.of(1, 1));
86
+ });
87
+
88
+ it('Closest point from (5,2) to segment((1,1) -> (2,4)) should be (2,4)', () => {
89
+ const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
90
+ expect(line.closestPointTo(Vec2.of(5, 2))).objEq(Vec2.of(2, 4));
91
+ });
77
92
  });
@@ -7,6 +7,7 @@ interface IntersectionResult {
7
7
  }
8
8
 
9
9
  export default class LineSegment2 {
10
+ // invariant: ||direction|| = 1
10
11
  public readonly direction: Vec2;
11
12
  public readonly length: number;
12
13
  public readonly bbox;
@@ -124,4 +125,23 @@ export default class LineSegment2 {
124
125
  t: resultT,
125
126
  };
126
127
  }
128
+
129
+ // Returns the closest point on this to [target]
130
+ public closestPointTo(target: Point2) {
131
+ // Distance from P1 along this' direction.
132
+ const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
133
+ const projectedDistFromP2 = this.length - projectedDistFromP1;
134
+
135
+ const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
136
+
137
+ if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
138
+ return projection;
139
+ }
140
+
141
+ if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
142
+ return this.p2;
143
+ } else {
144
+ return this.p1;
145
+ }
146
+ }
127
147
  }