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
@@ -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.
|
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
|
-
|
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(
|
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
|
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;
|
package/dist/cjs/version.js
CHANGED
package/dist/mjs/Editor.d.ts
CHANGED
@@ -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.
|
package/dist/mjs/Editor.mjs
CHANGED
@@ -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
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
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
|
-
|
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
|
1096
|
+
const descriptionLines = [];
|
1126
1097
|
if (this.settings.appInfo.version) {
|
1127
|
-
|
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
|
-
...
|
1133
|
-
`
|
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:
|
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.
|
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:
|
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
|
-
|
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,
|
45
|
+
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
46
46
|
getProportionalRenderingTime(): number;
|
47
47
|
intersects(lineSegment: LineSegment2): boolean;
|
48
48
|
getStyle(): ComponentStyle;
|