js-draw 1.6.1 → 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 (60) hide show
  1. package/README.md +0 -2
  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/localization.d.ts +2 -0
  11. package/dist/cjs/localization.js +2 -0
  12. package/dist/cjs/localizations/comments.js +1 -0
  13. package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
  14. package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  15. package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
  16. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  17. package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
  18. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
  19. package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
  20. package/dist/cjs/tools/SelectionTool/Selection.js +75 -48
  21. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  22. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
  23. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  24. package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
  25. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  26. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  27. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  28. package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
  29. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  30. package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/mjs/Editor.d.ts +5 -0
  33. package/dist/mjs/Editor.mjs +53 -70
  34. package/dist/mjs/components/BackgroundComponent.mjs +6 -1
  35. package/dist/mjs/components/TextComponent.d.ts +1 -1
  36. package/dist/mjs/components/TextComponent.mjs +19 -12
  37. package/dist/mjs/localization.d.ts +2 -0
  38. package/dist/mjs/localization.mjs +2 -0
  39. package/dist/mjs/localizations/comments.mjs +1 -0
  40. package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
  41. package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  42. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
  43. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  44. package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
  45. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
  46. package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
  47. package/dist/mjs/tools/SelectionTool/Selection.mjs +75 -48
  48. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  49. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
  50. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  51. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
  52. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  53. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  54. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  55. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
  56. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  57. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
  58. package/dist/mjs/version.mjs +1 -1
  59. package/package.json +4 -4
  60. package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -15,7 +15,7 @@ export class DragTransformer {
15
15
  this.selection.setTransform(Mat33.translation(delta));
16
16
  }
17
17
  onDragEnd() {
18
- this.selection.finalizeTransform();
18
+ return this.selection.finalizeTransform();
19
19
  }
20
20
  }
