js-draw 1.6.0 → 1.7.0

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 (65) hide show
  1. package/README.md +4 -6
  2. package/dist/Editor.css +30 -4
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +5 -0
  6. package/dist/cjs/Editor.js +53 -70
  7. package/dist/cjs/components/BackgroundComponent.js +6 -1
  8. package/dist/cjs/components/TextComponent.d.ts +1 -1
  9. package/dist/cjs/components/TextComponent.js +19 -12
  10. package/dist/cjs/image/EditorImage.js +8 -8
  11. package/dist/cjs/localization.d.ts +2 -0
  12. package/dist/cjs/localization.js +2 -0
  13. package/dist/cjs/localizations/comments.js +1 -0
  14. package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
  15. package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  16. package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
  17. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  18. package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
  19. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
  20. package/dist/cjs/toolbar/widgets/BaseWidget.js +3 -3
  21. package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
  22. package/dist/cjs/tools/SelectionTool/Selection.js +81 -52
  23. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  24. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
  25. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  26. package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
  27. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  28. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  29. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  30. package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
  31. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  32. package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
  33. package/dist/cjs/version.js +1 -1
  34. package/dist/mjs/Editor.d.ts +5 -0
  35. package/dist/mjs/Editor.mjs +53 -70
  36. package/dist/mjs/components/BackgroundComponent.mjs +6 -1
  37. package/dist/mjs/components/TextComponent.d.ts +1 -1
  38. package/dist/mjs/components/TextComponent.mjs +19 -12
  39. package/dist/mjs/image/EditorImage.mjs +8 -8
  40. package/dist/mjs/localization.d.ts +2 -0
  41. package/dist/mjs/localization.mjs +2 -0
  42. package/dist/mjs/localizations/comments.mjs +1 -0
  43. package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
  44. package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  45. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
  46. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  47. package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
  48. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
  49. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +3 -3
  50. package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
  51. package/dist/mjs/tools/SelectionTool/Selection.mjs +81 -52
  52. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  53. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
  54. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  55. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
  56. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  57. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  58. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  59. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
  60. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  61. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
  62. package/dist/mjs/version.mjs +1 -1
  63. package/docs/img/readme-images/js-draw.png +0 -0
  64. package/package.json +6 -6
  65. package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const math_1 = require("@js-draw/math");
