js-draw 0.0.1
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/.eslintrc.js +57 -0
- package/.husky/pre-commit +4 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/__mocks__/coloris.ts +8 -0
- package/__mocks__/styleMock.js +1 -0
- package/dist/__mocks__/coloris.d.ts +2 -0
- package/dist/__mocks__/coloris.js +5 -0
- package/dist/build_tools/BundledFile.d.ts +12 -0
- package/dist/build_tools/BundledFile.js +153 -0
- package/dist/scripts/bundle.d.ts +1 -0
- package/dist/scripts/bundle.js +19 -0
- package/dist/scripts/watchBundle.d.ts +1 -0
- package/dist/scripts/watchBundle.js +9 -0
- package/dist/src/Color4.d.ts +23 -0
- package/dist/src/Color4.js +102 -0
- package/dist/src/Display.d.ts +22 -0
- package/dist/src/Display.js +93 -0
- package/dist/src/Editor.d.ts +55 -0
- package/dist/src/Editor.js +366 -0
- package/dist/src/EditorImage.d.ts +44 -0
- package/dist/src/EditorImage.js +243 -0
- package/dist/src/EventDispatcher.d.ts +11 -0
- package/dist/src/EventDispatcher.js +39 -0
- package/dist/src/Pointer.d.ts +22 -0
- package/dist/src/Pointer.js +57 -0
- package/dist/src/SVGLoader.d.ts +21 -0
- package/dist/src/SVGLoader.js +204 -0
- package/dist/src/StrokeBuilder.d.ts +35 -0
- package/dist/src/StrokeBuilder.js +275 -0
- package/dist/src/UndoRedoHistory.d.ts +17 -0
- package/dist/src/UndoRedoHistory.js +46 -0
- package/dist/src/Viewport.d.ts +39 -0
- package/dist/src/Viewport.js +134 -0
- package/dist/src/commands/Command.d.ts +15 -0
- package/dist/src/commands/Command.js +29 -0
- package/dist/src/commands/Erase.d.ts +11 -0
- package/dist/src/commands/Erase.js +37 -0
- package/dist/src/commands/localization.d.ts +19 -0
- package/dist/src/commands/localization.js +17 -0
- package/dist/src/components/AbstractComponent.d.ts +19 -0
- package/dist/src/components/AbstractComponent.js +46 -0
- package/dist/src/components/Stroke.d.ts +16 -0
- package/dist/src/components/Stroke.js +79 -0
- package/dist/src/components/UnknownSVGObject.d.ts +15 -0
- package/dist/src/components/UnknownSVGObject.js +25 -0
- package/dist/src/components/localization.d.ts +5 -0
- package/dist/src/components/localization.js +4 -0
- package/dist/src/geometry/LineSegment2.d.ts +19 -0
- package/dist/src/geometry/LineSegment2.js +100 -0
- package/dist/src/geometry/Mat33.d.ts +31 -0
- package/dist/src/geometry/Mat33.js +187 -0
- package/dist/src/geometry/Path.d.ts +55 -0
- package/dist/src/geometry/Path.js +364 -0
- package/dist/src/geometry/Rect2.d.ts +47 -0
- package/dist/src/geometry/Rect2.js +148 -0
- package/dist/src/geometry/Vec2.d.ts +13 -0
- package/dist/src/geometry/Vec2.js +13 -0
- package/dist/src/geometry/Vec3.d.ts +32 -0
- package/dist/src/geometry/Vec3.js +98 -0
- package/dist/src/localization.d.ts +12 -0
- package/dist/src/localization.js +5 -0
- package/dist/src/main.d.ts +3 -0
- package/dist/src/main.js +4 -0
- package/dist/src/rendering/AbstractRenderer.d.ts +38 -0
- package/dist/src/rendering/AbstractRenderer.js +108 -0
- package/dist/src/rendering/CanvasRenderer.d.ts +23 -0
- package/dist/src/rendering/CanvasRenderer.js +108 -0
- package/dist/src/rendering/DummyRenderer.d.ts +25 -0
- package/dist/src/rendering/DummyRenderer.js +65 -0
- package/dist/src/rendering/SVGRenderer.d.ts +27 -0
- package/dist/src/rendering/SVGRenderer.js +122 -0
- package/dist/src/testing/loadExpectExtensions.d.ts +17 -0
- package/dist/src/testing/loadExpectExtensions.js +27 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +12 -0
- package/dist/src/toolbar/HTMLToolbar.js +444 -0
- package/dist/src/toolbar/types.d.ts +17 -0
- package/dist/src/toolbar/types.js +5 -0
- package/dist/src/tools/BaseTool.d.ts +20 -0
- package/dist/src/tools/BaseTool.js +44 -0
- package/dist/src/tools/Eraser.d.ts +16 -0
- package/dist/src/tools/Eraser.js +53 -0
- package/dist/src/tools/PanZoom.d.ts +40 -0
- package/dist/src/tools/PanZoom.js +191 -0
- package/dist/src/tools/Pen.d.ts +25 -0
- package/dist/src/tools/Pen.js +97 -0
- package/dist/src/tools/SelectionTool.d.ts +49 -0
- package/dist/src/tools/SelectionTool.js +437 -0
- package/dist/src/tools/ToolController.d.ts +18 -0
- package/dist/src/tools/ToolController.js +110 -0
- package/dist/src/tools/ToolEnabledGroup.d.ts +6 -0
- package/dist/src/tools/ToolEnabledGroup.js +11 -0
- package/dist/src/tools/localization.d.ts +10 -0
- package/dist/src/tools/localization.js +9 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +20 -0
- package/jest.config.js +22 -0
- package/lint-staged.config.js +6 -0
- package/package.json +82 -0
- package/src/Color4.test.ts +12 -0
- package/src/Color4.ts +122 -0
- package/src/Display.ts +118 -0
- package/src/Editor.css +58 -0
- package/src/Editor.ts +469 -0
- package/src/EditorImage.test.ts +90 -0
- package/src/EditorImage.ts +297 -0
- package/src/EventDispatcher.test.ts +123 -0
- package/src/EventDispatcher.ts +53 -0
- package/src/Pointer.ts +93 -0
- package/src/SVGLoader.ts +230 -0
- package/src/StrokeBuilder.ts +362 -0
- package/src/UndoRedoHistory.ts +61 -0
- package/src/Viewport.ts +168 -0
- package/src/commands/Command.ts +43 -0
- package/src/commands/Erase.ts +52 -0
- package/src/commands/localization.ts +38 -0
- package/src/components/AbstractComponent.ts +73 -0
- package/src/components/Stroke.test.ts +18 -0
- package/src/components/Stroke.ts +102 -0
- package/src/components/UnknownSVGObject.ts +36 -0
- package/src/components/localization.ts +9 -0
- package/src/editorStyles.js +3 -0
- package/src/geometry/LineSegment2.test.ts +77 -0
- package/src/geometry/LineSegment2.ts +127 -0
- package/src/geometry/Mat33.test.ts +144 -0
- package/src/geometry/Mat33.ts +268 -0
- package/src/geometry/Path.fromString.test.ts +146 -0
- package/src/geometry/Path.test.ts +96 -0
- package/src/geometry/Path.toString.test.ts +31 -0
- package/src/geometry/Path.ts +456 -0
- package/src/geometry/Rect2.test.ts +121 -0
- package/src/geometry/Rect2.ts +215 -0
- package/src/geometry/Vec2.test.ts +32 -0
- package/src/geometry/Vec2.ts +18 -0
- package/src/geometry/Vec3.test.ts +29 -0
- package/src/geometry/Vec3.ts +133 -0
- package/src/localization.ts +27 -0
- package/src/rendering/AbstractRenderer.ts +164 -0
- package/src/rendering/CanvasRenderer.ts +141 -0
- package/src/rendering/DummyRenderer.ts +80 -0
- package/src/rendering/SVGRenderer.ts +159 -0
- package/src/testing/loadExpectExtensions.ts +43 -0
- package/src/toolbar/HTMLToolbar.ts +551 -0
- package/src/toolbar/toolbar.css +110 -0
- package/src/toolbar/types.ts +20 -0
- package/src/tools/BaseTool.ts +58 -0
- package/src/tools/Eraser.ts +67 -0
- package/src/tools/PanZoom.ts +253 -0
- package/src/tools/Pen.ts +121 -0
- package/src/tools/SelectionTool.test.ts +85 -0
- package/src/tools/SelectionTool.ts +545 -0
- package/src/tools/ToolController.ts +126 -0
- package/src/tools/ToolEnabledGroup.ts +14 -0
- package/src/tools/localization.ts +22 -0
- package/src/types.ts +133 -0
- package/tsconfig.json +28 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types';
|
2
|
+
import { ToolType } from './ToolController';
|
3
|
+
import ToolEnabledGroup from './ToolEnabledGroup';
|
4
|
+
|
5
|
+
export default abstract class BaseTool implements PointerEvtListener {
|
6
|
+
private enabled: boolean = true;
|
7
|
+
private group: ToolEnabledGroup|null = null;
|
8
|
+
|
9
|
+
public abstract onPointerDown(event: PointerEvt): boolean;
|
10
|
+
public abstract onPointerMove(event: PointerEvt): void;
|
11
|
+
public abstract onPointerUp(event: PointerEvt): void;
|
12
|
+
public abstract onGestureCancel(): void;
|
13
|
+
public abstract readonly kind: ToolType;
|
14
|
+
|
15
|
+
protected constructor(private notifier: EditorNotifier, public readonly description: string) {
|
16
|
+
}
|
17
|
+
|
18
|
+
public onWheel(_event: WheelEvt): boolean {
|
19
|
+
return false;
|
20
|
+
}
|
21
|
+
|
22
|
+
public onKeyPress(_event: KeyPressEvent): boolean {
|
23
|
+
return false;
|
24
|
+
}
|
25
|
+
|
26
|
+
public setEnabled(enabled: boolean) {
|
27
|
+
this.enabled = enabled;
|
28
|
+
|
29
|
+
// Ensure that at most one tool in the group is enabled.
|
30
|
+
if (enabled) {
|
31
|
+
this.group?.notifyEnabled(this);
|
32
|
+
this.notifier.dispatch(EditorEventType.ToolEnabled, {
|
33
|
+
kind: EditorEventType.ToolEnabled,
|
34
|
+
tool: this,
|
35
|
+
});
|
36
|
+
} else {
|
37
|
+
this.notifier.dispatch(EditorEventType.ToolDisabled, {
|
38
|
+
kind: EditorEventType.ToolDisabled,
|
39
|
+
tool: this,
|
40
|
+
});
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
public isEnabled(): boolean {
|
45
|
+
return this.enabled;
|
46
|
+
}
|
47
|
+
|
48
|
+
// Connect this tool to a set of other tools, ensuring that at most one
|
49
|
+
// of the tools in the group is enabled.
|
50
|
+
public setToolGroup(group: ToolEnabledGroup) {
|
51
|
+
if (this.isEnabled()) {
|
52
|
+
group.notifyEnabled(this);
|
53
|
+
}
|
54
|
+
|
55
|
+
this.group = group;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import { PointerEvt } from '../types';
|
2
|
+
import BaseTool from './BaseTool';
|
3
|
+
import Editor from '../Editor';
|
4
|
+
import { Point2 } from '../geometry/Vec2';
|
5
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
6
|
+
import Erase from '../commands/Erase';
|
7
|
+
import { ToolType } from './ToolController';
|
8
|
+
import AbstractComponent from '../components/AbstractComponent';
|
9
|
+
import { PointerDevice } from '../Pointer';
|
10
|
+
|
11
|
+
export default class Eraser extends BaseTool {
|
12
|
+
private lastPoint: Point2;
|
13
|
+
private command: Erase|null = null;
|
14
|
+
public kind: ToolType = ToolType.Eraser;
|
15
|
+
private toRemove: AbstractComponent[];
|
16
|
+
|
17
|
+
public constructor(private editor: Editor, description: string) {
|
18
|
+
super(editor.notifier, description);
|
19
|
+
}
|
20
|
+
|
21
|
+
public onPointerDown(event: PointerEvt): boolean {
|
22
|
+
if (event.allPointers.length === 1 || event.current.device === PointerDevice.Eraser) {
|
23
|
+
this.lastPoint = event.current.canvasPos;
|
24
|
+
this.toRemove = [];
|
25
|
+
return true;
|
26
|
+
}
|
27
|
+
|
28
|
+
return false;
|
29
|
+
}
|
30
|
+
|
31
|
+
public onPointerMove(event: PointerEvt): void {
|
32
|
+
const currentPoint = event.current.canvasPos;
|
33
|
+
if (currentPoint.minus(this.lastPoint).magnitude() === 0) {
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
|
37
|
+
const line = new LineSegment2(this.lastPoint, currentPoint);
|
38
|
+
const region = line.bbox;
|
39
|
+
|
40
|
+
// Remove any intersecting elements.
|
41
|
+
this.toRemove.push(...this.editor.image
|
42
|
+
.getElementsIntersectingRegion(region).filter(component => {
|
43
|
+
return component.intersects(line);
|
44
|
+
}));
|
45
|
+
|
46
|
+
this.command?.unapply(this.editor);
|
47
|
+
this.command = new Erase(this.toRemove);
|
48
|
+
this.command.apply(this.editor);
|
49
|
+
|
50
|
+
this.lastPoint = currentPoint;
|
51
|
+
}
|
52
|
+
|
53
|
+
public onPointerUp(_event: PointerEvt): void {
|
54
|
+
if (this.command && this.toRemove.length > 0) {
|
55
|
+
this.command?.unapply(this.editor);
|
56
|
+
|
57
|
+
// Dispatch the command to make it undo-able
|
58
|
+
this.editor.dispatch(this.command);
|
59
|
+
}
|
60
|
+
this.command = null;
|
61
|
+
}
|
62
|
+
|
63
|
+
public onGestureCancel(): void {
|
64
|
+
this.command?.unapply(this.editor);
|
65
|
+
this.command = null;
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,253 @@
|
|
1
|
+
|
2
|
+
import { Editor } from '../Editor';
|
3
|
+
import Mat33 from '../geometry/Mat33';
|
4
|
+
import { Point2, Vec2 } from '../geometry/Vec2';
|
5
|
+
import Vec3 from '../geometry/Vec3';
|
6
|
+
import Pointer, { PointerDevice } from '../Pointer';
|
7
|
+
import { KeyPressEvent, PointerEvt, WheelEvt } from '../types';
|
8
|
+
import { Viewport } from '../Viewport';
|
9
|
+
import BaseTool from './BaseTool';
|
10
|
+
import { ToolType } from './ToolController';
|
11
|
+
|
12
|
+
interface PinchData {
|
13
|
+
canvasCenter: Point2;
|
14
|
+
screenCenter: Point2;
|
15
|
+
angle: number;
|
16
|
+
dist: number;
|
17
|
+
}
|
18
|
+
|
19
|
+
export enum PanZoomMode {
|
20
|
+
// Handle one-pointer gestures (touchscreen only unless AnyDevice is set)
|
21
|
+
OneFingerGestures = 0x1,
|
22
|
+
|
23
|
+
// Handle two-pointer gestures (touchscreen only unless AnyDevice is set)
|
24
|
+
TwoFingerGestures = 0x1 << 1,
|
25
|
+
|
26
|
+
// / Handle gestures from any device, rather than just touch
|
27
|
+
AnyDevice = 0x1 << 2,
|
28
|
+
}
|
29
|
+
|
30
|
+
export default class PanZoom extends BaseTool {
|
31
|
+
public readonly kind: ToolType.PanZoom|ToolType.TouchPanZoom = ToolType.PanZoom;
|
32
|
+
private transform: Viewport.ViewportTransform|null = null;
|
33
|
+
|
34
|
+
private lastAngle: number;
|
35
|
+
private lastDist: number;
|
36
|
+
private lastScreenCenter: Point2;
|
37
|
+
|
38
|
+
public constructor(private editor: Editor, private mode: PanZoomMode, description: string) {
|
39
|
+
super(editor.notifier, description);
|
40
|
+
|
41
|
+
if (mode === PanZoomMode.OneFingerGestures) {
|
42
|
+
this.kind = ToolType.TouchPanZoom;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
// Returns information about the pointers in a gesture
|
47
|
+
public computePinchData(p1: Pointer, p2: Pointer): PinchData {
|
48
|
+
const screenBetween = p2.screenPos.minus(p1.screenPos);
|
49
|
+
const angle = screenBetween.angle();
|
50
|
+
const dist = screenBetween.magnitude();
|
51
|
+
const canvasCenter = p2.canvasPos.plus(p1.canvasPos).times(0.5);
|
52
|
+
const screenCenter = p2.screenPos.plus(p1.screenPos).times(0.5);
|
53
|
+
|
54
|
+
return { canvasCenter, screenCenter, angle, dist };
|
55
|
+
}
|
56
|
+
|
57
|
+
private pointersHaveCorrectDeviceType(pointers: Pointer[]) {
|
58
|
+
return this.mode & PanZoomMode.AnyDevice || pointers.every(
|
59
|
+
pointer => pointer.device === PointerDevice.Touch
|
60
|
+
);
|
61
|
+
}
|
62
|
+
|
63
|
+
public onPointerDown({ allPointers }: PointerEvt): boolean {
|
64
|
+
let handlingGesture = false;
|
65
|
+
|
66
|
+
if (!this.pointersHaveCorrectDeviceType(allPointers)) {
|
67
|
+
handlingGesture = false;
|
68
|
+
} else if (allPointers.length === 2 && this.mode & PanZoomMode.TwoFingerGestures) {
|
69
|
+
const { screenCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
70
|
+
this.lastAngle = angle;
|
71
|
+
this.lastDist = dist;
|
72
|
+
this.lastScreenCenter = screenCenter;
|
73
|
+
handlingGesture = true;
|
74
|
+
} else if (allPointers.length === 1 && this.mode & PanZoomMode.OneFingerGestures) {
|
75
|
+
this.lastScreenCenter = allPointers[0].screenPos;
|
76
|
+
handlingGesture = true;
|
77
|
+
}
|
78
|
+
|
79
|
+
if (handlingGesture) {
|
80
|
+
this.transform ??= new Viewport.ViewportTransform(Mat33.identity);
|
81
|
+
}
|
82
|
+
|
83
|
+
return handlingGesture;
|
84
|
+
}
|
85
|
+
|
86
|
+
// Returns the change in position of the center of the given group of pointers.
|
87
|
+
// Assumes this.lastScreenCenter has been set appropriately.
|
88
|
+
private getCenterDelta(screenCenter: Point2): Vec2 {
|
89
|
+
// Use transformVec3 to avoid translating the delta
|
90
|
+
const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenCenter.minus(this.lastScreenCenter));
|
91
|
+
return delta;
|
92
|
+
}
|
93
|
+
|
94
|
+
private handleTwoFingerMove(allPointers: Pointer[]) {
|
95
|
+
const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
96
|
+
|
97
|
+
const delta = this.getCenterDelta(screenCenter);
|
98
|
+
|
99
|
+
const transformUpdate = Mat33.translation(delta)
|
100
|
+
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
101
|
+
.rightMul(Mat33.zRotation(angle - this.lastAngle, canvasCenter));
|
102
|
+
this.lastScreenCenter = screenCenter;
|
103
|
+
this.lastDist = dist;
|
104
|
+
this.lastAngle = angle;
|
105
|
+
this.transform = new Viewport.ViewportTransform(
|
106
|
+
this.transform!.transform.rightMul(transformUpdate)
|
107
|
+
);
|
108
|
+
}
|
109
|
+
|
110
|
+
private handleOneFingerMove(pointer: Pointer) {
|
111
|
+
const delta = this.getCenterDelta(pointer.screenPos);
|
112
|
+
this.transform = new Viewport.ViewportTransform(
|
113
|
+
this.transform!.transform.rightMul(
|
114
|
+
Mat33.translation(delta)
|
115
|
+
)
|
116
|
+
);
|
117
|
+
this.lastScreenCenter = pointer.screenPos;
|
118
|
+
}
|
119
|
+
|
120
|
+
public onPointerMove({ allPointers }: PointerEvt): void {
|
121
|
+
this.transform ??= new Viewport.ViewportTransform(Mat33.identity);
|
122
|
+
|
123
|
+
const lastTransform = this.transform;
|
124
|
+
if (allPointers.length === 2 && this.mode & PanZoomMode.TwoFingerGestures) {
|
125
|
+
this.handleTwoFingerMove(allPointers);
|
126
|
+
} else if (allPointers.length === 1 && this.mode & PanZoomMode.OneFingerGestures) {
|
127
|
+
this.handleOneFingerMove(allPointers[0]);
|
128
|
+
}
|
129
|
+
lastTransform.unapply(this.editor);
|
130
|
+
this.transform.apply(this.editor);
|
131
|
+
}
|
132
|
+
|
133
|
+
public onPointerUp(_event: PointerEvt): void {
|
134
|
+
if (this.transform) {
|
135
|
+
this.transform.unapply(this.editor);
|
136
|
+
this.editor.dispatch(this.transform, false);
|
137
|
+
}
|
138
|
+
|
139
|
+
this.transform = null;
|
140
|
+
}
|
141
|
+
|
142
|
+
public onGestureCancel(): void {
|
143
|
+
this.transform?.unapply(this.editor);
|
144
|
+
this.transform = null;
|
145
|
+
}
|
146
|
+
|
147
|
+
// Applies [transformUpdate] to the editor. This stacks on top of the
|
148
|
+
// current transformation, if it exists.
|
149
|
+
private updateTransform(transformUpdate: Mat33) {
|
150
|
+
let newTransform = transformUpdate;
|
151
|
+
if (this.transform) {
|
152
|
+
newTransform = this.transform.transform.rightMul(transformUpdate);
|
153
|
+
}
|
154
|
+
|
155
|
+
this.transform?.unapply(this.editor);
|
156
|
+
this.transform = new Viewport.ViewportTransform(newTransform);
|
157
|
+
this.transform.apply(this.editor);
|
158
|
+
}
|
159
|
+
|
160
|
+
public onWheel({ delta, screenPos }: WheelEvt): boolean {
|
161
|
+
if (this.transform === null) {
|
162
|
+
this.transform = new Viewport.ViewportTransform(Mat33.identity);
|
163
|
+
}
|
164
|
+
|
165
|
+
const canvasPos = this.editor.viewport.screenToCanvas(screenPos);
|
166
|
+
const toCanvas = this.editor.viewport.screenToCanvasTransform;
|
167
|
+
|
168
|
+
// Transform without including translation
|
169
|
+
const translation =
|
170
|
+
toCanvas.transformVec3(
|
171
|
+
Vec3.of(-delta.x, -delta.y, 0)
|
172
|
+
);
|
173
|
+
const pinchZoomScaleFactor = 1.04;
|
174
|
+
const transformUpdate = Mat33.scaling2D(
|
175
|
+
Math.pow(pinchZoomScaleFactor, -delta.z), canvasPos
|
176
|
+
).rightMul(
|
177
|
+
Mat33.translation(translation)
|
178
|
+
);
|
179
|
+
this.updateTransform(transformUpdate);
|
180
|
+
|
181
|
+
return true;
|
182
|
+
}
|
183
|
+
|
184
|
+
public onKeyPress({ key }: KeyPressEvent): boolean {
|
185
|
+
let translation = Vec2.zero;
|
186
|
+
let scale = 1;
|
187
|
+
let rotation = 0;
|
188
|
+
|
189
|
+
// Keyboard shortcut handling
|
190
|
+
switch (key) {
|
191
|
+
case 'a':
|
192
|
+
case 'h':
|
193
|
+
case 'ArrowLeft':
|
194
|
+
translation = Vec2.of(-1, 0);
|
195
|
+
break;
|
196
|
+
case 'd':
|
197
|
+
case 'l':
|
198
|
+
case 'ArrowRight':
|
199
|
+
translation = Vec2.of(1, 0);
|
200
|
+
break;
|
201
|
+
case 'k':
|
202
|
+
case 'ArrowUp':
|
203
|
+
translation = Vec2.of(0, -1);
|
204
|
+
break;
|
205
|
+
case 'j':
|
206
|
+
case 'ArrowDown':
|
207
|
+
translation = Vec2.of(0, 1);
|
208
|
+
break;
|
209
|
+
case 'w':
|
210
|
+
scale = 1 / 2;
|
211
|
+
break;
|
212
|
+
case 's':
|
213
|
+
scale = 2;
|
214
|
+
break;
|
215
|
+
case 'r':
|
216
|
+
rotation = 1;
|
217
|
+
break;
|
218
|
+
case 'R':
|
219
|
+
rotation = -1;
|
220
|
+
break;
|
221
|
+
default:
|
222
|
+
return false;
|
223
|
+
}
|
224
|
+
|
225
|
+
// For each keypress,
|
226
|
+
translation = translation.times(30); // Move at most 30 units
|
227
|
+
rotation *= Math.PI / 8; // Rotate at most a sixteenth of a rotation
|
228
|
+
|
229
|
+
// Transform the canvas, not the viewport:
|
230
|
+
translation = translation.times(-1);
|
231
|
+
rotation = rotation * -1;
|
232
|
+
scale = 1 / scale;
|
233
|
+
|
234
|
+
const toCanvas = this.editor.viewport.screenToCanvasTransform;
|
235
|
+
|
236
|
+
// Transform without translating (treat toCanvas as a linear instead of
|
237
|
+
// an affine transformation).
|
238
|
+
translation = toCanvas.transformVec3(translation);
|
239
|
+
|
240
|
+
// Rotate/scale about the center of the canvas
|
241
|
+
const transformCenter = this.editor.viewport.visibleRect.center;
|
242
|
+
const transformUpdate = Mat33.scaling2D(
|
243
|
+
scale, transformCenter
|
244
|
+
).rightMul(Mat33.zRotation(
|
245
|
+
rotation, transformCenter
|
246
|
+
)).rightMul(Mat33.translation(
|
247
|
+
translation
|
248
|
+
));
|
249
|
+
this.updateTransform(transformUpdate);
|
250
|
+
|
251
|
+
return true;
|
252
|
+
}
|
253
|
+
}
|
package/src/tools/Pen.ts
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
import Color4 from '../Color4';
|
2
|
+
import Editor from '../Editor';
|
3
|
+
import EditorImage from '../EditorImage';
|
4
|
+
import { Vec2 } from '../geometry/Vec2';
|
5
|
+
import Pointer, { PointerDevice } from '../Pointer';
|
6
|
+
import StrokeBuilder from '../StrokeBuilder';
|
7
|
+
import { EditorEventType, PointerEvt } from '../types';
|
8
|
+
import BaseTool from './BaseTool';
|
9
|
+
import { ToolType } from './ToolController';
|
10
|
+
|
11
|
+
export default class Pen extends BaseTool {
|
12
|
+
private builder: StrokeBuilder|null = null;
|
13
|
+
public readonly kind: ToolType = ToolType.Pen;
|
14
|
+
|
15
|
+
public constructor(
|
16
|
+
private editor: Editor,
|
17
|
+
description: string,
|
18
|
+
private color: Color4 = Color4.purple,
|
19
|
+
private thickness: number = 16.0,
|
20
|
+
) {
|
21
|
+
super(editor.notifier, description);
|
22
|
+
}
|
23
|
+
|
24
|
+
private getPressureMultiplier() {
|
25
|
+
return 1 / this.editor.viewport.getScaleFactor() * this.thickness;
|
26
|
+
}
|
27
|
+
|
28
|
+
private getStrokePoint(pointer: Pointer) {
|
29
|
+
const minPressure = 0.3;
|
30
|
+
const pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
|
31
|
+
return {
|
32
|
+
pos: pointer.canvasPos,
|
33
|
+
width: pressure * this.getPressureMultiplier(),
|
34
|
+
color: this.color,
|
35
|
+
time: pointer.timeStamp,
|
36
|
+
};
|
37
|
+
}
|
38
|
+
|
39
|
+
private addPointToStroke(pointer: Pointer) {
|
40
|
+
if (!this.builder) {
|
41
|
+
throw new Error('No stroke is currently being generated.');
|
42
|
+
}
|
43
|
+
this.builder.addPoint(this.getStrokePoint(pointer));
|
44
|
+
|
45
|
+
this.editor.clearWetInk();
|
46
|
+
this.editor.drawWetInk(...this.builder.preview());
|
47
|
+
}
|
48
|
+
|
49
|
+
public onPointerDown({ current, allPointers }: PointerEvt): boolean {
|
50
|
+
if (current.device === PointerDevice.Eraser) {
|
51
|
+
return false;
|
52
|
+
}
|
53
|
+
|
54
|
+
if (allPointers.length === 1 || current.device === PointerDevice.Pen) {
|
55
|
+
// Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
|
56
|
+
// less than ± 2 px from the curve.
|
57
|
+
const canvasTransform = this.editor.viewport.screenToCanvasTransform;
|
58
|
+
const maxSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 7;
|
59
|
+
const minSmoothingDist = canvasTransform.transformVec3(Vec2.unitX).magnitude() * 2;
|
60
|
+
|
61
|
+
this.builder = new StrokeBuilder(
|
62
|
+
this.getStrokePoint(current), minSmoothingDist, maxSmoothingDist
|
63
|
+
);
|
64
|
+
return true;
|
65
|
+
}
|
66
|
+
|
67
|
+
return false;
|
68
|
+
}
|
69
|
+
|
70
|
+
public onPointerMove({ current }: PointerEvt): void {
|
71
|
+
this.addPointToStroke(current);
|
72
|
+
}
|
73
|
+
|
74
|
+
public onPointerUp({ current }: PointerEvt): void {
|
75
|
+
if (!this.builder) {
|
76
|
+
return;
|
77
|
+
}
|
78
|
+
|
79
|
+
this.addPointToStroke(current);
|
80
|
+
if (this.builder && current.isPrimary) {
|
81
|
+
const stroke = this.builder.build();
|
82
|
+
|
83
|
+
this.editor.clearWetInk();
|
84
|
+
this.editor.drawWetInk(...this.builder.preview());
|
85
|
+
|
86
|
+
const canFlatten = true;
|
87
|
+
const action = new EditorImage.AddElementCommand(stroke, canFlatten);
|
88
|
+
this.editor.dispatch(action);
|
89
|
+
}
|
90
|
+
this.builder = null;
|
91
|
+
this.editor.clearWetInk();
|
92
|
+
}
|
93
|
+
|
94
|
+
public onGestureCancel(): void {
|
95
|
+
this.editor.clearWetInk();
|
96
|
+
}
|
97
|
+
|
98
|
+
private noteUpdated() {
|
99
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
100
|
+
kind: EditorEventType.ToolUpdated,
|
101
|
+
tool: this,
|
102
|
+
});
|
103
|
+
}
|
104
|
+
|
105
|
+
public setColor(color: Color4): void {
|
106
|
+
if (color.toHexString() !== this.color.toHexString()) {
|
107
|
+
this.color = color;
|
108
|
+
this.noteUpdated();
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
public setThickness(thickness: number) {
|
113
|
+
if (thickness !== this.thickness) {
|
114
|
+
this.thickness = thickness;
|
115
|
+
this.noteUpdated();
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
public getThickness() { return this.thickness; }
|
120
|
+
public getColor() { return this.color; }
|
121
|
+
}
|
@@ -0,0 +1,85 @@
|
|
1
|
+
/* @jest-environment jsdom */
|
2
|
+
|
3
|
+
import Color4 from '../Color4';
|
4
|
+
import Stroke from '../components/Stroke';
|
5
|
+
import { RenderingMode } from '../Display';
|
6
|
+
import Editor from '../Editor';
|
7
|
+
import EditorImage from '../EditorImage';
|
8
|
+
import Path from '../geometry/Path';
|
9
|
+
import { Vec2 } from '../geometry/Vec2';
|
10
|
+
import { InputEvtType } from '../types';
|
11
|
+
import SelectionTool from './SelectionTool';
|
12
|
+
import { ToolType } from './ToolController';
|
13
|
+
|
14
|
+
const getSelectionTool = (editor: Editor): SelectionTool => {
|
15
|
+
return editor.toolController.getMatchingTools(ToolType.Selection)[0] as SelectionTool;
|
16
|
+
};
|
17
|
+
|
18
|
+
const createEditor = () => new Editor(document.body, RenderingMode.DummyRenderer);
|
19
|
+
|
20
|
+
const createSquareStroke = () => {
|
21
|
+
const testStroke = new Stroke([
|
22
|
+
// A filled unit square
|
23
|
+
Path.fromString('M0,0 L1,0 L1,1 L0,1 Z').toRenderable({ fill: Color4.blue }),
|
24
|
+
]);
|
25
|
+
const addTestStrokeCommand = new EditorImage.AddElementCommand(testStroke);
|
26
|
+
|
27
|
+
return { testStroke, addTestStrokeCommand };
|
28
|
+
};
|
29
|
+
|
30
|
+
describe('SelectionTool', () => {
|
31
|
+
it('selection should shrink/grow to bounding box of selected objects', () => {
|
32
|
+
const { addTestStrokeCommand } = createSquareStroke();
|
33
|
+
|
34
|
+
const editor = createEditor();
|
35
|
+
editor.dispatch(addTestStrokeCommand);
|
36
|
+
|
37
|
+
const selectionTool = getSelectionTool(editor);
|
38
|
+
selectionTool.setEnabled(true);
|
39
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
40
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(0.1, 0.1));
|
41
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(0.1, 0.1));
|
42
|
+
|
43
|
+
// Should surround the selected object (which has bbox = (0, 0, 1, 1))
|
44
|
+
// with extra space.
|
45
|
+
const paddingSize = selectionTool.getSelection()!.getMinCanvasSize();
|
46
|
+
expect(selectionTool.getSelection()!.region).toMatchObject({
|
47
|
+
x: -paddingSize / 2,
|
48
|
+
y: -paddingSize / 2,
|
49
|
+
w: paddingSize + 1,
|
50
|
+
h: paddingSize + 1,
|
51
|
+
});
|
52
|
+
});
|
53
|
+
|
54
|
+
it('dragging the selected region should move selected items', () => {
|
55
|
+
const { testStroke, addTestStrokeCommand } = createSquareStroke();
|
56
|
+
const editor = createEditor();
|
57
|
+
editor.dispatch(addTestStrokeCommand);
|
58
|
+
|
59
|
+
// Select the object
|
60
|
+
const selectionTool = getSelectionTool(editor);
|
61
|
+
selectionTool.setEnabled(true);
|
62
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
63
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
|
64
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(5, 5));
|
65
|
+
|
66
|
+
const selection = selectionTool.getSelection();
|
67
|
+
expect(selection).not.toBeNull();
|
68
|
+
|
69
|
+
// Drag the object
|
70
|
+
selection!.handleBackgroundDrag(Vec2.of(5, 5));
|
71
|
+
selection!.finishDragging();
|
72
|
+
|
73
|
+
expect(testStroke.getBBox().topLeft).toMatchObject({
|
74
|
+
x: 5,
|
75
|
+
y: 5,
|
76
|
+
});
|
77
|
+
|
78
|
+
editor.history.undo();
|
79
|
+
|
80
|
+
expect(testStroke.getBBox().topLeft).toMatchObject({
|
81
|
+
x: 0,
|
82
|
+
y: 0,
|
83
|
+
});
|
84
|
+
});
|
85
|
+
});
|