21
21
  export class ResizeTransformer {
@@ -28,6 +28,32 @@ export class ResizeTransformer {
28
28
  this.selection.setTransform(Mat33.identity);
29
29
  this.mode = mode;
30
30
  this.dragStartPoint = startPoint;
31
+ this.computeOriginAndScaleRate();
32
+ }
33
+ computeOriginAndScaleRate() {
34
+ // Store the index of the furthest corner from startPoint. We'll use that
35
+ // to determine where the transform considers (0, 0) (where we scale from).
36
+ const selectionRect = this.selection.preTransformRegion;
37
+ const selectionBoxCorners = selectionRect.corners;
38
+ let largestDistSquared = 0;
39
+ for (let i = 0; i < selectionBoxCorners.length; i++) {
40
+ const currentCorner = selectionBoxCorners[i];
41
+ const distSquaredToCurrent = this.dragStartPoint.minus(currentCorner).magnitudeSquared();
42
+ if (distSquaredToCurrent > largestDistSquared) {
43
+ largestDistSquared = distSquaredToCurrent;
44
+ this.transformOrigin = currentCorner;
45
+ }
46
+ }
47
+ // Determine whether moving the mouse to the right increases or decreases the width.
48
+ let widthScaleRate = 1;
49
+ let heightScaleRate = 1;
50
+ if (this.transformOrigin.x > selectionRect.center.x) {
51
+ widthScaleRate = -1;
52
+ }
53
+ if (this.transformOrigin.y > selectionRect.center.y) {
54
+ heightScaleRate = -1;
55
+ }
56
+ this.scaleRate = Vec2.of(widthScaleRate, heightScaleRate);
31
57
  }
32
58
  onDragUpdate(canvasPos) {
33
59
  const canvasDelta = canvasPos.minus(this.dragStartPoint);
@@ -35,11 +61,11 @@ export class ResizeTransformer {
35
61
  const origHeight = this.selection.preTransformRegion.height;
36
62
  let scale = Vec2.of(1, 1);
37
63
  if (this.mode === ResizeMode.HorizontalOnly) {
38
- const newWidth = origWidth + canvasDelta.x;
64
+ const newWidth = origWidth + canvasDelta.x * this.scaleRate.x;
39
65
  scale = Vec2.of(newWidth / origWidth, scale.y);
40
66
  }
41
67
  if (this.mode === ResizeMode.VerticalOnly) {
42
- const newHeight = origHeight + canvasDelta.y;
68
+ const newHeight = origHeight + canvasDelta.y * this.scaleRate.y;
43
69
  scale = Vec2.of(scale.x, newHeight / origHeight);
44
70
  }
45
71
  if (this.mode === ResizeMode.Both) {
@@ -51,12 +77,12 @@ export class ResizeTransformer {
51
77
  // long decimal representations => large file sizes.
52
78
  scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
53
79
  if (scale.x !== 0 && scale.y !== 0) {
54
- const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
80
+ const origin = this.editor.viewport.roundPoint(this.transformOrigin);
55
81
  this.selection.setTransform(Mat33.scaling2D(scale, origin));
56
82
  }
57
83
  }
58
84
  onDragEnd() {
59
- this.selection.finalizeTransform();
85
+ return this.selection.finalizeTransform();
60
86
  }
61
87
  }
62
88
  export class RotateTransformer {
@@ -64,6 +90,8 @@ export class RotateTransformer {
64
90
  this.editor = editor;
65
91
  this.selection = selection;
66
92
  this.startAngle = 0;
93
+ this.targetRotation = 0;
94
+ this.maximumDistFromStart = 0;
67
95
  }
68
96
  getAngle(canvasPoint) {
69
97
  const selectionCenter = this.selection.preTransformRegion.center;
@@ -76,14 +104,16 @@ export class RotateTransformer {
76
104
  return Math.round(angle * roundingFactor) / roundingFactor;
77
105
  }
78
106
  onDragStart(startPoint) {
107
+ this.startPoint = startPoint;
79
108
  this.selection.setTransform(Mat33.identity);
80
109
  this.startAngle = this.getAngle(startPoint);
110
+ this.maximumDistFromStart = 0;
111
+ this.targetRotation = 0;
81
112
  }
82
- onDragUpdate(canvasPos) {
83
- const targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
113
+ setRotationTo(angle) {
84
114
  // Transform in canvas space
85
115
  const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center);
86
- const unrounded = Mat33.zRotation(targetRotation);
116
+ const unrounded = Mat33.zRotation(angle);
87
117
  const roundedRotationTransform = unrounded.mapEntries(entry => Viewport.roundScaleRatio(entry));
88
118
  const fullRoundedTransform = Mat33
89
119
  .translation(canvasSelCenter)
@@ -91,7 +121,20 @@ export class RotateTransformer {
91
121
  .rightMul(Mat33.translation(canvasSelCenter.times(-1)));
92
122
  this.selection.setTransform(fullRoundedTransform);
93
123
  }
124
+ onDragUpdate(canvasPos) {
125
+ this.targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
126
+ this.setRotationTo(this.targetRotation);
127
+ const distFromStart = canvasPos.minus(this.startPoint).magnitude();
128
+ if (distFromStart > this.maximumDistFromStart) {
129
+ this.maximumDistFromStart = distFromStart;
130
+ }
131
+ }
94
132
  onDragEnd() {
95
- this.selection.finalizeTransform();
133
+ // Anything less than this is considered a click
134
+ const clickThreshold = 15;
135
+ if (this.maximumDistFromStart < clickThreshold && this.targetRotation === 0) {
136
+ this.setRotationTo(-Math.PI / 2);
137
+ }
138
+ return this.selection.finalizeTransform();
96
139
  }
97
140
  }
@@ -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,140 @@
1
+ /**
2
+ * Calls `callbacks` when different keys are known to be pressed.
3
+ *
4
+ * `filter` can be used to ignore events.
5
+ *
6
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
7
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
8
+ * events). Artifical events are created for these changes and sent to `callbacks`.
9
+ */
10
+ const listenForKeyboardEventsFrom = (elem, callbacks) => {
11
+ // Track which keys are down so we can release them when the element
12
+ // loses focus. This is particularly important for keys like Control
13
+ // that can trigger shortcuts that cause the editor to lose focus before
14
+ // the keyup event is triggered.
15
+ let keysDown = [];
16
+ // Return whether two objects that are similar to keyboard events represent the
17
+ // same key.
18
+ const keyEventsMatch = (a, b) => {
19
+ return a.key === b.key && a.code === b.code;
20
+ };
21
+ const isKeyDown = (keyEvent) => {
22
+ return keysDown.some(other => keyEventsMatch(other, keyEvent));
23
+ };
24
+ const keyEventToRecord = (event) => {
25
+ return {
26
+ code: event.code,
27
+ key: event.key,
28
+ ctrlKey: event.ctrlKey,
29
+ altKey: event.altKey,
30
+ shiftKey: event.shiftKey,
31
+ metaKey: event.metaKey,
32
+ };
33
+ };
34
+ const handleKeyEvent = (htmlEvent) => {
35
+ if (htmlEvent.type === 'keydown') {
36
+ // Add event to the list of keys that are down (so long as it
37
+ // isn't a duplicate).
38
+ if (!isKeyDown(htmlEvent)) {
39
+ // Destructructring, then pushing seems to cause
40
+ // data loss. Copy properties individually:
41
+ keysDown.push(keyEventToRecord(htmlEvent));
42
+ }
43
+ if (!callbacks.filter(htmlEvent)) {
44
+ return;
45
+ }
46
+ callbacks.handleKeyDown(htmlEvent);
47
+ }
48
+ else { // keyup
49
+ console.assert(htmlEvent.type === 'keyup');
50
+ // Remove the key from keysDown -- it's no longer down.
51
+ keysDown = keysDown.filter(event => {
52
+ const matches = keyEventsMatch(event, htmlEvent);
53
+ return !matches;
54
+ });
55
+ if (!callbacks.filter(htmlEvent)) {
56
+ return;
57
+ }
58
+ callbacks.handleKeyUp(htmlEvent);
59
+ }
60
+ };
61
+ elem.addEventListener('keydown', htmlEvent => {
62
+ handleKeyEvent(htmlEvent);
63
+ });
64
+ elem.addEventListener('keyup', htmlEvent => {
65
+ handleKeyEvent(htmlEvent);
66
+ });
67
+ elem.addEventListener('focusout', (focusEvent) => {
68
+ const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
69
+ if (!stillHasFocus) {
70
+ for (const event of keysDown) {
71
+ callbacks.handleKeyUp(new KeyboardEvent('keyup', {
72
+ ...event,
73
+ }));
74
+ }
75
+ keysDown = [];
76
+ }
77
+ });
78
+ const fireArtificalEventsBasedOn = (htmlEvent) => {
79
+ let wasShiftDown = false;
80
+ let wasCtrlDown = false;
81
+ let wasAltDown = false;
82
+ let wasMetaDown = false;
83
+ for (const otherEvent of keysDown) {
84
+ const code = otherEvent.code;
85
+ wasShiftDown ||= !!code.match(/^Shift(Left|Right)$/);
86
+ wasCtrlDown ||= !!code.match(/^Control(Left|Right)$/);
87
+ wasAltDown ||= !!code.match(/^Alt(Left|Right)$/);
88
+ wasMetaDown ||= !!code.match(/^Meta(Left|Right)$/);
89
+ }
90
+ const eventName = (isDown) => {
91
+ if (isDown) {
92
+ return 'keydown';
93
+ }
94
+ else {
95
+ return 'keyup';
96
+ }
97
+ };
98
+ const eventInitDefaults = {
99
+ shiftKey: htmlEvent.shiftKey,
100
+ altKey: htmlEvent.altKey,
101
+ metaKey: htmlEvent.metaKey,
102
+ ctrlKey: htmlEvent.ctrlKey,
103
+ };
104
+ if (htmlEvent.shiftKey !== wasShiftDown) {
105
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.shiftKey), {
106
+ ...eventInitDefaults,
107
+ key: 'Shift',
108
+ code: 'ShiftLeft',
109
+ }));
110
+ }
111
+ if (htmlEvent.altKey !== wasAltDown) {
112
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.altKey), {
113
+ ...eventInitDefaults,
114
+ key: 'Alt',
115
+ code: 'AltLeft',
116
+ }));
117
+ }
118
+ if (htmlEvent.ctrlKey !== wasCtrlDown) {
119
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.ctrlKey), {
120
+ ...eventInitDefaults,
121
+ key: 'Control',
122
+ code: 'ControlLeft',
123
+ }));
124
+ }
125
+ if (htmlEvent.metaKey !== wasMetaDown) {
126
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.metaKey), {
127
+ ...eventInitDefaults,
128
+ key: 'Meta',
129
+ code: 'MetaLeft',
130
+ }));
131
+ }
132
+ };
133
+ elem.addEventListener('mousedown', (htmlEvent) => {
134
+ fireArtificalEventsBasedOn(htmlEvent);
135
+ });
136
+ elem.addEventListener('mousemove', (htmlEvent) => {
137
+ fireArtificalEventsBasedOn(htmlEvent);
138
+ });
139
+ };
140
+ export default listenForKeyboardEventsFrom;
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.6.1',
2
+ number: '1.7.0',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -64,11 +64,11 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.6.1",
67
+ "@js-draw/math": "^1.7.0",
68
68
  "@melloware/coloris": "0.21.0"