7
+ const untilNextAnimationFrame_1 = __importDefault(require("../../util/untilNextAnimationFrame"));
8
+ /**
9
+ * Automatically scrolls the viewport such that the user's pointer is visible.
10
+ */
11
+ class ToPointerAutoscroller {
12
+ constructor(viewport, scrollByCanvasDelta) {
13
+ this.viewport = viewport;
14
+ this.scrollByCanvasDelta = scrollByCanvasDelta;
15
+ this.started = false;
16
+ this.updateLoopId = 0;
17
+ this.updateLoopRunning = false;
18
+ this.targetPoint = null;
19
+ this.scrollRate = 1000; // px/s
20
+ }
21
+ getScrollForPoint(screenPoint) {
22
+ const screenSize = this.viewport.getScreenRectSize();
23
+ const screenRect = new math_1.Rect2(0, 0, screenSize.x, screenSize.y);
24
+ // Starts autoscrolling when the cursor is **outside of** this region
25
+ const marginSize = 44;
26
+ const autoscrollBoundary = screenRect.grownBy(-marginSize);
27
+ if (autoscrollBoundary.containsPoint(screenPoint)) {
28
+ return math_1.Vec2.zero;
29
+ }
30
+ const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
31
+ const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
32
+ const toEdge = closestEdgePoint.minus(screenPoint);
33
+ // Go faster for points further away from the boundary.
34
+ const maximumScaleFactor = 1.25;
35
+ const scaleFactor = Math.min(distToEdge / marginSize, maximumScaleFactor);
36
+ return toEdge.normalizedOrZero().times(scaleFactor);
37
+ }
38
+ start() {
39
+ this.started = true;
40
+ }
41
+ onPointerMove(pointerScreenPosition) {
42
+ if (!this.started) {
43
+ return;
44
+ }
45
+ if (this.getScrollForPoint(pointerScreenPosition) === math_1.Vec2.zero) {
46
+ this.stopUpdateLoop();
47
+ }
48
+ else {
49
+ this.targetPoint = pointerScreenPosition;
50
+ this.startUpdateLoop();
51
+ }
52
+ }
53
+ stop() {
54
+ this.targetPoint = null;
55
+ this.started = false;
56
+ this.stopUpdateLoop();
57
+ }
58
+ startUpdateLoop() {
59
+ if (this.updateLoopRunning) {
60
+ return;
61
+ }
62
+ (async () => {
63
+ this.updateLoopId++;
64
+ const currentUpdateLoopId = this.updateLoopId;
65
+ let lastUpdateTime = performance.now();
66
+ while (this.updateLoopId === currentUpdateLoopId && this.targetPoint) {
67
+ this.updateLoopRunning = true;
68
+ const currentTime = performance.now();
69
+ const deltaTimeMs = currentTime - lastUpdateTime;
70
+ const scrollDirection = this.getScrollForPoint(this.targetPoint);
71
+ const screenScrollAmount = scrollDirection.times(this.scrollRate * deltaTimeMs / 1000);
72
+ this.scrollByCanvasDelta(this.viewport.screenToCanvasTransform.transformVec3(screenScrollAmount));
73
+ lastUpdateTime = currentTime;
74
+ await (0, untilNextAnimationFrame_1.default)();
75
+ }
76
+ this.updateLoopRunning = false;
77
+ })();
78
+ }
79
+ stopUpdateLoop() {
80
+ this.updateLoopId++;
81
+ }
82
+ }
83
+ exports.default = ToPointerAutoscroller;
@@ -9,26 +9,33 @@ export declare class DragTransformer {
9
9
  constructor(editor: Editor, selection: Selection);
10
10
  onDragStart(startPoint: Vec3): void;
11
11
  onDragUpdate(canvasPos: Vec3): void;
12
- onDragEnd(): void;
12
+ onDragEnd(): void | Promise<void>;
13
13
  }
14
14
  export declare class ResizeTransformer {
15
15
  private readonly editor;
16
16
  private selection;
17
17
  private mode;
18
18
  private dragStartPoint;
19
+ private transformOrigin;
20
+ private scaleRate;
19
21
  constructor(editor: Editor, selection: Selection);
20
22
  onDragStart(startPoint: Vec3, mode: ResizeMode): void;
23
+ private computeOriginAndScaleRate;
21
24
  onDragUpdate(canvasPos: Vec3): void;
22
- onDragEnd(): void;
25
+ onDragEnd(): void | Promise<void>;
23
26
  }
24
27
  export declare class RotateTransformer {
25
28
  private readonly editor;
26
29
  private selection;
27
30
  private startAngle;
31
+ private targetRotation;
32
+ private maximumDistFromStart;
33
+ private startPoint;
28
34
  constructor(editor: Editor, selection: Selection);
29
35
  private getAngle;
30
36
  private roundAngle;
31
37
  onDragStart(startPoint: Vec3): void;
38
+ private setRotationTo;
32
39
  onDragUpdate(canvasPos: Vec3): void;
33
- onDragEnd(): void;
40
+ onDragEnd(): void | Promise<void>;
34
41
  }
@@ -21,7 +21,7 @@ class DragTransformer {
21
21
  this.selection.setTransform(math_1.Mat33.translation(delta));
22
22
  }
23
23
  onDragEnd() {
24
- this.selection.finalizeTransform();
24
+ return this.selection.finalizeTransform();
25
25
  }
26
26
  }
27
27
  exports.DragTransformer = DragTransformer;
