js-draw 0.1.8 → 0.1.11
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/CHANGELOG.md +15 -0
- package/README.md +15 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +3 -0
- package/dist/src/Editor.js +45 -16
- package/dist/src/UndoRedoHistory.js +3 -0
- package/dist/src/Viewport.js +2 -0
- package/dist/src/bundle/bundled.d.ts +2 -1
- package/dist/src/bundle/bundled.js +2 -1
- package/dist/src/geometry/LineSegment2.d.ts +1 -0
- package/dist/src/geometry/LineSegment2.js +16 -0
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +16 -0
- package/dist/src/localizations/en.d.ts +3 -0
- package/dist/src/localizations/en.js +4 -0
- package/dist/src/localizations/es.d.ts +3 -0
- package/dist/src/localizations/es.js +18 -0
- package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
- package/dist/src/localizations/getLocalizationTable.js +43 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
- package/dist/src/toolbar/HTMLToolbar.js +10 -8
- package/dist/src/toolbar/icons.js +13 -13
- package/dist/src/toolbar/localization.d.ts +2 -0
- package/dist/src/toolbar/localization.js +2 -0
- package/dist/src/toolbar/makeColorInput.js +22 -8
- package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +8 -1
- package/dist/src/tools/BaseTool.d.ts +2 -1
- package/dist/src/tools/BaseTool.js +3 -0
- package/dist/src/tools/PanZoom.d.ts +2 -1
- package/dist/src/tools/PanZoom.js +10 -4
- package/dist/src/tools/SelectionTool.d.ts +9 -2
- package/dist/src/tools/SelectionTool.js +131 -19
- package/dist/src/tools/ToolController.js +6 -2
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +9 -2
- package/dist/src/types.js +1 -0
- package/package.json +9 -1
- package/src/Editor.ts +54 -14
- package/src/UndoRedoHistory.ts +4 -0
- package/src/Viewport.ts +2 -0
- package/src/bundle/bundled.ts +2 -1
- package/src/geometry/LineSegment2.test.ts +15 -0
- package/src/geometry/LineSegment2.ts +20 -0
- package/src/geometry/Rect2.test.ts +20 -7
- package/src/geometry/Rect2.ts +19 -1
- package/src/localizations/en.ts +8 -0
- package/src/localizations/es.ts +62 -0
- package/src/localizations/getLocalizationTable.test.ts +27 -0
- package/src/localizations/getLocalizationTable.ts +53 -0
- package/src/toolbar/HTMLToolbar.ts +11 -9
- package/src/toolbar/icons.ts +13 -13
- package/src/toolbar/localization.ts +4 -0
- package/src/toolbar/makeColorInput.ts +25 -10
- package/src/toolbar/toolbar.css +6 -2
- package/src/toolbar/widgets/BaseWidget.ts +34 -0
- package/src/toolbar/widgets/HandToolWidget.ts +12 -1
- package/src/tools/BaseTool.ts +5 -1
- package/src/tools/PanZoom.ts +13 -4
- package/src/tools/SelectionTool.test.ts +24 -1
- package/src/tools/SelectionTool.ts +158 -23
- package/src/tools/ToolController.ts +6 -2
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +10 -1
- package/dist-test/test-dist-bundle.html +0 -42
@@ -14,7 +14,8 @@ export declare enum PanZoomMode {
|
|
14
14
|
OneFingerTouchGestures = 1,
|
15
15
|
TwoFingerTouchGestures = 2,
|
16
16
|
RightClickDrags = 4,
|
17
|
-
SinglePointerGestures = 8
|
17
|
+
SinglePointerGestures = 8,
|
18
|
+
Keyboard = 16
|
18
19
|
}
|
19
20
|
export default class PanZoom extends BaseTool {
|
20
21
|
private editor;
|
@@ -12,6 +12,7 @@ export var PanZoomMode;
|
|
12
12
|
PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
|
13
13
|
PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
|
14
14
|
PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
|
15
|
+
PanZoomMode[PanZoomMode["Keyboard"] = 16] = "Keyboard";
|
15
16
|
})(PanZoomMode || (PanZoomMode = {}));
|
16
17
|
export default class PanZoom extends BaseTool {
|
17
18
|
constructor(editor, mode, description) {
|
@@ -120,19 +121,24 @@ export default class PanZoom extends BaseTool {
|
|
120
121
|
this.transform.apply(this.editor);
|
121
122
|
}
|
122
123
|
onWheel({ delta, screenPos }) {
|
123
|
-
|
124
|
-
|
125
|
-
|
124
|
+
// Reset the transformation -- wheel events are individual events, so we don't
|
125
|
+
// need to unapply/reapply.
|
126
|
+
this.transform = new Viewport.ViewportTransform(Mat33.identity);
|
126
127
|
const canvasPos = this.editor.viewport.screenToCanvas(screenPos);
|
127
128
|
const toCanvas = this.editor.viewport.screenToCanvasTransform;
|
128
129
|
// Transform without including translation
|
129
130
|
const translation = toCanvas.transformVec3(Vec3.of(-delta.x, -delta.y, 0));
|
130
131
|
const pinchZoomScaleFactor = 1.04;
|
131
|
-
const transformUpdate = Mat33.scaling2D(Math.pow(pinchZoomScaleFactor, -delta.z), canvasPos).rightMul(Mat33.translation(translation));
|
132
|
+
const transformUpdate = Mat33.scaling2D(Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos).rightMul(Mat33.translation(translation));
|
132
133
|
this.updateTransform(transformUpdate);
|
133
134
|
return true;
|
134
135
|
}
|
135
136
|
onKeyPress({ key }) {
|
137
|
+
if (!(this.mode & PanZoomMode.Keyboard)) {
|
138
|
+
return false;
|
139
|
+
}
|
140
|
+
// No need to keep the same the transform for keyboard events.
|
141
|
+
this.transform = new Viewport.ViewportTransform(Mat33.identity);
|
136
142
|
let translation = Vec2.zero;
|
137
143
|
let scale = 1;
|
138
144
|
let rotation = 0;
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import Command from '../commands/Command';
|
2
2
|
import Editor from '../Editor';
|
3
|
+
import Mat33 from '../geometry/Mat33';
|
3
4
|
import Rect2 from '../geometry/Rect2';
|
4
5
|
import { Point2, Vec2 } from '../geometry/Vec2';
|
5
|
-
import { PointerEvt } from '../types';
|
6
|
+
import { KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
|
6
7
|
import BaseTool from './BaseTool';
|
7
8
|
import { ToolType } from './ToolController';
|
8
9
|
declare class Selection {
|
@@ -20,7 +21,8 @@ declare class Selection {
|
|
20
21
|
handleResizeCornerDrag(deltaPosition: Vec2): void;
|
21
22
|
handleRotateCircleDrag(offset: Vec2): void;
|
22
23
|
private computeTransformCommands;
|
23
|
-
|
24
|
+
transformPreview(transform: Mat33): void;
|
25
|
+
finalizeTransform(): void;
|
24
26
|
private static ApplyTransformationCommand;
|
25
27
|
private previewTransformCmds;
|
26
28
|
appendBackgroundBoxTo(elem: HTMLElement): void;
|
@@ -32,6 +34,7 @@ declare class Selection {
|
|
32
34
|
private recomputeBoxRotation;
|
33
35
|
getSelectedItemCount(): number;
|
34
36
|
updateUI(): void;
|
37
|
+
scrollTo(): void;
|
35
38
|
deleteSelectedObjects(): Command;
|
36
39
|
duplicateSelectedObjects(): Command;
|
37
40
|
}
|
@@ -45,8 +48,12 @@ export default class SelectionTool extends BaseTool {
|
|
45
48
|
onPointerDown(event: PointerEvt): boolean;
|
46
49
|
onPointerMove(event: PointerEvt): void;
|
47
50
|
private onGestureEnd;
|
51
|
+
private zoomToSelection;
|
48
52
|
onPointerUp(event: PointerEvt): void;
|
49
53
|
onGestureCancel(): void;
|
54
|
+
private static handleableKeys;
|
55
|
+
onKeyPress(event: KeyPressEvent): boolean;
|
56
|
+
onKeyUp(evt: KeyUpEvent): boolean;
|
50
57
|
setEnabled(enabled: boolean): void;
|
51
58
|
getSelection(): Selection | null;
|
52
59
|
clearSelection(): void;
|
@@ -15,6 +15,7 @@ import Mat33 from '../geometry/Mat33';
|
|
15
15
|
import Rect2 from '../geometry/Rect2';
|
16
16
|
import { Vec2 } from '../geometry/Vec2';
|
17
17
|
import { EditorEventType } from '../types';
|
18
|
+
import Viewport from '../Viewport';
|
18
19
|
import BaseTool from './BaseTool';
|
19
20
|
import { ToolType } from './ToolController';
|
20
21
|
const handleScreenSize = 30;
|
@@ -143,13 +144,13 @@ class Selection {
|
|
143
144
|
this.transform = Mat33.identity;
|
144
145
|
makeDraggable(draggableBackground, (deltaPosition) => {
|
145
146
|
this.handleBackgroundDrag(deltaPosition);
|
146
|
-
}, () => this.
|
147
|
+
}, () => this.finalizeTransform());
|
147
148
|
makeDraggable(resizeCorner, (deltaPosition) => {
|
148
149
|
this.handleResizeCornerDrag(deltaPosition);
|
149
|
-
}, () => this.
|
150
|
+
}, () => this.finalizeTransform());
|
150
151
|
makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
|
151
152
|
this.handleRotateCircleDrag(offset);
|
152
|
-
}, () => this.
|
153
|
+
}, () => this.finalizeTransform());
|
153
154
|
}
|
154
155
|
// Note a small change in the position of this' background while dragging
|
155
156
|
// At the end of a drag, changes should be applied by calling this.finishDragging()
|
@@ -159,9 +160,7 @@ class Selection {
|
|
159
160
|
deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
|
160
161
|
// Snap position to a multiple of 10 (additional decimal points lead to larger files).
|
161
162
|
deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
|
162
|
-
this.
|
163
|
-
this.transform = this.transform.rightMul(Mat33.translation(deltaPosition));
|
164
|
-
this.previewTransformCmds();
|
163
|
+
this.transformPreview(Mat33.translation(deltaPosition));
|
165
164
|
}
|
166
165
|
handleResizeCornerDrag(deltaPosition) {
|
167
166
|
deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
|
@@ -170,18 +169,11 @@ class Selection {
|
|
170
169
|
const oldHeight = this.region.h;
|
171
170
|
const newSize = this.region.size.plus(deltaPosition);
|
172
171
|
if (newSize.y > 0 && newSize.x > 0) {
|
173
|
-
|
174
|
-
|
175
|
-
const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft);
|
176
|
-
this.transform = this.transform.rightMul(currentTransfm);
|
177
|
-
this.previewTransformCmds();
|
172
|
+
const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight);
|
173
|
+
this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft));
|
178
174
|
}
|
179
175
|
}
|
180
176
|
handleRotateCircleDrag(offset) {
|
181
|
-
this.boxRotation = this.boxRotation % (2 * Math.PI);
|
182
|
-
if (this.boxRotation < 0) {
|
183
|
-
this.boxRotation += 2 * Math.PI;
|
184
|
-
}
|
185
177
|
let targetRotation = offset.angle();
|
186
178
|
targetRotation = targetRotation % (2 * Math.PI);
|
187
179
|
if (targetRotation < 0) {
|
@@ -198,17 +190,32 @@ class Selection {
|
|
198
190
|
deltaRotation = Math.floor(Math.abs(deltaRotation) / rotationStep) * rotationStep;
|
199
191
|
deltaRotation *= rotationDirection;
|
200
192
|
}
|
201
|
-
this.
|
202
|
-
this.boxRotation += deltaRotation;
|
203
|
-
this.previewTransformCmds();
|
193
|
+
this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center));
|
204
194
|
}
|
205
195
|
computeTransformCommands() {
|
206
196
|
return this.selectedElems.map(elem => {
|
207
197
|
return elem.transformBy(this.transform);
|
208
198
|
});
|
209
199
|
}
|
200
|
+
// Applies, previews, but doesn't finalize the given transformation.
|
201
|
+
transformPreview(transform) {
|
202
|
+
this.transform = this.transform.rightMul(transform);
|
203
|
+
const deltaRotation = transform.transformVec3(Vec2.unitX).angle();
|
204
|
+
transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center));
|
205
|
+
this.boxRotation += deltaRotation;
|
206
|
+
this.boxRotation = this.boxRotation % (2 * Math.PI);
|
207
|
+
if (this.boxRotation < 0) {
|
208
|
+
this.boxRotation += 2 * Math.PI;
|
209
|
+
}
|
210
|
+
const newSize = transform.transformVec3(this.region.size);
|
211
|
+
const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft);
|
212
|
+
this.region = this.region.resizedTo(newSize);
|
213
|
+
this.region = this.region.translatedBy(translation);
|
214
|
+
this.previewTransformCmds();
|
215
|
+
this.scrollTo();
|
216
|
+
}
|
210
217
|
// Applies the current transformation to the selection
|
211
|
-
|
218
|
+
finalizeTransform() {
|
212
219
|
this.transformationCommands.forEach(cmd => {
|
213
220
|
cmd.unapply(this.editor);
|
214
221
|
});
|
@@ -328,6 +335,16 @@ class Selection {
|
|
328
335
|
this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
|
329
336
|
this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
|
330
337
|
}
|
338
|
+
// Scroll the viewport to this. Does not zoom
|
339
|
+
scrollTo() {
|
340
|
+
const viewport = this.editor.viewport;
|
341
|
+
const visibleRect = viewport.visibleRect;
|
342
|
+
if (!visibleRect.containsPoint(this.region.center)) {
|
343
|
+
const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center);
|
344
|
+
const delta = this.region.center.minus(closestPoint);
|
345
|
+
this.editor.dispatchNoAnnounce(new Viewport.ViewportTransform(Mat33.translation(delta.times(-1))), false);
|
346
|
+
}
|
347
|
+
}
|
331
348
|
deleteSelectedObjects() {
|
332
349
|
return new Erase(this.selectedElems);
|
333
350
|
}
|
@@ -384,6 +401,7 @@ export default class SelectionTool extends BaseTool {
|
|
384
401
|
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.recomputeRegion();
|
385
402
|
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.updateUI();
|
386
403
|
});
|
404
|
+
this.editor.handleKeyEventsFrom(this.handleOverlay);
|
387
405
|
}
|
388
406
|
onPointerDown(event) {
|
389
407
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
@@ -413,6 +431,11 @@ export default class SelectionTool extends BaseTool {
|
|
413
431
|
});
|
414
432
|
if (hasSelection) {
|
415
433
|
this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
|
434
|
+
this.zoomToSelection();
|
435
|
+
}
|
436
|
+
}
|
437
|
+
zoomToSelection() {
|
438
|
+
if (this.selectionBox) {
|
416
439
|
const selectionRect = this.selectionBox.region;
|
417
440
|
this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
|
418
441
|
}
|
@@ -430,12 +453,93 @@ export default class SelectionTool extends BaseTool {
|
|
430
453
|
this.selectionBox = this.prevSelectionBox;
|
431
454
|
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.appendBackgroundBoxTo(this.handleOverlay);
|
432
455
|
}
|
456
|
+
onKeyPress(event) {
|
457
|
+
let rotationSteps = 0;
|
458
|
+
let xTranslateSteps = 0;
|
459
|
+
let yTranslateSteps = 0;
|
460
|
+
let xScaleSteps = 0;
|
461
|
+
let yScaleSteps = 0;
|
462
|
+
switch (event.key) {
|
463
|
+
case 'a':
|
464
|
+
case 'h':
|
465
|
+
case 'ArrowLeft':
|
466
|
+
xTranslateSteps -= 1;
|
467
|
+
break;
|
468
|
+
case 'd':
|
469
|
+
case 'l':
|
470
|
+
case 'ArrowRight':
|
471
|
+
xTranslateSteps += 1;
|
472
|
+
break;
|
473
|
+
case 'q':
|
474
|
+
case 'k':
|
475
|
+
case 'ArrowUp':
|
476
|
+
yTranslateSteps -= 1;
|
477
|
+
break;
|
478
|
+
case 'e':
|
479
|
+
case 'j':
|
480
|
+
case 'ArrowDown':
|
481
|
+
yTranslateSteps += 1;
|
482
|
+
break;
|
483
|
+
case 'r':
|
484
|
+
rotationSteps += 1;
|
485
|
+
break;
|
486
|
+
case 'R':
|
487
|
+
rotationSteps -= 1;
|
488
|
+
break;
|
489
|
+
case 'i':
|
490
|
+
xScaleSteps -= 1;
|
491
|
+
break;
|
492
|
+
case 'I':
|
493
|
+
xScaleSteps += 1;
|
494
|
+
break;
|
495
|
+
case 'o':
|
496
|
+
yScaleSteps -= 1;
|
497
|
+
break;
|
498
|
+
case 'O':
|
499
|
+
yScaleSteps += 1;
|
500
|
+
break;
|
501
|
+
}
|
502
|
+
let handled = xTranslateSteps !== 0
|
503
|
+
|| yTranslateSteps !== 0
|
504
|
+
|| rotationSteps !== 0
|
505
|
+
|| xScaleSteps !== 0
|
506
|
+
|| yScaleSteps !== 0;
|
507
|
+
if (!this.selectionBox) {
|
508
|
+
handled = false;
|
509
|
+
}
|
510
|
+
else if (handled) {
|
511
|
+
const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
|
512
|
+
const rotateStepSize = Math.PI / 8;
|
513
|
+
const scaleStepSize = translateStepSize / 2;
|
514
|
+
const region = this.selectionBox.region;
|
515
|
+
const scaledSize = this.selectionBox.region.size.plus(Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize));
|
516
|
+
const transform = Mat33.scaling2D(Vec2.of(
|
517
|
+
// Don't more-than-half the size of the selection
|
518
|
+
Math.max(0.5, scaledSize.x / region.size.x), Math.max(0.5, scaledSize.y / region.size.y)), region.topLeft).rightMul(Mat33.zRotation(rotationSteps * rotateStepSize, region.center)).rightMul(Mat33.translation(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)));
|
519
|
+
this.selectionBox.transformPreview(transform);
|
520
|
+
}
|
521
|
+
return handled;
|
522
|
+
}
|
523
|
+
onKeyUp(evt) {
|
524
|
+
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
525
|
+
this.selectionBox.finalizeTransform();
|
526
|
+
return true;
|
527
|
+
}
|
528
|
+
return false;
|
529
|
+
}
|
433
530
|
setEnabled(enabled) {
|
434
531
|
super.setEnabled(enabled);
|
435
532
|
// Clear the selection
|
436
533
|
this.handleOverlay.replaceChildren();
|
437
534
|
this.selectionBox = null;
|
438
535
|
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
536
|
+
if (enabled) {
|
537
|
+
this.handleOverlay.tabIndex = 0;
|
538
|
+
this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts;
|
539
|
+
}
|
540
|
+
else {
|
541
|
+
this.handleOverlay.tabIndex = -1;
|
542
|
+
}
|
439
543
|
}
|
440
544
|
// Get the object responsible for displaying this' selection.
|
441
545
|
getSelection() {
|
@@ -451,3 +555,11 @@ export default class SelectionTool extends BaseTool {
|
|
451
555
|
});
|
452
556
|
}
|
453
557
|
}
|
558
|
+
SelectionTool.handleableKeys = [
|
559
|
+
'a', 'h', 'ArrowLeft',
|
560
|
+
'd', 'l', 'ArrowRight',
|
561
|
+
'q', 'k', 'ArrowUp',
|
562
|
+
'e', 'j', 'ArrowDown',
|
563
|
+
'r', 'R',
|
564
|
+
'i', 'I', 'o', 'O',
|
565
|
+
];
|
@@ -23,6 +23,7 @@ export default class ToolController {
|
|
23
23
|
constructor(editor, localization) {
|
24
24
|
const primaryToolEnabledGroup = new ToolEnabledGroup();
|
25
25
|
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
|
26
|
+
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
|
26
27
|
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
|
27
28
|
const primaryTools = [
|
28
29
|
new SelectionTool(editor, localization.selectionTool),
|
@@ -38,6 +39,7 @@ export default class ToolController {
|
|
38
39
|
new PipetteTool(editor, localization.pipetteTool),
|
39
40
|
panZoomTool,
|
40
41
|
...primaryTools,
|
42
|
+
keyboardPanZoomTool,
|
41
43
|
new UndoRedoShortcut(editor),
|
42
44
|
];
|
43
45
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
|
@@ -76,8 +78,9 @@ export default class ToolController {
|
|
76
78
|
this.activeTool = null;
|
77
79
|
handled = true;
|
78
80
|
}
|
79
|
-
else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent) {
|
81
|
+
else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent) {
|
80
82
|
const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
|
83
|
+
const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent;
|
81
84
|
const isWheelEvt = event.kind === InputEvtType.WheelEvt;
|
82
85
|
for (const tool of this.tools) {
|
83
86
|
if (!tool.isEnabled()) {
|
@@ -85,7 +88,8 @@ export default class ToolController {
|
|
85
88
|
}
|
86
89
|
const wheelResult = isWheelEvt && tool.onWheel(event);
|
87
90
|
const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
|
88
|
-
|
91
|
+
const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event);
|
92
|
+
handled = keyPressResult || wheelResult || keyReleaseResult;
|
89
93
|
if (handled) {
|
90
94
|
break;
|
91
95
|
}
|
@@ -7,6 +7,7 @@ export const defaultToolLocalization = {
|
|
7
7
|
undoRedoTool: 'Undo/Redo',
|
8
8
|
rightClickDragPanTool: 'Right-click drag',
|
9
9
|
pipetteTool: 'Pick color from screen',
|
10
|
+
keyboardPanZoom: 'Keyboard pan/zoom shortcuts',
|
10
11
|
textTool: 'Text',
|
11
12
|
enterTextToInsert: 'Text to insert',
|
12
13
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
package/dist/src/types.d.ts
CHANGED
@@ -19,7 +19,8 @@ export declare enum InputEvtType {
|
|
19
19
|
PointerUpEvt = 2,
|
20
20
|
GestureCancelEvt = 3,
|
21
21
|
WheelEvt = 4,
|
22
|
-
KeyPressEvent = 5
|
22
|
+
KeyPressEvent = 5,
|
23
|
+
KeyUpEvent = 6
|
23
24
|
}
|
24
25
|
export interface WheelEvt {
|
25
26
|
readonly kind: InputEvtType.WheelEvt;
|
@@ -31,6 +32,11 @@ export interface KeyPressEvent {
|
|
31
32
|
readonly key: string;
|
32
33
|
readonly ctrlKey: boolean;
|
33
34
|
}
|
35
|
+
export interface KeyUpEvent {
|
36
|
+
readonly kind: InputEvtType.KeyUpEvent;
|
37
|
+
readonly key: string;
|
38
|
+
readonly ctrlKey: boolean;
|
39
|
+
}
|
34
40
|
export interface GestureCancelEvt {
|
35
41
|
readonly kind: InputEvtType.GestureCancelEvt;
|
36
42
|
}
|
@@ -48,7 +54,7 @@ export interface PointerUpEvt extends PointerEvtBase {
|
|
48
54
|
readonly kind: InputEvtType.PointerUpEvt;
|
49
55
|
}
|
50
56
|
export declare type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
|
51
|
-
export declare type InputEvt = KeyPressEvent | WheelEvt | GestureCancelEvt | PointerEvt;
|
57
|
+
export declare type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt;
|
52
58
|
export declare type EditorNotifier = EventDispatcher<EditorEventType, EditorEventDataType>;
|
53
59
|
export declare enum EditorEventType {
|
54
60
|
ToolEnabled = 0,
|
@@ -73,6 +79,7 @@ export interface EditorObjectEvent {
|
|
73
79
|
export interface EditorViewportChangedEvent {
|
74
80
|
readonly kind: EditorEventType.ViewportChanged;
|
75
81
|
readonly newTransform: Mat33;
|
82
|
+
readonly oldTransform: Mat33;
|
76
83
|
}
|
77
84
|
export interface DisplayResizedEvent {
|
78
85
|
readonly kind: EditorEventType.DisplayResized;
|
package/dist/src/types.js
CHANGED
@@ -7,6 +7,7 @@ export var InputEvtType;
|
|
7
7
|
InputEvtType[InputEvtType["GestureCancelEvt"] = 3] = "GestureCancelEvt";
|
8
8
|
InputEvtType[InputEvtType["WheelEvt"] = 4] = "WheelEvt";
|
9
9
|
InputEvtType[InputEvtType["KeyPressEvent"] = 5] = "KeyPressEvent";
|
10
|
+
InputEvtType[InputEvtType["KeyUpEvent"] = 6] = "KeyUpEvent";
|
10
11
|
})(InputEvtType || (InputEvtType = {}));
|
11
12
|
export var EditorEventType;
|
12
13
|
(function (EditorEventType) {
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.11",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "dist/src/Editor.js",
|
6
6
|
"types": "dist/src/Editor.d.ts",
|
@@ -9,6 +9,14 @@
|
|
9
9
|
"types": "./dist/src/Editor.d.ts",
|
10
10
|
"default": "./dist/src/Editor.js"
|
11
11
|
},
|
12
|
+
"./localizations/getLocalizationTable": {
|
13
|
+
"types": "./dist/src/localizations/getLocalizationTable.d.ts",
|
14
|
+
"default": "./dist/src/localizations/getLocalizationTable.js"
|
15
|
+
},
|
16
|
+
"./getLocalizationTable": {
|
17
|
+
"types": "./dist/src/localizations/getLocalizationTable.d.ts",
|
18
|
+
"default": "./dist/src/localizations/getLocalizationTable.js"
|
19
|
+
},
|
12
20
|
"./styles": {
|
13
21
|
"default": "./src/styles.js"
|
14
22
|
},
|
package/src/Editor.ts
CHANGED
@@ -17,7 +17,8 @@ import SVGLoader from './SVGLoader';
|
|
17
17
|
import Pointer from './Pointer';
|
18
18
|
import Mat33 from './geometry/Mat33';
|
19
19
|
import Rect2 from './geometry/Rect2';
|
20
|
-
import {
|
20
|
+
import { EditorLocalization } from './localization';
|
21
|
+
import getLocalizationTable from './localizations/getLocalizationTable';
|
21
22
|
|
22
23
|
export interface EditorSettings {
|
23
24
|
// Defaults to RenderingMode.CanvasRenderer
|
@@ -30,6 +31,9 @@ export interface EditorSettings {
|
|
30
31
|
// This does not include pinch-zoom events.
|
31
32
|
// Defaults to true.
|
32
33
|
wheelEventsEnabled: boolean|'only-if-focused';
|
34
|
+
|
35
|
+
minZoom: number,
|
36
|
+
maxZoom: number,
|
33
37
|
}
|
34
38
|
|
35
39
|
export class Editor {
|
@@ -43,7 +47,7 @@ export class Editor {
|
|
43
47
|
|
44
48
|
// Viewport for the exported/imported image
|
45
49
|
private importExportViewport: Viewport;
|
46
|
-
public localization: EditorLocalization
|
50
|
+
public localization: EditorLocalization;
|
47
51
|
|
48
52
|
public viewport: Viewport;
|
49
53
|
public toolController: ToolController;
|
@@ -59,7 +63,7 @@ export class Editor {
|
|
59
63
|
settings: Partial<EditorSettings> = {},
|
60
64
|
) {
|
61
65
|
this.localization = {
|
62
|
-
...
|
66
|
+
...getLocalizationTable(),
|
63
67
|
...settings.localization,
|
64
68
|
};
|
65
69
|
|
@@ -68,6 +72,8 @@ export class Editor {
|
|
68
72
|
wheelEventsEnabled: settings.wheelEventsEnabled ?? true,
|
69
73
|
renderingMode: settings.renderingMode ?? RenderingMode.CanvasRenderer,
|
70
74
|
localization: this.localization,
|
75
|
+
minZoom: settings.minZoom ?? 2e-10,
|
76
|
+
maxZoom: settings.maxZoom ?? 1e12,
|
71
77
|
};
|
72
78
|
|
73
79
|
this.container = document.createElement('div');
|
@@ -110,6 +116,24 @@ export class Editor {
|
|
110
116
|
this.registerListeners();
|
111
117
|
this.queueRerender();
|
112
118
|
this.hideLoadingWarning();
|
119
|
+
|
120
|
+
|
121
|
+
// Enforce zoom limits.
|
122
|
+
this.notifier.on(EditorEventType.ViewportChanged, evt => {
|
123
|
+
if (evt.kind === EditorEventType.ViewportChanged) {
|
124
|
+
const zoom = evt.newTransform.transformVec3(Vec2.unitX).length();
|
125
|
+
if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
|
126
|
+
const oldZoom = evt.oldTransform.transformVec3(Vec2.unitX).length();
|
127
|
+
let resetTransform = Mat33.identity;
|
128
|
+
|
129
|
+
if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
|
130
|
+
resetTransform = evt.oldTransform;
|
131
|
+
}
|
132
|
+
|
133
|
+
this.viewport.resetTransform(resetTransform);
|
134
|
+
}
|
135
|
+
}
|
136
|
+
});
|
113
137
|
}
|
114
138
|
|
115
139
|
// Returns a reference to this' container.
|
@@ -238,17 +262,7 @@ export class Editor {
|
|
238
262
|
pointerEnd(evt);
|
239
263
|
});
|
240
264
|
|
241
|
-
this.renderingRegion
|
242
|
-
if (this.toolController.dispatchInputEvent({
|
243
|
-
kind: InputEvtType.KeyPressEvent,
|
244
|
-
key: evt.key,
|
245
|
-
ctrlKey: evt.ctrlKey,
|
246
|
-
})) {
|
247
|
-
evt.preventDefault();
|
248
|
-
} else if (evt.key === 'Escape') {
|
249
|
-
this.renderingRegion.blur();
|
250
|
-
}
|
251
|
-
});
|
265
|
+
this.handleKeyEventsFrom(this.renderingRegion);
|
252
266
|
|
253
267
|
this.container.addEventListener('wheel', evt => {
|
254
268
|
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
@@ -307,6 +321,32 @@ export class Editor {
|
|
307
321
|
});
|
308
322
|
}
|
309
323
|
|
324
|
+
// Adds event listners for keypresses to [elem] and forwards those events to the
|
325
|
+
// editor.
|
326
|
+
public handleKeyEventsFrom(elem: HTMLElement) {
|
327
|
+
elem.addEventListener('keydown', evt => {
|
328
|
+
if (this.toolController.dispatchInputEvent({
|
329
|
+
kind: InputEvtType.KeyPressEvent,
|
330
|
+
key: evt.key,
|
331
|
+
ctrlKey: evt.ctrlKey,
|
332
|
+
})) {
|
333
|
+
evt.preventDefault();
|
334
|
+
} else if (evt.key === 'Escape') {
|
335
|
+
this.renderingRegion.blur();
|
336
|
+
}
|
337
|
+
});
|
338
|
+
|
339
|
+
elem.addEventListener('keyup', evt => {
|
340
|
+
if (this.toolController.dispatchInputEvent({
|
341
|
+
kind: InputEvtType.KeyUpEvent,
|
342
|
+
key: evt.key,
|
343
|
+
ctrlKey: evt.ctrlKey,
|
344
|
+
})) {
|
345
|
+
evt.preventDefault();
|
346
|
+
}
|
347
|
+
});
|
348
|
+
}
|
349
|
+
|
310
350
|
// Adds to history by default
|
311
351
|
public dispatch(command: Command, addToHistory: boolean = true) {
|
312
352
|
if (addToHistory) {
|
package/src/UndoRedoHistory.ts
CHANGED
package/src/Viewport.ts
CHANGED
@@ -103,11 +103,13 @@ export class Viewport {
|
|
103
103
|
// Updates the transformation directly. Using ViewportTransform is preferred.
|
104
104
|
// [newTransform] should map from canvas coordinates to screen coordinates.
|
105
105
|
public resetTransform(newTransform: Mat33 = Mat33.identity) {
|
106
|
+
const oldTransform = this.transform;
|
106
107
|
this.transform = newTransform;
|
107
108
|
this.inverseTransform = newTransform.inverse();
|
108
109
|
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
109
110
|
kind: EditorEventType.ViewportChanged,
|
110
111
|
newTransform,
|
112
|
+
oldTransform,
|
111
113
|
});
|
112
114
|
}
|
113
115
|
|
package/src/bundle/bundled.ts
CHANGED
@@ -74,4 +74,19 @@ describe('Line2', () => {
|
|
74
74
|
expect(line1.intersection(line2)).toBeNull();
|
75
75
|
expect(line2.intersection(line1)).toBeNull();
|
76
76
|
});
|
77
|
+
|
78
|
+
it('Closest point to (0,0) on the line x = 1 should be (1,0)', () => {
|
79
|
+
const line = new LineSegment2(Vec2.of(1, 100), Vec2.of(1, -100));
|
80
|
+
expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
|
81
|
+
});
|
82
|
+
|
83
|
+
it('Closest point from (-1,2) to segment((1,1) -> (2,4)) should be (1,1)', () => {
|
84
|
+
const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
|
85
|
+
expect(line.closestPointTo(Vec2.of(-1, 2))).objEq(Vec2.of(1, 1));
|
86
|
+
});
|
87
|
+
|
88
|
+
it('Closest point from (5,2) to segment((1,1) -> (2,4)) should be (2,4)', () => {
|
89
|
+
const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
|
90
|
+
expect(line.closestPointTo(Vec2.of(5, 2))).objEq(Vec2.of(2, 4));
|
91
|
+
});
|
77
92
|
});
|
@@ -7,6 +7,7 @@ interface IntersectionResult {
|
|
7
7
|
}
|
8
8
|
|
9
9
|
export default class LineSegment2 {
|
10
|
+
// invariant: ||direction|| = 1
|
10
11
|
public readonly direction: Vec2;
|
11
12
|
public readonly length: number;
|
12
13
|
public readonly bbox;
|
@@ -124,4 +125,23 @@ export default class LineSegment2 {
|
|
124
125
|
t: resultT,
|
125
126
|
};
|
126
127
|
}
|
128
|
+
|
129
|
+
// Returns the closest point on this to [target]
|
130
|
+
public closestPointTo(target: Point2) {
|
131
|
+
// Distance from P1 along this' direction.
|
132
|
+
const projectedDistFromP1 = target.minus(this.p1).dot(this.direction);
|
133
|
+
const projectedDistFromP2 = this.length - projectedDistFromP1;
|
134
|
+
|
135
|
+
const projection = this.p1.plus(this.direction.times(projectedDistFromP1));
|
136
|
+
|
137
|
+
if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) {
|
138
|
+
return projection;
|
139
|
+
}
|
140
|
+
|
141
|
+
if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) {
|
142
|
+
return this.p2;
|
143
|
+
} else {
|
144
|
+
return this.p1;
|
145
|
+
}
|
146
|
+
}
|
127
147
|
}
|