69
69
  },
70
70
  "devDependencies": {
71
- "@js-draw/build-tool": "^1.6.1",
71
+ "@js-draw/build-tool": "^1.7.0",
72
72
  "@types/jest": "29.5.5",
73
73
  "@types/jsdom": "21.1.3"
74
74
  },
@@ -86,5 +86,5 @@
86
86
  "freehand",
87
87
  "svg"
88
88
  ],
89
- "gitHead": "00f1a0ab34de40bec100d5c18657f947895bfaed"
89
+ "gitHead": "e7d8a68e6a5ae3063de9c1f033ed8f08c567b393"
90
90
  }
@@ -3,27 +3,51 @@
3
3
  background-color: var(--selection-background-color);
4
4
  opacity: 0.5;
5
5
  overflow: visible;
6
- position: absolute;
7
6
  }
8
7
 
9
8
  .selection-tool-handle {
10
- border: 1px solid var(--foreground-color-1);
11
- background: var(--background-color-1);
12
9
  position: absolute;
13
-
14
10
  box-sizing: border-box;
15
- padding: 3px;
16
11
 
17
- & .icon {
12
+ // Center content
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+
17
+ // Maximum size of the visible region (make the handle slightly larger
18
+ // so that the resize cursor is visible everywhere in the actual selection
19
+ // box).
20
+ --max-size: 17px;
21
+
22
+ .selection-tool-content {
23
+ border: 1px solid var(--foreground-color-1);
24
+ background: var(--background-color-1);
25
+ box-sizing: border-box;
26
+
27
+ max-width: var(--max-size);
28
+ max-height: var(--max-size);
18
29
  width: 100%;
19
30
  height: 100%;
31
+
32
+ display: flex;
33
+ justify-content: center;
34
+ align-items: center;
35
+
36
+ padding: 3px;
37
+ .icon {
38
+ width: 100%;
39
+ height: 100%;
40
+ }
20
41
  }
21
42
 
22
- &.selection-tool-circle {
43
+ &.selection-tool-circle .selection-tool-content {
23
44
  border-radius: 100%;
24
45
  }
25
46
 
26
47
  &.selection-tool-rotate {
48
+ // Shrink less if a rotation handle
49
+ --max-size: 28px;
50
+
27
51
  cursor: grab;
28
52
  }
29
53
  }
@@ -57,8 +81,37 @@
57
81
  }
58
82
 
59
83
  .overlay.handleOverlay {
60
- height: 0;
61
- overflow: visible;
84
+ touch-action: none;
85
+
86
+ // When expanding a selection with shift+click&drag, multiple selection boxes
87
+ // can be present in the same handleOverlay. As such, so that other overlayed
88
+ // selection boxes are in the correct place, the outer container needs to have
89
+ // zero height.
90
+ //
91
+ // This is in addition to the overlay container, which needs zero height to prevent
92
+ // other overlay containers from being affected by its size.
93
+ &, .selection-tool-selection-outer-container {
94
+ height: 0;
95
+ overflow: visible;
96
+ }
97
+
98
+ .selection-tool-selection-inner-container {
99
+ width: var(--editor-current-display-width-px);
100
+ height: var(--editor-current-display-height-px);
101
+ overflow: hidden;
102
+
103
+ // Disable pointer events: If the parent (or the container) has
104
+ // captured pointers and the container is removed, this prevents
105
+ // us from receiving the following events (e.g. in Firefox).
106
+ pointer-events: none;
107
+
108
+ & > * {
109
+ // We *do* want pointer events for handles and the background. This
110
+ // allows the mouse cursor to change shape when hovering over resize
111
+ // handles.
112
+ pointer-events: all;
113
+ }
114
+ }
62
115
  }
63
116
 
64
117
  @keyframes selection-duplicated-animation {