@@ -35,6 +35,32 @@ class ResizeTransformer {
35
35
  this.selection.setTransform(math_1.Mat33.identity);
36
36
  this.mode = mode;
37
37
  this.dragStartPoint = startPoint;
38
+ this.computeOriginAndScaleRate();
39
+ }
40
+ computeOriginAndScaleRate() {
41
+ // Store the index of the furthest corner from startPoint. We'll use that
42
+ // to determine where the transform considers (0, 0) (where we scale from).
43
+ const selectionRect = this.selection.preTransformRegion;
44
+ const selectionBoxCorners = selectionRect.corners;
45
+ let largestDistSquared = 0;
46
+ for (let i = 0; i < selectionBoxCorners.length; i++) {
47
+ const currentCorner = selectionBoxCorners[i];
48
+ const distSquaredToCurrent = this.dragStartPoint.minus(currentCorner).magnitudeSquared();
49
+ if (distSquaredToCurrent > largestDistSquared) {
50
+ largestDistSquared = distSquaredToCurrent;
51
+ this.transformOrigin = currentCorner;
52
+ }
53
+ }
54
+ // Determine whether moving the mouse to the right increases or decreases the width.
55
+ let widthScaleRate = 1;
56
+ let heightScaleRate = 1;
57
+ if (this.transformOrigin.x > selectionRect.center.x) {
58
+ widthScaleRate = -1;
59
+ }
60
+ if (this.transformOrigin.y > selectionRect.center.y) {
61
+ heightScaleRate = -1;
62
+ }
63
+ this.scaleRate = math_1.Vec2.of(widthScaleRate, heightScaleRate);
38
64
  }
