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.
- package/README.md +4 -6
- package/dist/Editor.css +30 -4
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +5 -0
- package/dist/cjs/Editor.js +53 -70
- package/dist/cjs/components/BackgroundComponent.js +6 -1
- package/dist/cjs/components/TextComponent.d.ts +1 -1
- package/dist/cjs/components/TextComponent.js +19 -12
- package/dist/cjs/image/EditorImage.js +8 -8
- package/dist/cjs/localization.d.ts +2 -0
- package/dist/cjs/localization.js +2 -0
- package/dist/cjs/localizations/comments.js +1 -0
- package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
- package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
- package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
- package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
- package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
- package/dist/cjs/toolbar/widgets/BaseWidget.js +3 -3
- package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
- package/dist/cjs/tools/SelectionTool/Selection.js +81 -52
- package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
- package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
- package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
- package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
- package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
- package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
- package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
- package/dist/cjs/version.js +1 -1
- package/dist/mjs/Editor.d.ts +5 -0
- package/dist/mjs/Editor.mjs +53 -70
- package/dist/mjs/components/BackgroundComponent.mjs +6 -1
- package/dist/mjs/components/TextComponent.d.ts +1 -1
- package/dist/mjs/components/TextComponent.mjs +19 -12
- package/dist/mjs/image/EditorImage.mjs +8 -8
- package/dist/mjs/localization.d.ts +2 -0
- package/dist/mjs/localization.mjs +2 -0
- package/dist/mjs/localizations/comments.mjs +1 -0
- package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
- package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
- package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
- package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
- package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
- package/dist/mjs/toolbar/widgets/BaseWidget.mjs +3 -3
- package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
- package/dist/mjs/tools/SelectionTool/Selection.mjs +81 -52
- package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
- package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
- package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
- package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
- package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
- package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
- package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
- package/dist/mjs/version.mjs +1 -1
- package/docs/img/readme-images/js-draw.png +0 -0
- package/package.json +6 -6
- package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -6,6 +6,7 @@ import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
|
|
6
6
|
import Selection from './Selection.mjs';
|
7
7
|
import TextComponent from '../../components/TextComponent.mjs';
|
8
8
|
import { duplicateSelectionShortcut, selectAllKeyboardShortcut, snapToGridKeyboardShortcutId } from '../keybindings.mjs';
|
9
|
+
import ToPointerAutoscroller from './ToPointerAutoscroller.mjs';
|
9
10
|
export const cssPrefix = 'selection-tool-';
|
10
11
|
// Allows users to select/transform portions of the `EditorImage`.
|
11
12
|
// With respect to `extend`ing, `SelectionTool` is not stable.
|
@@ -17,8 +18,19 @@ class SelectionTool extends BaseTool {
|
|
17
18
|
this.expandingSelectionBox = false;
|
18
19
|
this.shiftKeyPressed = false;
|
19
20
|
this.snapToGrid = false;
|
21
|
+
this.lastPointer = null;
|
20
22
|
this.selectionBoxHandlingEvt = false;
|
21
23
|
this.lastSelectedObjects = [];
|
24
|
+
this.autoscroller = new ToPointerAutoscroller(editor.viewport, (scrollBy) => {
|
25
|
+
editor.dispatch(Viewport.transformBy(Mat33.translation(scrollBy)), false);
|
26
|
+
// Update the selection box/content to match the new viewport.
|
27
|
+
if (this.lastPointer) {
|
28
|
+
// The viewport has changed -- ensure that the screen and canvas positions
|
29
|
+
// of the pointer are both correct
|
30
|
+
const updatedPointer = this.lastPointer.withScreenPosition(this.lastPointer.screenPos, editor.viewport);
|
31
|
+
this.onMainPointerUpdated(updatedPointer);
|
32
|
+
}
|
33
|
+
});
|
22
34
|
this.handleOverlay = document.createElement('div');
|
23
35
|
editor.createHTMLOverlay(this.handleOverlay);
|
24
36
|
this.handleOverlay.style.display = 'none';
|
@@ -81,14 +93,22 @@ class SelectionTool extends BaseTool {
|
|
81
93
|
this.expandingSelectionBox = this.shiftKeyPressed;
|
82
94
|
this.makeSelectionBox(current.canvasPos);
|
83
95
|
}
|
96
|
+
else {
|
97
|
+
// Only autoscroll if we're transforming an existing selection
|
98
|
+
this.autoscroller.start();
|
99
|
+
}
|
84
100
|
return true;
|
85
101
|
}
|
86
102
|
return false;
|
87
103
|
}
|
88
104
|
onPointerMove(event) {
|
105
|
+
this.onMainPointerUpdated(event.current);
|
106
|
+
}
|
107
|
+
onMainPointerUpdated(currentPointer) {
|
108
|
+
this.lastPointer = currentPointer;
|
89
109
|
if (!this.selectionBox)
|
90
110
|
return;
|
91
|
-
|
111
|
+
this.autoscroller.onPointerMove(currentPointer.screenPos);
|
92
112
|
if (!this.expandingSelectionBox && this.shiftKeyPressed && this.startPoint) {
|
93
113
|
const screenPos = this.editor.viewport.canvasToScreen(this.startPoint);
|
94
114
|
currentPointer = currentPointer.lockedToXYAxesScreen(screenPos, this.editor.viewport);
|
@@ -103,21 +123,8 @@ class SelectionTool extends BaseTool {
|
|
103
123
|
this.selectionBox.setToPoint(currentPointer.canvasPos);
|
104
124
|
}
|
105
125
|
}
|
106
|
-
// Called after a gestureCancel and a pointerUp
|
107
|
-
onGestureEnd() {
|
108
|
-
if (!this.selectionBox)
|
109
|
-
return;
|
110
|
-
if (!this.selectionBoxHandlingEvt) {
|
111
|
-
// Expand/shrink the selection rectangle, if applicable
|
112
|
-
this.selectionBox.resolveToObjects();
|
113
|
-
this.onSelectionUpdated();
|
114
|
-
}
|
115
|
-
else {
|
116
|
-
this.selectionBox.onDragEnd();
|
117
|
-
}
|
118
|
-
this.selectionBoxHandlingEvt = false;
|
119
|
-
}
|
120
126
|
onPointerUp(event) {
|
127
|
+
this.autoscroller.stop();
|
121
128
|
if (!this.selectionBox)
|
122
129
|
return;
|
123
130
|
let currentPointer = event.current;
|
@@ -136,10 +143,20 @@ class SelectionTool extends BaseTool {
|
|
136
143
|
]);
|
137
144
|
}
|
138
145
|
else {
|
139
|
-
this.
|
146
|
+
if (!this.selectionBoxHandlingEvt) {
|
147
|
+
// Expand/shrink the selection rectangle, if applicable
|
148
|
+
this.selectionBox.resolveToObjects();
|
149
|
+
this.onSelectionUpdated();
|
150
|
+
}
|
151
|
+
else {
|
152
|
+
this.selectionBox.onDragEnd();
|
153
|
+
}
|
154
|
+
this.selectionBoxHandlingEvt = false;
|
155
|
+
this.lastPointer = null;
|
140
156
|
}
|
141
157
|
}
|
142
158
|
onGestureCancel() {
|
159
|
+
this.autoscroller.stop();
|
143
160
|
if (this.selectionBoxHandlingEvt) {
|
144
161
|
this.selectionBox?.onDragCancel();
|
145
162
|
}
|
@@ -152,6 +169,8 @@ class SelectionTool extends BaseTool {
|
|
152
169
|
this.prevSelectionBox = null;
|
153
170
|
}
|
154
171
|
this.expandingSelectionBox = false;
|
172
|
+
this.lastPointer = null;
|
173
|
+
this.selectionBoxHandlingEvt = false;
|
155
174
|
}
|
156
175
|
onSelectionUpdated() {
|
157
176
|
const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
|
@@ -277,6 +296,7 @@ class SelectionTool extends BaseTool {
|
|
277
296
|
const transform = Mat33.scaling2D(scaleFactor, this.editor.viewport.roundPoint(region.topLeft)).rightMul(Mat33.translation(regionCenter).rightMul(roundedRotationMatrix).rightMul(Mat33.translation(regionCenter.times(-1)))).rightMul(Mat33.translation(this.editor.viewport.roundPoint(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))));
|
278
297
|
const oldTransform = this.selectionBox.getTransform();
|
279
298
|
this.selectionBox.setTransform(oldTransform.rightMul(transform));
|
299
|
+
this.selectionBox.scrollTo();
|
280
300
|
}
|
281
301
|
if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
|
282
302
|
this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { Point2, Vec2 } from '@js-draw/math';
|
2
|
+
import Viewport from '../../Viewport';
|
3
|
+
type ScrollByCallback = (delta: Vec2) => void;
|
4
|
+
/**
|
5
|
+
* Automatically scrolls the viewport such that the user's pointer is visible.
|
6
|
+
*/
|
7
|
+
export default class ToPointerAutoscroller {
|
8
|
+
private viewport;
|
9
|
+
private scrollByCanvasDelta;
|
10
|
+
private started;
|
11
|
+
private updateLoopId;
|
12
|
+
private updateLoopRunning;
|
13
|
+
private targetPoint;
|
14
|
+
private scrollRate;
|
15
|
+
constructor(viewport: Viewport, scrollByCanvasDelta: ScrollByCallback);
|
16
|
+
private getScrollForPoint;
|
17
|
+
start(): void;
|
18
|
+
onPointerMove(pointerScreenPosition: Point2): void;
|
19
|
+
stop(): void;
|
20
|
+
private startUpdateLoop;
|
21
|
+
private stopUpdateLoop;
|
22
|
+
}
|
23
|
+
export {};
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import { Rect2, Vec2 } from '@js-draw/math';
|
2
|
+
import untilNextAnimationFrame from '../../util/untilNextAnimationFrame.mjs';
|
3
|
+
/**
|
4
|
+
* Automatically scrolls the viewport such that the user's pointer is visible.
|
5
|
+
*/
|
6
|
+
export default class ToPointerAutoscroller {
|
7
|
+
constructor(viewport, scrollByCanvasDelta) {
|
8
|
+
this.viewport = viewport;
|
9
|
+
this.scrollByCanvasDelta = scrollByCanvasDelta;
|
10
|
+
this.started = false;
|
11
|
+
this.updateLoopId = 0;
|
12
|
+
this.updateLoopRunning = false;
|
13
|
+
this.targetPoint = null;
|
14
|
+
this.scrollRate = 1000; // px/s
|
15
|
+
}
|
16
|
+
getScrollForPoint(screenPoint) {
|
17
|
+
const screenSize = this.viewport.getScreenRectSize();
|
18
|
+
const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
|
19
|
+
// Starts autoscrolling when the cursor is **outside of** this region
|
20
|
+
const marginSize = 44;
|
21
|
+
const autoscrollBoundary = screenRect.grownBy(-marginSize);
|
22
|
+
if (autoscrollBoundary.containsPoint(screenPoint)) {
|
23
|
+
return Vec2.zero;
|
24
|
+
}
|
25
|
+
const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
|
26
|
+
const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
|
27
|
+
const toEdge = closestEdgePoint.minus(screenPoint);
|
28
|
+
// Go faster for points further away from the boundary.
|
29
|
+
const maximumScaleFactor = 1.25;
|
30
|
+
const scaleFactor = Math.min(distToEdge / marginSize, maximumScaleFactor);
|
31
|
+
return toEdge.normalizedOrZero().times(scaleFactor);
|
32
|
+
}
|
33
|
+
start() {
|
34
|
+
this.started = true;
|
35
|
+
}
|
36
|
+
onPointerMove(pointerScreenPosition) {
|
37
|
+
if (!this.started) {
|
38
|
+
return;
|
39
|
+
}
|
40
|
+
if (this.getScrollForPoint(pointerScreenPosition) === Vec2.zero) {
|
41
|
+
this.stopUpdateLoop();
|
42
|
+
}
|
43
|
+
else {
|
44
|
+
this.targetPoint = pointerScreenPosition;
|
45
|
+
this.startUpdateLoop();
|
46
|
+
}
|
47
|
+
}
|
48
|
+
stop() {
|
49
|
+
this.targetPoint = null;
|
50
|
+
this.started = false;
|
51
|
+
this.stopUpdateLoop();
|
52
|
+
}
|
53
|
+
startUpdateLoop() {
|
54
|
+
if (this.updateLoopRunning) {
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
(async () => {
|
58
|
+
this.updateLoopId++;
|
59
|
+
const currentUpdateLoopId = this.updateLoopId;
|
60
|
+
let lastUpdateTime = performance.now();
|
61
|
+
while (this.updateLoopId === currentUpdateLoopId && this.targetPoint) {
|
62
|
+
this.updateLoopRunning = true;
|
63
|
+
const currentTime = performance.now();
|
64
|
+
const deltaTimeMs = currentTime - lastUpdateTime;
|
65
|
+
const scrollDirection = this.getScrollForPoint(this.targetPoint);
|
66
|
+
const screenScrollAmount = scrollDirection.times(this.scrollRate * deltaTimeMs / 1000);
|
67
|
+
this.scrollByCanvasDelta(this.viewport.screenToCanvasTransform.transformVec3(screenScrollAmount));
|
68
|
+
lastUpdateTime = currentTime;
|
69
|
+
await untilNextAnimationFrame();
|
70
|
+
}
|
71
|
+
this.updateLoopRunning = false;
|
72
|
+
})();
|
73
|
+
}
|
74
|
+
stopUpdateLoop() {
|
75
|
+
this.updateLoopId++;
|
76
|
+
}
|
77
|
+
}
|
@@ -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
|
}
|
@@ -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.
|
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
|
-
|
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(
|
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
|
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;
|
package/dist/mjs/version.mjs
CHANGED
Binary file
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "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,13 +64,13 @@
|
|
64
64
|
"postpack": "ts-node tools/copyREADME.ts revert"
|
65
65
|
},
|
66
66
|
"dependencies": {
|
67
|
-
"@js-draw/math": "^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.
|
72
|
-
"@types/jest": "29.5.
|
73
|
-
"@types/jsdom": "21.1.
|
71
|
+
"@js-draw/build-tool": "^1.7.0",
|
72
|
+
"@types/jest": "29.5.5",
|
73
|
+
"@types/jsdom": "21.1.3"
|
74
74
|
},
|
75
75
|
"bugs": {
|
76
76
|
"url": "https://github.com/personalizedrefrigerator/js-draw/issues"
|
@@ -86,5 +86,5 @@
|
|
86
86
|
"freehand",
|
87
87
|
"svg"
|
88
88
|
],
|
89
|
-
"gitHead": "
|
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
|
-
|
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
|
-
|
61
|
-
|
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 {
|