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,275 @@
|
|
1
|
+
import { Bezier } from 'bezier-js';
|
2
|
+
import { Vec2 } from './geometry/Vec2';
|
3
|
+
import Rect2 from './geometry/Rect2';
|
4
|
+
import { PathCommandType } from './geometry/Path';
|
5
|
+
import LineSegment2 from './geometry/LineSegment2';
|
6
|
+
import Stroke from './components/Stroke';
|
7
|
+
import Viewport from './Viewport';
|
8
|
+
// Handles stroke smoothing and creates Strokes from user/stylus input.
|
9
|
+
export default class StrokeBuilder {
|
10
|
+
constructor(startPoint,
|
11
|
+
// Maximum distance from the actual curve (irrespective of stroke width)
|
12
|
+
// for which a point is considered 'part of the curve'.
|
13
|
+
// Note that the maximum will be smaller if the stroke width is less than
|
14
|
+
// [maxFitAllowed].
|
15
|
+
minFitAllowed, maxFitAllowed) {
|
16
|
+
this.startPoint = startPoint;
|
17
|
+
this.minFitAllowed = minFitAllowed;
|
18
|
+
this.maxFitAllowed = maxFitAllowed;
|
19
|
+
this.currentCurve = null;
|
20
|
+
this.lastPoint = this.startPoint;
|
21
|
+
this.segments = [];
|
22
|
+
this.buffer = [this.startPoint.pos];
|
23
|
+
this.momentum = Vec2.zero;
|
24
|
+
this.currentCurve = null;
|
25
|
+
this.bbox = new Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0);
|
26
|
+
}
|
27
|
+
getBBox() {
|
28
|
+
return this.bbox;
|
29
|
+
}
|
30
|
+
getRenderingStyle() {
|
31
|
+
var _a;
|
32
|
+
return {
|
33
|
+
fill: (_a = this.lastPoint.color) !== null && _a !== void 0 ? _a : null,
|
34
|
+
};
|
35
|
+
}
|
36
|
+
// Get the segments that make up this' path. Can be called after calling build()
|
37
|
+
preview() {
|
38
|
+
if (this.currentCurve && this.lastPoint) {
|
39
|
+
const currentPath = this.currentSegmentToPath();
|
40
|
+
return this.segments.concat(currentPath);
|
41
|
+
}
|
42
|
+
return this.segments;
|
43
|
+
}
|
44
|
+
build() {
|
45
|
+
if (this.lastPoint) {
|
46
|
+
this.finalizeCurrentCurve();
|
47
|
+
}
|
48
|
+
return new Stroke(this.segments);
|
49
|
+
}
|
50
|
+
roundPoint(point) {
|
51
|
+
return Viewport.roundPoint(point, this.minFitAllowed);
|
52
|
+
}
|
53
|
+
finalizeCurrentCurve() {
|
54
|
+
// Case where no points have been added
|
55
|
+
if (!this.currentCurve) {
|
56
|
+
// Don't create a circle around the initial point if the stroke has more than one point.
|
57
|
+
if (this.segments.length > 0) {
|
58
|
+
return;
|
59
|
+
}
|
60
|
+
const width = Viewport.roundPoint(this.startPoint.width / 3, this.minFitAllowed);
|
61
|
+
const center = this.roundPoint(this.startPoint.pos);
|
62
|
+
// Draw a circle-ish shape around the start point
|
63
|
+
this.segments.push({
|
64
|
+
// Start on the right, cycle clockwise:
|
65
|
+
// |
|
66
|
+
// ----- ←
|
67
|
+
// |
|
68
|
+
startPoint: this.startPoint.pos.plus(Vec2.of(width, 0)),
|
69
|
+
commands: [
|
70
|
+
{
|
71
|
+
kind: PathCommandType.QuadraticBezierTo,
|
72
|
+
controlPoint: center.plus(Vec2.of(width, width)),
|
73
|
+
// Bottom of the circle
|
74
|
+
// |
|
75
|
+
// -----
|
76
|
+
// |
|
77
|
+
// ↑
|
78
|
+
endPoint: center.plus(Vec2.of(0, width)),
|
79
|
+
},
|
80
|
+
{
|
81
|
+
kind: PathCommandType.QuadraticBezierTo,
|
82
|
+
controlPoint: center.plus(Vec2.of(-width, width)),
|
83
|
+
endPoint: center.plus(Vec2.of(-width, 0)),
|
84
|
+
},
|
85
|
+
{
|
86
|
+
kind: PathCommandType.QuadraticBezierTo,
|
87
|
+
controlPoint: center.plus(Vec2.of(-width, -width)),
|
88
|
+
endPoint: center.plus(Vec2.of(0, -width)),
|
89
|
+
},
|
90
|
+
{
|
91
|
+
kind: PathCommandType.QuadraticBezierTo,
|
92
|
+
controlPoint: center.plus(Vec2.of(width, -width)),
|
93
|
+
endPoint: center.plus(Vec2.of(width, 0)),
|
94
|
+
},
|
95
|
+
],
|
96
|
+
style: this.getRenderingStyle(),
|
97
|
+
});
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
this.segments.push(this.currentSegmentToPath());
|
101
|
+
const lastPoint = this.buffer[this.buffer.length - 1];
|
102
|
+
this.lastExitingVec = Vec2.ofXY(this.currentCurve.points[2]).minus(Vec2.ofXY(this.currentCurve.points[1]));
|
103
|
+
console.assert(this.lastExitingVec.magnitude() !== 0);
|
104
|
+
// Use the last two points to start a new curve (the last point isn't used
|
105
|
+
// in the current curve and we want connected curves to share end points)
|
106
|
+
this.buffer = [
|
107
|
+
this.buffer[this.buffer.length - 2], lastPoint,
|
108
|
+
];
|
109
|
+
this.currentCurve = null;
|
110
|
+
}
|
111
|
+
currentSegmentToPath() {
|
112
|
+
if (this.currentCurve == null) {
|
113
|
+
throw new Error('Invalid State: currentCurve is null!');
|
114
|
+
}
|
115
|
+
let startVec = Vec2.ofXY(this.currentCurve.normal(0)).normalized();
|
116
|
+
let endVec = Vec2.ofXY(this.currentCurve.normal(1)).normalized();
|
117
|
+
startVec = startVec.times(this.curveStartWidth / 2);
|
118
|
+
endVec = endVec.times(this.curveEndWidth / 2);
|
119
|
+
if (isNaN(startVec.magnitude())) {
|
120
|
+
// TODO: This can happen when events are too close together. Find out why and
|
121
|
+
// fix.
|
122
|
+
console.error('startVec is NaN', startVec, endVec, this.currentCurve);
|
123
|
+
startVec = endVec;
|
124
|
+
}
|
125
|
+
const startPt = Vec2.ofXY(this.currentCurve.get(0));
|
126
|
+
const endPt = Vec2.ofXY(this.currentCurve.get(1));
|
127
|
+
const controlPoint = Vec2.ofXY(this.currentCurve.points[1]);
|
128
|
+
// Approximate the normal at the location of the control point
|
129
|
+
let projectionT = this.currentCurve.project(controlPoint.xy).t;
|
130
|
+
if (!projectionT) {
|
131
|
+
if (startPt.minus(controlPoint).magnitudeSquared() < endPt.minus(controlPoint).magnitudeSquared()) {
|
132
|
+
projectionT = 0.1;
|
133
|
+
}
|
134
|
+
else {
|
135
|
+
projectionT = 0.9;
|
136
|
+
}
|
137
|
+
}
|
138
|
+
const halfVec = Vec2.ofXY(this.currentCurve.normal(projectionT))
|
139
|
+
.normalized().times(this.curveStartWidth / 2 * projectionT
|
140
|
+
+ this.curveEndWidth / 2 * (1 - projectionT));
|
141
|
+
const pathCommands = [
|
142
|
+
{
|
143
|
+
kind: PathCommandType.QuadraticBezierTo,
|
144
|
+
controlPoint: this.roundPoint(controlPoint.plus(halfVec)),
|
145
|
+
endPoint: this.roundPoint(endPt.plus(endVec)),
|
146
|
+
},
|
147
|
+
{
|
148
|
+
kind: PathCommandType.LineTo,
|
149
|
+
point: this.roundPoint(endPt.minus(endVec)),
|
150
|
+
},
|
151
|
+
{
|
152
|
+
kind: PathCommandType.QuadraticBezierTo,
|
153
|
+
controlPoint: this.roundPoint(controlPoint.minus(halfVec)),
|
154
|
+
endPoint: this.roundPoint(startPt.minus(startVec)),
|
155
|
+
},
|
156
|
+
];
|
157
|
+
return {
|
158
|
+
startPoint: this.roundPoint(startPt.plus(startVec)),
|
159
|
+
commands: pathCommands,
|
160
|
+
style: this.getRenderingStyle(),
|
161
|
+
};
|
162
|
+
}
|
163
|
+
// Compute the direction of the velocity at the end of this.buffer
|
164
|
+
computeExitingVec() {
|
165
|
+
return this.momentum.normalized().times(this.lastPoint.width / 2);
|
166
|
+
}
|
167
|
+
addPoint(newPoint) {
|
168
|
+
var _a, _b;
|
169
|
+
if (this.lastPoint) {
|
170
|
+
// Ignore points that are identical
|
171
|
+
const fuzzEq = 1e-10;
|
172
|
+
const deltaTime = newPoint.time - this.lastPoint.time;
|
173
|
+
if (newPoint.pos.eq(this.lastPoint.pos, fuzzEq) || deltaTime === 0) {
|
174
|
+
console.warn('Discarding identical point');
|
175
|
+
return;
|
176
|
+
}
|
177
|
+
else if (isNaN(newPoint.pos.magnitude())) {
|
178
|
+
console.warn('Discarding NaN point.', newPoint);
|
179
|
+
return;
|
180
|
+
}
|
181
|
+
const threshold = Math.min(this.lastPoint.width, newPoint.width) / 4;
|
182
|
+
if (this.lastPoint.pos.minus(newPoint.pos).magnitude() < threshold) {
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
const velocity = newPoint.pos.minus(this.lastPoint.pos).times(1 / (deltaTime) * 1000);
|
186
|
+
this.momentum = this.momentum.lerp(velocity, 0.9);
|
187
|
+
}
|
188
|
+
const lastPoint = (_a = this.lastPoint) !== null && _a !== void 0 ? _a : newPoint;
|
189
|
+
this.lastPoint = newPoint;
|
190
|
+
this.buffer.push(newPoint.pos);
|
191
|
+
const pointRadius = newPoint.width / 2;
|
192
|
+
const prevEndWidth = this.curveEndWidth;
|
193
|
+
this.curveEndWidth = pointRadius;
|
194
|
+
// recompute bbox
|
195
|
+
this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
|
196
|
+
if (this.currentCurve === null) {
|
197
|
+
const p1 = lastPoint.pos;
|
198
|
+
const p2 = lastPoint.pos.plus((_b = this.lastExitingVec) !== null && _b !== void 0 ? _b : Vec2.unitX);
|
199
|
+
const p3 = newPoint.pos;
|
200
|
+
// Quadratic Bézier curve
|
201
|
+
this.currentCurve = new Bezier(p1.xy, p2.xy, p3.xy);
|
202
|
+
this.curveStartWidth = lastPoint.width / 2;
|
203
|
+
console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
|
204
|
+
}
|
205
|
+
let enteringVec = this.lastExitingVec;
|
206
|
+
if (!enteringVec) {
|
207
|
+
let sampleIdx = Math.ceil(this.buffer.length / 3);
|
208
|
+
if (sampleIdx === 0) {
|
209
|
+
sampleIdx = this.buffer.length - 1;
|
210
|
+
}
|
211
|
+
enteringVec = this.buffer[sampleIdx].minus(this.buffer[0]);
|
212
|
+
}
|
213
|
+
let exitingVec = this.computeExitingVec();
|
214
|
+
// Find the intersection between the entering vector and the exiting vector
|
215
|
+
const maxRelativeLength = 2;
|
216
|
+
const segmentStart = this.buffer[0];
|
217
|
+
const segmentEnd = newPoint.pos;
|
218
|
+
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
219
|
+
const maxControlPointDist = maxRelativeLength * startEndDist;
|
220
|
+
// Exit in cases where we would divide by zero
|
221
|
+
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || isNaN(exitingVec.magnitude())) {
|
222
|
+
return;
|
223
|
+
}
|
224
|
+
console.assert(!isNaN(enteringVec.magnitude()));
|
225
|
+
enteringVec = enteringVec.normalized();
|
226
|
+
exitingVec = exitingVec.normalized();
|
227
|
+
console.assert(!isNaN(enteringVec.magnitude()));
|
228
|
+
const lineFromStart = new LineSegment2(segmentStart, segmentStart.plus(enteringVec.times(maxControlPointDist)));
|
229
|
+
const lineFromEnd = new LineSegment2(segmentEnd.minus(exitingVec.times(maxControlPointDist)), segmentEnd);
|
230
|
+
const intersection = lineFromEnd.intersection(lineFromStart);
|
231
|
+
// Position the control point at this intersection
|
232
|
+
let controlPoint;
|
233
|
+
if (intersection) {
|
234
|
+
controlPoint = intersection.point;
|
235
|
+
}
|
236
|
+
else {
|
237
|
+
// Position the control point closer to the first -- the connecting
|
238
|
+
// segment will be roughly a line.
|
239
|
+
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
|
240
|
+
}
|
241
|
+
if (isNaN(controlPoint.magnitude()) || isNaN(segmentStart.magnitude())) {
|
242
|
+
console.error('controlPoint is NaN', intersection, 'Start:', segmentStart, 'End:', segmentEnd, 'in:', enteringVec, 'out:', exitingVec);
|
243
|
+
}
|
244
|
+
const prevCurve = this.currentCurve;
|
245
|
+
this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
|
246
|
+
if (isNaN(Vec2.ofXY(this.currentCurve.normal(0)).magnitude())) {
|
247
|
+
console.error('NaN normal at 0. Curve:', this.currentCurve);
|
248
|
+
this.currentCurve = prevCurve;
|
249
|
+
}
|
250
|
+
// Should we start making a new curve? Check whether all buffer points are within
|
251
|
+
// ±strokeWidth of the curve.
|
252
|
+
const curveMatchesPoints = (curve) => {
|
253
|
+
for (const point of this.buffer) {
|
254
|
+
const proj = Vec2.ofXY(curve.project(point.xy));
|
255
|
+
const dist = proj.minus(point).magnitude();
|
256
|
+
const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 2, this.minFitAllowed);
|
257
|
+
if (dist > minFit || dist > this.maxFitAllowed) {
|
258
|
+
return false;
|
259
|
+
}
|
260
|
+
}
|
261
|
+
return true;
|
262
|
+
};
|
263
|
+
if (this.buffer.length > 3) {
|
264
|
+
if (!curveMatchesPoints(this.currentCurve)) {
|
265
|
+
// Use a curve that better fits the points
|
266
|
+
this.currentCurve = prevCurve;
|
267
|
+
this.curveEndWidth = prevEndWidth;
|
268
|
+
// Reset the last point -- the current point was not added to the curve.
|
269
|
+
this.lastPoint = lastPoint;
|
270
|
+
this.finalizeCurrentCurve();
|
271
|
+
return;
|
272
|
+
}
|
273
|
+
}
|
274
|
+
}
|
275
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import Editor from './Editor';
|
2
|
+
import Command from './commands/Command';
|
3
|
+
declare type AnnounceRedoCallback = (command: Command) => void;
|
4
|
+
declare type AnnounceUndoCallback = (command: Command) => void;
|
5
|
+
declare class UndoRedoHistory {
|
6
|
+
private readonly editor;
|
7
|
+
private announceRedoCallback;
|
8
|
+
private announceUndoCallback;
|
9
|
+
private undoStack;
|
10
|
+
private redoStack;
|
11
|
+
constructor(editor: Editor, announceRedoCallback: AnnounceRedoCallback, announceUndoCallback: AnnounceUndoCallback);
|
12
|
+
private fireUpdateEvent;
|
13
|
+
push(command: Command, apply?: boolean): void;
|
14
|
+
undo(): void;
|
15
|
+
redo(): void;
|
16
|
+
}
|
17
|
+
export default UndoRedoHistory;
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { EditorEventType } from './types';
|
2
|
+
class UndoRedoHistory {
|
3
|
+
constructor(editor, announceRedoCallback, announceUndoCallback) {
|
4
|
+
this.editor = editor;
|
5
|
+
this.announceRedoCallback = announceRedoCallback;
|
6
|
+
this.announceUndoCallback = announceUndoCallback;
|
7
|
+
this.undoStack = [];
|
8
|
+
this.redoStack = [];
|
9
|
+
}
|
10
|
+
fireUpdateEvent() {
|
11
|
+
this.editor.notifier.dispatch(EditorEventType.UndoRedoStackUpdated, {
|
12
|
+
kind: EditorEventType.UndoRedoStackUpdated,
|
13
|
+
undoStackSize: this.undoStack.length,
|
14
|
+
redoStackSize: this.redoStack.length,
|
15
|
+
});
|
16
|
+
}
|
17
|
+
// Adds the given command to this and applies it to the editor.
|
18
|
+
push(command, apply = true) {
|
19
|
+
if (apply) {
|
20
|
+
command.apply(this.editor);
|
21
|
+
}
|
22
|
+
this.undoStack.push(command);
|
23
|
+
this.redoStack = [];
|
24
|
+
this.fireUpdateEvent();
|
25
|
+
}
|
26
|
+
// Remove the last command from this' undo stack and apply it.
|
27
|
+
undo() {
|
28
|
+
const command = this.undoStack.pop();
|
29
|
+
if (command) {
|
30
|
+
this.redoStack.push(command);
|
31
|
+
command.unapply(this.editor);
|
32
|
+
this.announceUndoCallback(command);
|
33
|
+
}
|
34
|
+
this.fireUpdateEvent();
|
35
|
+
}
|
36
|
+
redo() {
|
37
|
+
const command = this.redoStack.pop();
|
38
|
+
if (command) {
|
39
|
+
this.undoStack.push(command);
|
40
|
+
command.apply(this.editor);
|
41
|
+
this.announceRedoCallback(command);
|
42
|
+
}
|
43
|
+
this.fireUpdateEvent();
|
44
|
+
}
|
45
|
+
}
|
46
|
+
export default UndoRedoHistory;
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { CommandLocalization } from './commands/localization';
|
2
|
+
import Editor from './Editor';
|
3
|
+
import Mat33 from './geometry/Mat33';
|
4
|
+
import Rect2 from './geometry/Rect2';
|
5
|
+
import { Point2, Vec2 } from './geometry/Vec2';
|
6
|
+
import { StrokeDataPoint } from './StrokeBuilder';
|
7
|
+
import { EditorNotifier } from './types';
|
8
|
+
declare type PointDataType<T extends Point2 | StrokeDataPoint | number> = T extends Point2 ? Point2 : number;
|
9
|
+
export declare class Viewport {
|
10
|
+
private notifier;
|
11
|
+
static ViewportTransform: {
|
12
|
+
new (transform: Mat33): {
|
13
|
+
readonly "__#2@#inverseTransform": Mat33;
|
14
|
+
readonly transform: Mat33;
|
15
|
+
apply(editor: Editor): void;
|
16
|
+
unapply(editor: Editor): void;
|
17
|
+
description(localizationTable: CommandLocalization): string;
|
18
|
+
};
|
19
|
+
};
|
20
|
+
private transform;
|
21
|
+
private inverseTransform;
|
22
|
+
private screenRect;
|
23
|
+
constructor(notifier: EditorNotifier);
|
24
|
+
updateScreenSize(screenSize: Vec2): void;
|
25
|
+
get visibleRect(): Rect2;
|
26
|
+
screenToCanvas(screenPoint: Point2): Point2;
|
27
|
+
canvasToScreen(canvasPoint: Point2): Point2;
|
28
|
+
resetTransform(newTransform: Mat33): void;
|
29
|
+
get screenToCanvasTransform(): Mat33;
|
30
|
+
get canvasToScreenTransform(): Mat33;
|
31
|
+
getScaleFactor(): number;
|
32
|
+
getRotationAngle(): number;
|
33
|
+
static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
|
34
|
+
roundPoint(point: Point2): Point2;
|
35
|
+
}
|
36
|
+
export declare namespace Viewport {
|
37
|
+
type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
|
38
|
+
}
|
39
|
+
export default Viewport;
|
@@ -0,0 +1,134 @@
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
6
|
+
};
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
11
|
+
};
|
12
|
+
var _inverseTransform, _a;
|
13
|
+
import Mat33 from './geometry/Mat33';
|
14
|
+
import Rect2 from './geometry/Rect2';
|
15
|
+
import { Vec2 } from './geometry/Vec2';
|
16
|
+
import Vec3 from './geometry/Vec3';
|
17
|
+
import { EditorEventType } from './types';
|
18
|
+
export class Viewport {
|
19
|
+
constructor(notifier) {
|
20
|
+
this.notifier = notifier;
|
21
|
+
this.resetTransform(Mat33.identity);
|
22
|
+
this.screenRect = Rect2.empty;
|
23
|
+
}
|
24
|
+
updateScreenSize(screenSize) {
|
25
|
+
this.screenRect = this.screenRect.resizedTo(screenSize);
|
26
|
+
}
|
27
|
+
get visibleRect() {
|
28
|
+
return this.screenRect.transformedBoundingBox(this.inverseTransform);
|
29
|
+
}
|
30
|
+
// the given point, but in canvas coordinates
|
31
|
+
screenToCanvas(screenPoint) {
|
32
|
+
return this.inverseTransform.transformVec2(screenPoint);
|
33
|
+
}
|
34
|
+
canvasToScreen(canvasPoint) {
|
35
|
+
return this.transform.transformVec2(canvasPoint);
|
36
|
+
}
|
37
|
+
// Updates the transformation directly. Using ViewportTransform is preferred.
|
38
|
+
// [newTransform] should map from canvas coordinates to screen coordinates.
|
39
|
+
resetTransform(newTransform) {
|
40
|
+
this.transform = newTransform;
|
41
|
+
this.inverseTransform = newTransform.inverse();
|
42
|
+
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
43
|
+
kind: EditorEventType.ViewportChanged,
|
44
|
+
newTransform,
|
45
|
+
});
|
46
|
+
}
|
47
|
+
get screenToCanvasTransform() {
|
48
|
+
return this.inverseTransform;
|
49
|
+
}
|
50
|
+
get canvasToScreenTransform() {
|
51
|
+
return this.transform;
|
52
|
+
}
|
53
|
+
// Returns the amount a vector on the canvas is scaled to become a vector on the screen.
|
54
|
+
getScaleFactor() {
|
55
|
+
// Use transformVec3 to avoid translating the vector
|
56
|
+
return this.transform.transformVec3(Vec3.unitX).magnitude();
|
57
|
+
}
|
58
|
+
// Returns the angle of the canvas in radians
|
59
|
+
getRotationAngle() {
|
60
|
+
return this.transform.transformVec3(Vec3.unitX).angle();
|
61
|
+
}
|
62
|
+
// The separate function type definition seems necessary here.
|
63
|
+
// See https://stackoverflow.com/a/58163623/17055750.
|
64
|
+
// eslint-disable-next-line no-dupe-class-members
|
65
|
+
static roundPoint(point, tolerance) {
|
66
|
+
const scaleFactor = Math.pow(10, Math.floor(Math.log10(tolerance)));
|
67
|
+
const roundComponent = (component) => {
|
68
|
+
return Math.round(component / scaleFactor) * scaleFactor;
|
69
|
+
};
|
70
|
+
if (typeof point === 'number') {
|
71
|
+
return roundComponent(point);
|
72
|
+
}
|
73
|
+
return point.map(roundComponent);
|
74
|
+
}
|
75
|
+
// Round a point with a tolerance of ±1 screen unit.
|
76
|
+
roundPoint(point) {
|
77
|
+
return Viewport.roundPoint(point, 1 / this.getScaleFactor());
|
78
|
+
}
|
79
|
+
}
|
80
|
+
// Command that translates/scales the viewport.
|
81
|
+
Viewport.ViewportTransform = (_a = class {
|
82
|
+
constructor(transform) {
|
83
|
+
this.transform = transform;
|
84
|
+
_inverseTransform.set(this, void 0);
|
85
|
+
__classPrivateFieldSet(this, _inverseTransform, transform.inverse(), "f");
|
86
|
+
}
|
87
|
+
apply(editor) {
|
88
|
+
const viewport = editor.viewport;
|
89
|
+
viewport.resetTransform(viewport.transform.rightMul(this.transform));
|
90
|
+
editor.queueRerender();
|
91
|
+
}
|
92
|
+
unapply(editor) {
|
93
|
+
const viewport = editor.viewport;
|
94
|
+
viewport.resetTransform(viewport.transform.rightMul(__classPrivateFieldGet(this, _inverseTransform, "f")));
|
95
|
+
editor.queueRerender();
|
96
|
+
}
|
97
|
+
description(localizationTable) {
|
98
|
+
const result = [];
|
99
|
+
// Describe the transformation's affect on the viewport (note that transformation transforms
|
100
|
+
// the **elements** within the viewport). Assumes the transformation only does rotation/scale/translation.
|
101
|
+
const origVec = Vec2.unitX;
|
102
|
+
const linearTransformedVec = this.transform.transformVec3(Vec2.unitX);
|
103
|
+
const affineTransformedVec = this.transform.transformVec2(Vec2.unitX);
|
104
|
+
const scale = linearTransformedVec.magnitude();
|
105
|
+
const rotation = 180 / Math.PI * linearTransformedVec.angle();
|
106
|
+
const translation = affineTransformedVec.minus(origVec);
|
107
|
+
if (scale > 1.2) {
|
108
|
+
result.push(localizationTable.zoomedIn);
|
109
|
+
}
|
110
|
+
else if (scale < 0.8) {
|
111
|
+
result.push(localizationTable.zoomedOut);
|
112
|
+
}
|
113
|
+
if (Math.floor(Math.abs(rotation)) > 0) {
|
114
|
+
result.push(localizationTable.rotatedBy(Math.round(rotation)));
|
115
|
+
}
|
116
|
+
const minTranslation = 1e-4;
|
117
|
+
if (translation.x > minTranslation) {
|
118
|
+
result.push(localizationTable.movedLeft);
|
119
|
+
}
|
120
|
+
else if (translation.x < -minTranslation) {
|
121
|
+
result.push(localizationTable.movedRight);
|
122
|
+
}
|
123
|
+
if (translation.y < minTranslation) {
|
124
|
+
result.push(localizationTable.movedDown);
|
125
|
+
}
|
126
|
+
else if (translation.y > minTranslation) {
|
127
|
+
result.push(localizationTable.movedUp);
|
128
|
+
}
|
129
|
+
return result.join('; ');
|
130
|
+
}
|
131
|
+
},
|
132
|
+
_inverseTransform = new WeakMap(),
|
133
|
+
_a);
|
134
|
+
export default Viewport;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { EditorLocalization } from '../localization';
|
3
|
+
interface Command {
|
4
|
+
apply(editor: Editor): void;
|
5
|
+
unapply(editor: Editor): void;
|
6
|
+
description(localizationTable: EditorLocalization): string;
|
7
|
+
}
|
8
|
+
declare namespace Command {
|
9
|
+
const empty: {
|
10
|
+
apply(_editor: Editor): void;
|
11
|
+
unapply(_editor: Editor): void;
|
12
|
+
};
|
13
|
+
const union: (a: Command, b: Command) => Command;
|
14
|
+
}
|
15
|
+
export default Command;
|
@@ -0,0 +1,29 @@
|
|
1
|
+
// eslint-disable-next-line no-redeclare
|
2
|
+
var Command;
|
3
|
+
(function (Command) {
|
4
|
+
Command.empty = {
|
5
|
+
apply(_editor) { },
|
6
|
+
unapply(_editor) { },
|
7
|
+
};
|
8
|
+
Command.union = (a, b) => {
|
9
|
+
return {
|
10
|
+
apply(editor) {
|
11
|
+
a.apply(editor);
|
12
|
+
b.apply(editor);
|
13
|
+
},
|
14
|
+
unapply(editor) {
|
15
|
+
b.unapply(editor);
|
16
|
+
a.unapply(editor);
|
17
|
+
},
|
18
|
+
description(localizationTable) {
|
19
|
+
const aDescription = a.description(localizationTable);
|
20
|
+
const bDescription = b.description(localizationTable);
|
21
|
+
if (aDescription === bDescription) {
|
22
|
+
return aDescription;
|
23
|
+
}
|
24
|
+
return `${aDescription}, ${bDescription}`;
|
25
|
+
},
|
26
|
+
};
|
27
|
+
};
|
28
|
+
})(Command || (Command = {}));
|
29
|
+
export default Command;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import AbstractComponent from '../components/AbstractComponent';
|
2
|
+
import Editor from '../Editor';
|
3
|
+
import { EditorLocalization } from '../localization';
|
4
|
+
import Command from './Command';
|
5
|
+
export default class Erase implements Command {
|
6
|
+
private toRemove;
|
7
|
+
constructor(toRemove: AbstractComponent[]);
|
8
|
+
apply(editor: Editor): void;
|
9
|
+
unapply(editor: Editor): void;
|
10
|
+
description(localizationTable: EditorLocalization): string;
|
11
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import EditorImage from '../EditorImage';
|
2
|
+
export default class Erase {
|
3
|
+
constructor(toRemove) {
|
4
|
+
// Clone the list
|
5
|
+
this.toRemove = toRemove.map(elem => elem);
|
6
|
+
}
|
7
|
+
apply(editor) {
|
8
|
+
for (const part of this.toRemove) {
|
9
|
+
const parent = editor.image.findParent(part);
|
10
|
+
if (parent) {
|
11
|
+
parent.remove();
|
12
|
+
}
|
13
|
+
}
|
14
|
+
editor.queueRerender();
|
15
|
+
}
|
16
|
+
unapply(editor) {
|
17
|
+
for (const part of this.toRemove) {
|
18
|
+
if (!editor.image.findParent(part)) {
|
19
|
+
new EditorImage.AddElementCommand(part).apply(editor);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
editor.queueRerender();
|
23
|
+
}
|
24
|
+
description(localizationTable) {
|
25
|
+
if (this.toRemove.length === 0) {
|
26
|
+
return localizationTable.erasedNoElements;
|
27
|
+
}
|
28
|
+
let description = this.toRemove[0].description(localizationTable);
|
29
|
+
for (const elem of this.toRemove) {
|
30
|
+
if (elem.description(localizationTable) !== description) {
|
31
|
+
description = localizationTable.elements;
|
32
|
+
break;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
return localizationTable.eraseAction(description, this.toRemove.length);
|
36
|
+
}
|
37
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import Rect2 from '../geometry/Rect2';
|
2
|
+
export interface CommandLocalization {
|
3
|
+
movedLeft: string;
|
4
|
+
movedUp: string;
|
5
|
+
movedDown: string;
|
6
|
+
movedRight: string;
|
7
|
+
rotatedBy: (degrees: number) => string;
|
8
|
+
zoomedOut: string;
|
9
|
+
zoomedIn: string;
|
10
|
+
erasedNoElements: string;
|
11
|
+
elements: string;
|
12
|
+
updatedViewport: string;
|
13
|
+
transformedElements: (elemCount: number) => string;
|
14
|
+
resizeOutputCommand: (newSize: Rect2) => string;
|
15
|
+
addElementAction: (elemDescription: string) => string;
|
16
|
+
eraseAction: (elemDescription: string, numElems: number) => string;
|
17
|
+
selectedElements: (count: number) => string;
|
18
|
+
}
|
19
|
+
export declare const defaultCommandLocalization: CommandLocalization;
|
@@ -0,0 +1,17 @@
|
|
1
|
+
export const defaultCommandLocalization = {
|
2
|
+
updatedViewport: 'Transformed Viewport',
|
3
|
+
transformedElements: (elemCount) => `Transformed ${elemCount} element${elemCount === 1 ? '' : 's'}`,
|
4
|
+
resizeOutputCommand: (newSize) => `Resized image to ${newSize.w}x${newSize.h}`,
|
5
|
+
addElementAction: (componentDescription) => `Added ${componentDescription}`,
|
6
|
+
eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
|
7
|
+
elements: 'Elements',
|
8
|
+
erasedNoElements: 'Erased nothing',
|
9
|
+
rotatedBy: (degrees) => `Rotated by ${Math.abs(degrees)} degrees ${degrees < 0 ? 'clockwise' : 'counter-clockwise'}`,
|
10
|
+
movedLeft: 'Moved left',
|
11
|
+
movedUp: 'Moved up',
|
12
|
+
movedDown: 'Moved down',
|
13
|
+
movedRight: 'Moved right',
|
14
|
+
zoomedOut: 'Zoomed out',
|
15
|
+
zoomedIn: 'Zoomed in',
|
16
|
+
selectedElements: (count) => `Selected ${count} element${count === 1 ? '' : 's'}`,
|
17
|
+
};
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import Command from '../commands/Command';
|
2
|
+
import LineSegment2 from '../geometry/LineSegment2';
|
3
|
+
import Mat33 from '../geometry/Mat33';
|
4
|
+
import Rect2 from '../geometry/Rect2';
|
5
|
+
import AbstractRenderer from '../rendering/AbstractRenderer';
|
6
|
+
import { ImageComponentLocalization } from './localization';
|
7
|
+
export default abstract class AbstractComponent {
|
8
|
+
protected lastChangedTime: number;
|
9
|
+
protected abstract contentBBox: Rect2;
|
10
|
+
zIndex: number;
|
11
|
+
private static zIndexCounter;
|
12
|
+
protected constructor();
|
13
|
+
getBBox(): Rect2;
|
14
|
+
abstract render(canvas: AbstractRenderer, visibleRect: Rect2): void;
|
15
|
+
abstract intersects(lineSegment: LineSegment2): boolean;
|
16
|
+
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
17
|
+
transformBy(affineTransfm: Mat33): Command;
|
18
|
+
abstract description(localizationTable: ImageComponentLocalization): string;
|
19
|
+
}
|