39
65
  onDragUpdate(canvasPos) {
40
66
  const canvasDelta = canvasPos.minus(this.dragStartPoint);
@@ -42,11 +68,11 @@ class ResizeTransformer {
42
68
  const origHeight = this.selection.preTransformRegion.height;
43
69
  let scale = math_1.Vec2.of(1, 1);
44
70
  if (this.mode === types_1.ResizeMode.HorizontalOnly) {
45
- const newWidth = origWidth + canvasDelta.x;
71
+ const newWidth = origWidth + canvasDelta.x * this.scaleRate.x;
46
72
  scale = math_1.Vec2.of(newWidth / origWidth, scale.y);
47
73
  }
48
74
  if (this.mode === types_1.ResizeMode.VerticalOnly) {
49
- const newHeight = origHeight + canvasDelta.y;
75
+ const newHeight = origHeight + canvasDelta.y * this.scaleRate.y;
50
76
  scale = math_1.Vec2.of(scale.x, newHeight / origHeight);
51
77
  }
52
78
  if (this.mode === types_1.ResizeMode.Both) {
@@ -58,12 +84,12 @@ class ResizeTransformer {
58
84
  // long decimal representations => large file sizes.
59
85
  scale = scale.map(component => Viewport_1.default.roundScaleRatio(component, 2));
60
86
  if (scale.x !== 0 && scale.y !== 0) {
61
- const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
87
+ const origin = this.editor.viewport.roundPoint(this.transformOrigin);
62
88
  this.selection.setTransform(math_1.Mat33.scaling2D(scale, origin));
63
89
  }
64
90
  }
65
91
  onDragEnd() {
66
- this.selection.finalizeTransform();
92
+ return this.selection.finalizeTransform();
67
93
  }
68
94
  }
69
95
  exports.ResizeTransformer = ResizeTransformer;
@@ -72,6 +98,8 @@ class RotateTransformer {
72
98
  this.editor = editor;
73
99
  this.selection = selection;
74
100
  this.startAngle = 0;
101
+ this.targetRotation = 0;
102
+ this.maximumDistFromStart = 0;
75
103
  }
76
104
  getAngle(canvasPoint) {
77
105
  const selectionCenter = this.selection.preTransformRegion.center;
@@ -84,14 +112,16 @@ class RotateTransformer {
84
112
  return Math.round(angle * roundingFactor) / roundingFactor;
85
113
  }
86
114
  onDragStart(startPoint) {
115
+ this.startPoint = startPoint;
87
116
  this.selection.setTransform(math_1.Mat33.identity);
88
117
  this.startAngle = this.getAngle(startPoint);
118
+ this.maximumDistFromStart = 0;
119
+ this.targetRotation = 0;
89
120
  }
90
- onDragUpdate(canvasPos) {
91
- const targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
121
+ setRotationTo(angle) {
92
122
  // Transform in canvas space
93
123
  const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center);
94
- const unrounded = math_1.Mat33.zRotation(targetRotation);
124
+ const unrounded = math_1.Mat33.zRotation(angle);
95
125
  const roundedRotationTransform = unrounded.mapEntries(entry => Viewport_1.default.roundScaleRatio(entry));
96
126
  const fullRoundedTransform = math_1.Mat33
97
127
  .translation(canvasSelCenter)
@@ -99,8 +129,21 @@ class RotateTransformer {
99
129
  .rightMul(math_1.Mat33.translation(canvasSelCenter.times(-1)));
100
130
  this.selection.setTransform(fullRoundedTransform);
101
131
  }
132
+ onDragUpdate(canvasPos) {
133
+ this.targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
134
+ this.setRotationTo(this.targetRotation);
135
+ const distFromStart = canvasPos.minus(this.startPoint).magnitude();
136
+ if (distFromStart > this.maximumDistFromStart) {
137
+ this.maximumDistFromStart = distFromStart;
138
+ }
139
+ }
102
140
  onDragEnd() {
103
- this.selection.finalizeTransform();
141
+ // Anything less than this is considered a click
142
+ const clickThreshold = 15;
143
+ if (this.maximumDistFromStart < clickThreshold && this.targetRotation === 0) {
144
+ this.setRotationTo(-Math.PI / 2);
145
+ }
146
+ return this.selection.finalizeTransform();
104
147
  }
105
148
  }
106
149
  exports.RotateTransformer = RotateTransformer;
@@ -0,0 +1,16 @@
1
+ interface Callbacks {
2
+ filter(event: KeyboardEvent): boolean;
3
+ handleKeyDown(event: KeyboardEvent): void;
4
+ handleKeyUp(event: KeyboardEvent): void;
5
+ }
6
+ /**
7
+ * Calls `callbacks` when different keys are known to be pressed.
8
+ *
9
+ * `filter` can be used to ignore events.
10
+ *
11
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
12
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
13
+ * events). Artifical events are created for these changes and sent to `callbacks`.
14
+ */
15
+ declare const listenForKeyboardEventsFrom: (elem: HTMLElement, callbacks: Callbacks) => void;
16
+ export default listenForKeyboardEventsFrom;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Calls `callbacks` when different keys are known to be pressed.
5
+ *
6
+ * `filter` can be used to ignore events.
7
+ *
8
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
9
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
10
+ * events). Artifical events are created for these changes and sent to `callbacks`.
11
+ */
12
+ const listenForKeyboardEventsFrom = (elem, callbacks) => {
13
+ // Track which keys are down so we can release them when the element
14
+ // loses focus. This is particularly important for keys like Control
15
+ // that can trigger shortcuts that cause the editor to lose focus before
16
+ // the keyup event is triggered.
17
+ let keysDown = [];
18
+ // Return whether two objects that are similar to keyboard events represent the
19
+ // same key.
20
+ const keyEventsMatch = (a, b) => {
21
+ return a.key === b.key && a.code === b.code;
22
+ };
23
+ const isKeyDown = (keyEvent) => {
24
+ return keysDown.some(other => keyEventsMatch(other, keyEvent));
25
+ };
26
+ const keyEventToRecord = (event) => {
27
+ return {
28
+ code: event.code,
29
+ key: event.key,
30
+ ctrlKey: event.ctrlKey,
31
+ altKey: event.altKey,
32
+ shiftKey: event.shiftKey,
33
+ metaKey: event.metaKey,
34
+ };
35
+ };
36
+ const handleKeyEvent = (htmlEvent) => {
37
+ if (htmlEvent.type === 'keydown') {
38
+ // Add event to the list of keys that are down (so long as it
39
+ // isn't a duplicate).
40
+ if (!isKeyDown(htmlEvent)) {
41
+ // Destructructring, then pushing seems to cause
42
+ // data loss. Copy properties individually:
43
+ keysDown.push(keyEventToRecord(htmlEvent));
44
+ }
45
+ if (!callbacks.filter(htmlEvent)) {
46
+ return;
47
+ }
48
+ callbacks.handleKeyDown(htmlEvent);
49
+ }
50
+ else { // keyup
51
+ console.assert(htmlEvent.type === 'keyup');
52
+ // Remove the key from keysDown -- it's no longer down.
53
+ keysDown = keysDown.filter(event => {
54
+ const matches = keyEventsMatch(event, htmlEvent);
55
+ return !matches;
56
+ });
57
+ if (!callbacks.filter(htmlEvent)) {
58
+ return;
59
+ }
60
+ callbacks.handleKeyUp(htmlEvent);
61
+ }
62
+ };
63
+ elem.addEventListener('keydown', htmlEvent => {
64
+ handleKeyEvent(htmlEvent);
65
+ });
66
+ elem.addEventListener('keyup', htmlEvent => {
67
+ handleKeyEvent(htmlEvent);
68
+ });
69
+ elem.addEventListener('focusout', (focusEvent) => {
70
+ const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
71
+ if (!stillHasFocus) {
72
+ for (const event of keysDown) {
73
+ callbacks.handleKeyUp(new KeyboardEvent('keyup', {
74
+ ...event,
75
+ }));
76
+ }
77
+ keysDown = [];
78
+ }
79
+ });
80
+ const fireArtificalEventsBasedOn = (htmlEvent) => {
81
+ let wasShiftDown = false;
82
+ let wasCtrlDown = false;
83
+ let wasAltDown = false;
84
+ let wasMetaDown = false;
85
+ for (const otherEvent of keysDown) {
86
+ const code = otherEvent.code;
87
+ wasShiftDown ||= !!code.match(/^Shift(Left|Right)$/);
88
+ wasCtrlDown ||= !!code.match(/^Control(Left|Right)$/);
89
+ wasAltDown ||= !!code.match(/^Alt(Left|Right)$/);
90
+ wasMetaDown ||= !!code.match(/^Meta(Left|Right)$/);
91
+ }
92
+ const eventName = (isDown) => {
93
+ if (isDown) {
94
+ return 'keydown';
95
+ }
96
+ else {
97
+ return 'keyup';
98
+ }
99
+ };
100
+ const eventInitDefaults = {
101
+ shiftKey: htmlEvent.shiftKey,
102
+ altKey: htmlEvent.altKey,
103
+ metaKey: htmlEvent.metaKey,
104
+ ctrlKey: htmlEvent.ctrlKey,
105
+ };
106
+ if (htmlEvent.shiftKey !== wasShiftDown) {
107
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.shiftKey), {
108
+ ...eventInitDefaults,
109
+ key: 'Shift',
110
+ code: 'ShiftLeft',
111
+ }));
112
+ }
113
+ if (htmlEvent.altKey !== wasAltDown) {
114
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.altKey), {
115
+ ...eventInitDefaults,
116
+ key: 'Alt',
117
+ code: 'AltLeft',
118
+ }));
119
+ }
120
+ if (htmlEvent.ctrlKey !== wasCtrlDown) {
121
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.ctrlKey), {
122
+ ...eventInitDefaults,
123
+ key: 'Control',
124
+ code: 'ControlLeft',
125
+ }));
126
+ }
127
+ if (htmlEvent.metaKey !== wasMetaDown) {
128
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.metaKey), {
129
+ ...eventInitDefaults,
130
+ key: 'Meta',
131
+ code: 'MetaLeft',
132
+ }));
133
+ }
134
+ };
135
+ elem.addEventListener('mousedown', (htmlEvent) => {
136
+ fireArtificalEventsBasedOn(htmlEvent);
137
+ });
138
+ elem.addEventListener('mousemove', (htmlEvent) => {
139
+ fireArtificalEventsBasedOn(htmlEvent);
140
+ });
141
+ };
142
+ exports.default = listenForKeyboardEventsFrom;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.6.0',
4
+ number: '1.7.0',
5
5
  };
@@ -70,6 +70,7 @@ export interface EditorSettings {
70
70
  */
71
71
  appInfo: {
72
72
  name: string;
73
+ description?: string;
73
74
  version?: string;
74
75
  } | null;
75
76
  }
@@ -271,6 +272,10 @@ export declare class Editor {
271
272
  /** Remove all event listeners registered by this function. */
272
273
  remove: () => void;
273
274
  };
275
+ /** @internal */
276
+ protected handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent): void;
277
+ /** @internal */
278
+ protected handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent): void;
274
279
  /**
275
280
  * Adds event listners for keypresses (and drop events) on `elem` and forwards those
276
281
  * events to the editor.
@@ -27,6 +27,7 @@ import makeAboutDialog from './dialogs/makeAboutDialog.mjs';
27
27
  import version from './version.mjs';
28
28
  import { editorImageToSVGSync, editorImageToSVGAsync } from './image/export/editorImageToSVG.mjs';
29
29
  import { MutableReactiveValue } from './util/ReactiveValue.mjs';
30
+ import listenForKeyboardEventsFrom from './util/listenForKeyboardEventsFrom.mjs';
30
31
  /**
31
32
  * The main entrypoint for the full editor.
32
33
  *
@@ -592,6 +593,29 @@ export class Editor {
592
593
  return sendToEditor;
593
594
  }, otherEventsFilter);
594
595
  }
596
+ /** @internal */
597
+ handleHTMLKeyDownEvent(htmlEvent) {
598
+ console.assert(htmlEvent.type === 'keydown', `handling a keydown event with type ${htmlEvent.type}`);
599
+ const event = keyPressEventFromHTMLEvent(htmlEvent);
600
+ if (this.toolController.dispatchInputEvent(event)) {
601
+ htmlEvent.preventDefault();
602
+ }
603
+ else if (event.key === 't' || event.key === 'T') {
604
+ htmlEvent.preventDefault();
605
+ this.display.rerenderAsText();
606
+ }
607
+ else if (event.key === 'Escape') {
608
+ this.renderingRegion.blur();
609
+ }
610
+ }
611
+ /** @internal */
612
+ handleHTMLKeyUpEvent(htmlEvent) {
613
+ console.assert(htmlEvent.type === 'keyup', `Handling a keyup event with type ${htmlEvent.type}`);
614
+ const event = keyUpEventFromHTMLEvent(htmlEvent);
615
+ if (this.toolController.dispatchInputEvent(event)) {
616
+ htmlEvent.preventDefault();
617
+ }
618
+ }
595
619
  /**
596
620
  * Adds event listners for keypresses (and drop events) on `elem` and forwards those
597
621
  * events to the editor.
@@ -600,62 +624,14 @@ export class Editor {
600
624
  * passed to the editor.
601
625
  */
602
626
  handleKeyEventsFrom(elem, filter = () => true) {
603
- // Track which keys are down so we can release them when the element
604
- // loses focus. This is particularly important for keys like Control
605
- // that can trigger shortcuts that cause the editor to lose focus before
606
- // the keyup event is triggered.
607
- let keysDown = [];
608
- // Return whether two objects that are similar to keyboard events represent the
609
- // same key.
610
- const keyEventsMatch = (a, b) => {
611
- return a.key === b.key && a.code === b.code;
612
- };
613
- elem.addEventListener('keydown', htmlEvent => {
614
- if (!filter(htmlEvent)) {
615
- return;
616
- }
617
- const event = keyPressEventFromHTMLEvent(htmlEvent);
618
- // Add event to the list of keys that are down (so long as it
619
- // isn't a duplicate).
620
- if (!keysDown.some(other => keyEventsMatch(other, event))) {
621
- keysDown.push(event);
622
- }
623
- if (event.key === 't' || event.key === 'T') {
624
- htmlEvent.preventDefault();
625
- this.display.rerenderAsText();
626
- }
627
- else if (this.toolController.dispatchInputEvent(event)) {
628
- htmlEvent.preventDefault();
629
- }
630
- else if (event.key === 'Escape') {
631
- this.renderingRegion.blur();
632
- }
633
- });
634
- elem.addEventListener('keyup', htmlEvent => {
635
- // Remove the key from keysDown -- it's no longer down.
636
- keysDown = keysDown.filter(event => {
637
- const matches = keyEventsMatch(event, htmlEvent);
638
- return !matches;
639
- });
640
- if (!filter(htmlEvent)) {
641
- return;
642
- }
643
- const event = keyUpEventFromHTMLEvent(htmlEvent);
644
- if (this.toolController.dispatchInputEvent(event)) {
645
- htmlEvent.preventDefault();
646
- }
647
- });
648
- elem.addEventListener('focusout', (event) => {
649
- const stillHasFocus = event.relatedTarget && elem.contains(event.relatedTarget);
650
- if (!stillHasFocus) {
651
- for (const event of keysDown) {
652
- this.toolController.dispatchInputEvent({
653
- ...event,
654
- kind: InputEvtType.KeyUpEvent,
655
- });
656
- }
657
- keysDown = [];
658
- }
627
+ listenForKeyboardEventsFrom(elem, {
628
+ filter,
629
+ handleKeyDown: (htmlEvent) => {
630
+ this.handleHTMLKeyDownEvent(htmlEvent);
631
+ },
632
+ handleKeyUp: (htmlEvent) => {
633
+ this.handleHTMLKeyUpEvent(htmlEvent);
634
+ },
659
635
  });
660
636
  // Allow drop.
661
637
  elem.ondragover = evt => {
@@ -1012,7 +988,6 @@ export class Editor {
1012
988
  this.display.setDraftMode(true);
1013
989
  const originalBackgrounds = this.image.getBackgroundComponents();
1014
990
  const eraseBackgroundCommand = new Erase(originalBackgrounds);
1015
- let autoresizeEnabled = false;
1016
991
  await loader.start(async (component) => {
1017
992
  await this.dispatchNoAnnounce(EditorImage.addElement(component));
1018
993
  }, (countProcessed, totalToProcess) => {
@@ -1026,13 +1001,9 @@ export class Editor {
1026
1001
  this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
1027
1002
  this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
1028
1003
  if (options) {
1029
- autoresizeEnabled = options.autoresize;
1004
+ this.dispatchNoAnnounce(this.image.setAutoresizeEnabled(options.autoresize), false);
1030
1005
  }
1031
1006
  });
1032
- // TODO: Move this call into the callback above. Currently, this would cause
1033
- // decrease in performance as the main background would be repeatedly added
1034
- // and removed from the editor every time another component is added.
1035
- this.dispatchNoAnnounce(this.image.setAutoresizeEnabled(autoresizeEnabled), false);
1036
1007
  // Ensure that we don't have multiple overlapping BackgroundComponents. Remove
1037
1008
  // old BackgroundComponents.
1038
1009
  // Overlapping BackgroundComponents may cause changing the background color to
@@ -1122,15 +1093,18 @@ export class Editor {
1122
1093
  const iconLicenseText = this.icons.licenseInfo();
1123
1094
  const notices = [];
1124
1095
  if (this.settings.appInfo) {
1125
- const versionLines = [];
1096
+ const descriptionLines = [];
1126
1097
  if (this.settings.appInfo.version) {
1127
- versionLines.push(`v${this.settings.appInfo.version}`, '');
1098
+ descriptionLines.push(`v${this.settings.appInfo.version}`, '');
1099
+ }
1100
+ if (this.settings.appInfo.description) {
1101
+ descriptionLines.push(this.settings.appInfo.description + '\n');
1128
1102
  }
1129
1103
  notices.push({
1130
1104
  heading: `${this.settings.appInfo.name}`,
1131
1105
  text: [
1132
- ...versionLines,
1133
- `Image editor library: js-draw v${version.number}.`,
1106
+ ...descriptionLines,
1107
+ `(js-draw v${version.number})`,
1134
1108
  ].join('\n'),
1135
1109
  });
1136
1110
  }
@@ -1140,19 +1114,28 @@ export class Editor {
1140
1114
  text: `v${version.number}`,
1141
1115
  });
1142
1116
  }
1117
+ const screenSize = this.viewport.getScreenRectSize();
1143
1118
  notices.push({
1144
- heading: 'Developer information',
1119
+ heading: this.localization.developerInformation,
1145
1120
  text: [
1146
1121
  'Image debug information (from when this dialog was opened):',
1147
- ` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()} rotation`,
1122
+ ` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()}° rotation`,
1148
1123
  ` ${this.image.estimateNumElements()} components`,
1149
- ` ${this.getImportExportRect().w}x${this.getImportExportRect().h} size`,
1124
+ ` auto-resize: ${this.image.getAutoresizeEnabled() ? 'enabled' : 'disabled'}`,
1125
+ ` ${this.getImportExportRect().w}x${this.getImportExportRect().h} image size`,
1126
+ ` ${screenSize.x}x${screenSize.y} screen size`,
1127
+ ' cache:',
1128
+ ` ${this.display.getCache().getDebugInfo()
1129
+ // Indent
1130
+ .replace(/([\n])/g, '\n ')}`,
1150
1131
  ].join('\n'),
1151
1132
  minimized: true,
1152
1133
  });
1153
1134
  notices.push({
1154
- heading: 'Libraries',
1135
+ heading: this.localization.softwareLibraries,
1155
1136
  text: [
1137
+ `This image editor is powered by js-draw v${version.number}.`,
1138
+ '',
1156
1139
  'js-draw uses several libraries at runtime. Particularly noteworthy are:',
1157
1140
  ' - The Coloris color picker: https://github.com/mdbassit/Coloris',
1158
1141
  ' - The bezier.js Bézier curve library: https://github.com/Pomax/bezierjs'
@@ -122,7 +122,12 @@ export default class BackgroundComponent extends AbstractComponent {
122
122
  let needsRerender = false;
123
123
  if (!this.contentBBox.eq(importExportRect)) {
124
124
  this.contentBBox = importExportRect;
125
- needsRerender = true;
125
+ // If the box already fills the screen, rerendering it will have
126
+ // no visual effect.
127
+ //
128
+ // TODO: This decision should be made by queueRerenderOf and not here.
129
+ //
130
+ needsRerender ||= !this.fillsScreen;
126
131
  }
127
132
  const imageAutoresizes = image.getAutoresizeEnabled();
128
133
  if (imageAutoresizes !== this.fillsScreen) {
@@ -42,7 +42,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
42
42
  private computeUntransformedBBoxOfPart;
43
43
  private recomputeBBox;
44
44
  private renderInternal;
45
- render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
45
+ render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
46
46
  getProportionalRenderingTime(): number;
47
47
  intersects(lineSegment: LineSegment2): boolean;
48
48
  getStyle(): ComponentStyle;