js-draw 0.15.0 → 0.15.2
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/.github/ISSUE_TEMPLATE/translation.yml +16 -0
- package/CHANGELOG.md +13 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +1 -1
- package/dist/src/Color4.js +5 -1
- package/dist/src/Editor.d.ts +0 -2
- package/dist/src/Editor.js +15 -30
- package/dist/src/EditorImage.d.ts +25 -0
- package/dist/src/EditorImage.js +57 -2
- package/dist/src/EventDispatcher.d.ts +4 -3
- package/dist/src/SVGLoader.d.ts +1 -0
- package/dist/src/SVGLoader.js +15 -1
- package/dist/src/Viewport.d.ts +3 -3
- package/dist/src/Viewport.js +4 -8
- package/dist/src/components/AbstractComponent.d.ts +5 -1
- package/dist/src/components/AbstractComponent.js +22 -8
- package/dist/src/components/ImageBackground.d.ts +41 -0
- package/dist/src/components/ImageBackground.js +132 -0
- package/dist/src/components/ImageComponent.js +2 -0
- package/dist/src/components/builders/ArrowBuilder.d.ts +3 -1
- package/dist/src/components/builders/ArrowBuilder.js +43 -40
- package/dist/src/components/builders/LineBuilder.d.ts +3 -1
- package/dist/src/components/builders/LineBuilder.js +25 -28
- package/dist/src/components/builders/RectangleBuilder.js +1 -1
- package/dist/src/components/lib.d.ts +2 -1
- package/dist/src/components/lib.js +2 -1
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/Mat33.js +43 -5
- package/dist/src/math/Path.d.ts +5 -0
- package/dist/src/math/Path.js +80 -28
- package/dist/src/math/Vec3.js +1 -1
- package/dist/src/rendering/Display.js +1 -1
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +13 -1
- package/dist/src/rendering/renderers/AbstractRenderer.js +18 -3
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +12 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
- package/dist/src/testing/sendTouchEvent.d.ts +6 -0
- package/dist/src/testing/sendTouchEvent.js +26 -0
- package/dist/src/toolbar/IconProvider.js +1 -2
- package/dist/src/toolbar/widgets/HandToolWidget.js +1 -1
- package/dist/src/tools/Eraser.js +5 -2
- package/dist/src/tools/PanZoom.js +12 -0
- package/dist/src/tools/SelectionTool/Selection.js +1 -1
- package/dist/src/tools/SelectionTool/SelectionTool.js +8 -2
- package/package.json +1 -1
- package/src/Color4.test.ts +6 -0
- package/src/Color4.ts +6 -1
- package/src/Editor.ts +15 -36
- package/src/EditorImage.ts +74 -2
- package/src/EventDispatcher.ts +4 -1
- package/src/SVGLoader.ts +12 -1
- package/src/Viewport.ts +4 -7
- package/src/components/AbstractComponent.transformBy.test.ts +22 -0
- package/src/components/AbstractComponent.ts +21 -4
- package/src/components/ImageBackground.ts +167 -0
- package/src/components/ImageComponent.ts +2 -0
- package/src/components/builders/ArrowBuilder.ts +44 -41
- package/src/components/builders/LineBuilder.ts +26 -28
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/lib.ts +2 -0
- package/src/components/localization.ts +4 -0
- package/src/math/Mat33.test.ts +20 -1
- package/src/math/Mat33.ts +47 -5
- package/src/math/Path.ts +87 -28
- package/src/math/Vec3.test.ts +4 -0
- package/src/math/Vec3.ts +1 -1
- package/src/rendering/Display.ts +1 -1
- package/src/rendering/renderers/AbstractRenderer.ts +20 -3
- package/src/rendering/renderers/CanvasRenderer.ts +16 -3
- package/src/rendering/renderers/DummyRenderer.test.ts +1 -2
- package/src/rendering/renderers/SVGRenderer.ts +8 -1
- package/src/testing/sendTouchEvent.ts +43 -0
- package/src/toolbar/IconProvider.ts +1 -2
- package/src/toolbar/toolbar.css +7 -0
- package/src/toolbar/widgets/HandToolWidget.ts +1 -1
- package/src/tools/Eraser.test.ts +24 -1
- package/src/tools/Eraser.ts +6 -2
- package/src/tools/PanZoom.test.ts +267 -23
- package/src/tools/PanZoom.ts +15 -1
- package/src/tools/SelectionTool/Selection.ts +1 -1
- package/src/tools/SelectionTool/SelectionTool.ts +8 -1
- package/src/types.ts +1 -0
@@ -64,19 +64,34 @@ export default class AbstractRenderer {
|
|
64
64
|
this.currentPaths.push(path);
|
65
65
|
}
|
66
66
|
}
|
67
|
-
//
|
67
|
+
// Strokes a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
|
68
68
|
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
|
69
69
|
drawRect(rect, lineWidth, lineFill) {
|
70
70
|
const path = Path.fromRect(rect, lineWidth);
|
71
71
|
this.drawPath(path.toRenderable(lineFill));
|
72
72
|
}
|
73
|
-
//
|
73
|
+
// Fills a rectangle.
|
74
|
+
fillRect(rect, fill) {
|
75
|
+
const path = Path.fromRect(rect);
|
76
|
+
this.drawPath(path.toRenderable({ fill }));
|
77
|
+
}
|
78
|
+
// Note the start of an object with the given bounding box.
|
74
79
|
// Renderers are not required to support [clip]
|
75
80
|
startObject(_boundingBox, _clip) {
|
76
81
|
this.currentPaths = [];
|
77
82
|
this.objectLevel++;
|
78
83
|
}
|
79
|
-
|
84
|
+
/**
|
85
|
+
* Notes the end of an object.
|
86
|
+
* @param _loaderData - a map from strings to JSON-ifyable objects
|
87
|
+
* and contains properties attached to the object by whatever loader loaded the image. This
|
88
|
+
* is used to preserve attributes not supported by js-draw when loading/saving an image.
|
89
|
+
* Renderers may ignore this.
|
90
|
+
*
|
91
|
+
* @param _objectTags - a list of labels (e.g. `className`s) to be attached to the object.
|
92
|
+
* Renderers may ignore this.
|
93
|
+
*/
|
94
|
+
endObject(_loaderData, _objectTags) {
|
80
95
|
// Render the paths all at once
|
81
96
|
this.flushPath();
|
82
97
|
this.currentPaths = null;
|
@@ -10,6 +10,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
10
10
|
private ctx;
|
11
11
|
private ignoreObjectsAboveLevel;
|
12
12
|
private ignoringObject;
|
13
|
+
private currentObjectBBox;
|
13
14
|
private minSquareCurveApproxDist;
|
14
15
|
private minRenderSizeAnyDimen;
|
15
16
|
private minRenderSizeBothDimens;
|
@@ -30,7 +31,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
30
31
|
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
31
32
|
drawImage(image: RenderableImage): void;
|
32
33
|
private clipLevels;
|
33
|
-
startObject(boundingBox: Rect2, clip
|
34
|
+
startObject(boundingBox: Rect2, clip?: boolean): void;
|
34
35
|
endObject(): void;
|
35
36
|
drawPoints(...points: Point2[]): void;
|
36
37
|
isTooSmallToRender(rect: Rect2): boolean;
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
2
|
import TextComponent from '../../components/TextComponent';
|
3
|
+
import Path from '../../math/Path';
|
3
4
|
import { Vec2 } from '../../math/Vec2';
|
4
5
|
import AbstractRenderer from './AbstractRenderer';
|
5
6
|
export default class CanvasRenderer extends AbstractRenderer {
|
@@ -8,6 +9,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
8
9
|
this.ctx = ctx;
|
9
10
|
this.ignoreObjectsAboveLevel = null;
|
10
11
|
this.ignoringObject = false;
|
12
|
+
this.currentObjectBBox = null;
|
11
13
|
this.clipLevels = [];
|
12
14
|
this.setDraftMode(false);
|
13
15
|
}
|
@@ -43,8 +45,8 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
43
45
|
}
|
44
46
|
else {
|
45
47
|
this.minSquareCurveApproxDist = 0.5;
|
46
|
-
this.minRenderSizeBothDimens = 0.
|
47
|
-
this.minRenderSizeAnyDimen = 1e-
|
48
|
+
this.minRenderSizeBothDimens = 0.2;
|
49
|
+
this.minRenderSizeAnyDimen = 1e-6;
|
48
50
|
}
|
49
51
|
}
|
50
52
|
displaySize() {
|
@@ -106,9 +108,15 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
106
108
|
}
|
107
109
|
}
|
108
110
|
drawPath(path) {
|
111
|
+
var _a;
|
109
112
|
if (this.ignoringObject) {
|
110
113
|
return;
|
111
114
|
}
|
115
|
+
// If part of a huge object, it might be worth trimming the path
|
116
|
+
if ((_a = this.currentObjectBBox) === null || _a === void 0 ? void 0 : _a.containsRect(this.getViewport().visibleRect)) {
|
117
|
+
// Try to trim/remove parts of the path outside of the bounding box.
|
118
|
+
path = Path.visualEquivalent(path, this.getViewport().visibleRect);
|
119
|
+
}
|
112
120
|
super.drawPath(path);
|
113
121
|
}
|
114
122
|
drawText(text, transform, style) {
|
@@ -140,6 +148,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
140
148
|
this.ignoringObject = true;
|
141
149
|
}
|
142
150
|
super.startObject(boundingBox);
|
151
|
+
this.currentObjectBBox = boundingBox;
|
143
152
|
if (!this.ignoringObject && clip) {
|
144
153
|
this.clipLevels.push(this.objectLevel);
|
145
154
|
this.ctx.save();
|
@@ -158,6 +167,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
158
167
|
this.clipLevels.pop();
|
159
168
|
}
|
160
169
|
}
|
170
|
+
this.currentObjectBBox = null;
|
161
171
|
super.endObject();
|
162
172
|
// If exiting an object with a too-small-to-draw bounding box,
|
163
173
|
if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
|
@@ -28,7 +28,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
28
28
|
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
29
29
|
drawImage(image: RenderableImage): void;
|
30
30
|
startObject(boundingBox: Rect2): void;
|
31
|
-
endObject(loaderData?: LoadSaveDataTable): void;
|
31
|
+
endObject(loaderData?: LoadSaveDataTable, elemClassNames?: string[]): void;
|
32
32
|
private unimplementedMessage;
|
33
33
|
protected beginPath(_startPoint: Point2): void;
|
34
34
|
protected endPath(_style: RenderingStyle): void;
|
@@ -215,8 +215,8 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
215
215
|
this.textParentStyle = null;
|
216
216
|
this.objectElems = [];
|
217
217
|
}
|
218
|
-
endObject(loaderData) {
|
219
|
-
var _a;
|
218
|
+
endObject(loaderData, elemClassNames) {
|
219
|
+
var _a, _b;
|
220
220
|
super.endObject(loaderData);
|
221
221
|
// Don't extend paths across objects
|
222
222
|
this.addPathToSVG();
|
@@ -237,6 +237,12 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
237
237
|
}
|
238
238
|
}
|
239
239
|
}
|
240
|
+
// Add class names to the object, if given.
|
241
|
+
if (elemClassNames) {
|
242
|
+
for (const elem of (_b = this.objectElems) !== null && _b !== void 0 ? _b : []) {
|
243
|
+
elem.classList.add(...elemClassNames);
|
244
|
+
}
|
245
|
+
}
|
240
246
|
}
|
241
247
|
// Not implemented -- use drawPath instead.
|
242
248
|
unimplementedMessage() { throw new Error('Not implemenented!'); }
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import Editor from '../Editor';
|
2
|
+
import { Vec2 } from '../math/Vec2';
|
3
|
+
import Pointer from '../Pointer';
|
4
|
+
import { InputEvtType } from '../types';
|
5
|
+
declare const sendTouchEvent: (editor: Editor, eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, screenPos: Vec2, allOtherPointers?: Pointer[]) => Pointer;
|
6
|
+
export default sendTouchEvent;
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import Pointer, { PointerDevice } from '../Pointer';
|
2
|
+
import { InputEvtType } from '../types';
|
3
|
+
const sendTouchEvent = (editor, eventType, screenPos, allOtherPointers) => {
|
4
|
+
const canvasPos = editor.viewport.screenToCanvas(screenPos);
|
5
|
+
let ptrId = 0;
|
6
|
+
let maxPtrId = 0;
|
7
|
+
// Get a unique ID for the main pointer
|
8
|
+
// (try to use id=0, but don't use it if it's already in use).
|
9
|
+
for (const pointer of allOtherPointers !== null && allOtherPointers !== void 0 ? allOtherPointers : []) {
|
10
|
+
maxPtrId = Math.max(pointer.id, maxPtrId);
|
11
|
+
if (pointer.id === ptrId) {
|
12
|
+
ptrId = maxPtrId + 1;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
const mainPointer = Pointer.ofCanvasPoint(canvasPos, eventType !== InputEvtType.PointerUpEvt, editor.viewport, ptrId, PointerDevice.Touch);
|
16
|
+
editor.toolController.dispatchInputEvent({
|
17
|
+
kind: eventType,
|
18
|
+
allPointers: [
|
19
|
+
...(allOtherPointers !== null && allOtherPointers !== void 0 ? allOtherPointers : []),
|
20
|
+
mainPointer,
|
21
|
+
],
|
22
|
+
current: mainPointer,
|
23
|
+
});
|
24
|
+
return mainPointer;
|
25
|
+
};
|
26
|
+
export default sendTouchEvent;
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
|
-
import EventDispatcher from '../EventDispatcher';
|
3
2
|
import { Vec2 } from '../math/Vec2';
|
4
3
|
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
5
4
|
import Viewport from '../Viewport';
|
@@ -482,7 +481,7 @@ export default class IconProvider {
|
|
482
481
|
color: pen.getColor(),
|
483
482
|
time: nowTime,
|
484
483
|
};
|
485
|
-
const viewport = new Viewport(
|
484
|
+
const viewport = new Viewport(() => { });
|
486
485
|
const builder = factory(startPoint, viewport);
|
487
486
|
builder.addPoint(endPoint);
|
488
487
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
@@ -71,7 +71,7 @@ class ZoomWidget extends BaseWidget {
|
|
71
71
|
this.setDropdownVisible(!this.isDropdownVisible());
|
72
72
|
}
|
73
73
|
fillDropdown(dropdown) {
|
74
|
-
dropdown.
|
74
|
+
dropdown.replaceChildren(makeZoomControl(this.localizationTable, this.editor));
|
75
75
|
return true;
|
76
76
|
}
|
77
77
|
}
|
package/dist/src/tools/Eraser.js
CHANGED
@@ -51,10 +51,13 @@ export default class Eraser extends BaseTool {
|
|
51
51
|
const intersectingElems = this.editor.image.getElementsIntersectingRegion(region).filter(component => {
|
52
52
|
return component.intersects(line) || component.intersectsRect(eraserRect);
|
53
53
|
});
|
54
|
+
// Only erase components that could be selected (and thus interacted with)
|
55
|
+
// by the user.
|
56
|
+
const toErase = intersectingElems.filter(elem => elem.isSelectable());
|
54
57
|
// Remove any intersecting elements.
|
55
|
-
this.toRemove.push(...
|
58
|
+
this.toRemove.push(...toErase);
|
56
59
|
// Create new Erase commands for the now-to-be-erased elements and apply them.
|
57
|
-
const newPartialCommands =
|
60
|
+
const newPartialCommands = toErase.map(elem => new Erase([elem]));
|
58
61
|
newPartialCommands.forEach(cmd => cmd.apply(this.editor));
|
59
62
|
this.partialCommands.push(...newPartialCommands);
|
60
63
|
this.drawPreviewAt(currentPoint);
|
@@ -85,6 +85,12 @@ export default class PanZoom extends BaseTool {
|
|
85
85
|
}
|
86
86
|
// Returns information about the pointers in a gesture
|
87
87
|
computePinchData(p1, p2) {
|
88
|
+
// Swap the pointers to ensure consistent ordering.
|
89
|
+
if (p1.id < p2.id) {
|
90
|
+
const tmp = p1;
|
91
|
+
p1 = p2;
|
92
|
+
p2 = tmp;
|
93
|
+
}
|
88
94
|
const screenBetween = p2.screenPos.minus(p1.screenPos);
|
89
95
|
const angle = screenBetween.angle();
|
90
96
|
const dist = screenBetween.magnitude();
|
@@ -169,6 +175,12 @@ export default class PanZoom extends BaseTool {
|
|
169
175
|
// Snap the rotation
|
170
176
|
if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
|
171
177
|
fullRotation = roundedFullRotation;
|
178
|
+
// Work around a rotation/matrix multiply bug.
|
179
|
+
// (See commit after 4abe27ff8e7913155828f98dee77b09c57c51d30).
|
180
|
+
// TODO: Fix the underlying issue and remove this.
|
181
|
+
if (fullRotation !== 0) {
|
182
|
+
fullRotation += 0.0001;
|
183
|
+
}
|
172
184
|
}
|
173
185
|
return fullRotation - this.editor.viewport.getRotationAngle();
|
174
186
|
}
|
@@ -157,7 +157,7 @@ export default class Selection {
|
|
157
157
|
singleItemSelectionMode = true;
|
158
158
|
}
|
159
159
|
this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
|
160
|
-
return elem.intersectsRect(this.region);
|
160
|
+
return elem.intersectsRect(this.region) && elem.isSelectable();
|
161
161
|
});
|
162
162
|
if (singleItemSelectionMode && this.selectedElems.length > 0) {
|
163
163
|
this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
|
@@ -158,7 +158,7 @@ export default class SelectionTool extends BaseTool {
|
|
158
158
|
}
|
159
159
|
}
|
160
160
|
onGestureCancel() {
|
161
|
-
var _a, _b, _c;
|
161
|
+
var _a, _b, _c, _d;
|
162
162
|
if (this.selectionBoxHandlingEvt) {
|
163
163
|
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragCancel();
|
164
164
|
}
|
@@ -167,6 +167,8 @@ export default class SelectionTool extends BaseTool {
|
|
167
167
|
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.cancelSelection();
|
168
168
|
this.selectionBox = this.prevSelectionBox;
|
169
169
|
(_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.addTo(this.handleOverlay);
|
170
|
+
(_d = this.selectionBox) === null || _d === void 0 ? void 0 : _d.recomputeRegion();
|
171
|
+
this.prevSelectionBox = null;
|
170
172
|
}
|
171
173
|
this.expandingSelectionBox = false;
|
172
174
|
}
|
@@ -282,6 +284,10 @@ export default class SelectionTool extends BaseTool {
|
|
282
284
|
});
|
283
285
|
return true;
|
284
286
|
}
|
287
|
+
if (evt.key === 'a') {
|
288
|
+
// Selected all in onKeyDown. Don't finalizeTransform.
|
289
|
+
return true;
|
290
|
+
}
|
285
291
|
}
|
286
292
|
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
287
293
|
this.selectionBox.finalizeTransform();
|
@@ -298,7 +304,7 @@ export default class SelectionTool extends BaseTool {
|
|
298
304
|
if (selectedElems.length === 0) {
|
299
305
|
return false;
|
300
306
|
}
|
301
|
-
const exportViewport = new Viewport(
|
307
|
+
const exportViewport = new Viewport(() => { });
|
302
308
|
exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
|
303
309
|
exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
|
304
310
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
package/package.json
CHANGED
package/src/Color4.test.ts
CHANGED
@@ -31,4 +31,10 @@ describe('Color4', () => {
|
|
31
31
|
it('should mix red with nothing and get red', () => {
|
32
32
|
expect(Color4.average([ Color4.red ])).objEq(Color4.red);
|
33
33
|
});
|
34
|
+
|
35
|
+
it('different colors should be different', () => {
|
36
|
+
expect(Color4.red.eq(Color4.red)).toBe(true);
|
37
|
+
expect(Color4.red.eq(Color4.green)).toBe(false);
|
38
|
+
expect(Color4.fromString('#ff000000').eq(Color4.transparent)).toBe(true);
|
39
|
+
});
|
34
40
|
});
|
package/src/Color4.ts
CHANGED
@@ -10,7 +10,7 @@ export default class Color4 {
|
|
10
10
|
/** Blue component. `b` ∈ [0, 1] */
|
11
11
|
public readonly b: number,
|
12
12
|
|
13
|
-
/** Alpha/transparent component. `a` ∈ [0, 1] */
|
13
|
+
/** Alpha/transparent component. `a` ∈ [0, 1]. 0 = transparent */
|
14
14
|
public readonly a: number
|
15
15
|
) {
|
16
16
|
}
|
@@ -126,6 +126,11 @@ export default class Color4 {
|
|
126
126
|
return false;
|
127
127
|
}
|
128
128
|
|
129
|
+
// If both completely transparent,
|
130
|
+
if (this.a === 0 && other.a === 0) {
|
131
|
+
return true;
|
132
|
+
}
|
133
|
+
|
129
134
|
return this.toHexString() === other.toHexString();
|
130
135
|
}
|
131
136
|
|
package/src/Editor.ts
CHANGED
@@ -109,9 +109,6 @@ export class Editor {
|
|
109
109
|
*/
|
110
110
|
public readonly image: EditorImage;
|
111
111
|
|
112
|
-
/** Viewport for the exported/imported image. */
|
113
|
-
private importExportViewport: Viewport;
|
114
|
-
|
115
112
|
/**
|
116
113
|
* Allows transforming the view and querying information about
|
117
114
|
* what is currently visible.
|
@@ -215,8 +212,13 @@ export class Editor {
|
|
215
212
|
this.renderingRegion.setAttribute('alt', '');
|
216
213
|
|
217
214
|
this.notifier = new EventDispatcher();
|
218
|
-
this.
|
219
|
-
|
215
|
+
this.viewport = new Viewport((oldTransform, newTransform) => {
|
216
|
+
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
217
|
+
kind: EditorEventType.ViewportChanged,
|
218
|
+
newTransform,
|
219
|
+
oldTransform,
|
220
|
+
});
|
221
|
+
});
|
220
222
|
this.display = new Display(this, this.settings.renderingMode, this.renderingRegion);
|
221
223
|
this.image = new EditorImage();
|
222
224
|
this.history = new UndoRedoHistory(this, this.announceRedoCallback, this.announceUndoCallback);
|
@@ -224,9 +226,6 @@ export class Editor {
|
|
224
226
|
|
225
227
|
parent.appendChild(this.container);
|
226
228
|
|
227
|
-
// Default to a 500x500 image
|
228
|
-
this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
|
229
|
-
|
230
229
|
this.viewport.updateScreenSize(
|
231
230
|
Vec2.of(this.display.width, this.display.height)
|
232
231
|
);
|
@@ -773,7 +772,7 @@ export class Editor {
|
|
773
772
|
const exportRectFill = { fill: Color4.fromHex('#44444455') };
|
774
773
|
const exportRectStrokeWidth = 5 * this.viewport.getSizeOfPixelOnCanvas();
|
775
774
|
renderer.drawRect(
|
776
|
-
this.
|
775
|
+
this.getImportExportRect(),
|
777
776
|
exportRectStrokeWidth,
|
778
777
|
exportRectFill
|
779
778
|
);
|
@@ -920,13 +919,14 @@ export class Editor {
|
|
920
919
|
public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
|
921
920
|
const canvas = document.createElement('canvas');
|
922
921
|
|
923
|
-
const
|
922
|
+
const importExportViewport = this.image.getImportExportViewport();
|
923
|
+
const resolution = importExportViewport.getScreenRectSize();
|
924
924
|
|
925
925
|
canvas.width = resolution.x;
|
926
926
|
canvas.height = resolution.y;
|
927
927
|
|
928
928
|
const ctx = canvas.getContext('2d')!;
|
929
|
-
const renderer = new CanvasRenderer(ctx,
|
929
|
+
const renderer = new CanvasRenderer(ctx, importExportViewport);
|
930
930
|
|
931
931
|
this.image.renderAll(renderer);
|
932
932
|
|
@@ -935,12 +935,12 @@ export class Editor {
|
|
935
935
|
}
|
936
936
|
|
937
937
|
public toSVG(): SVGElement {
|
938
|
-
const importExportViewport = this.
|
938
|
+
const importExportViewport = this.image.getImportExportViewport();
|
939
939
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
940
940
|
const result = document.createElementNS(svgNameSpace, 'svg');
|
941
941
|
const renderer = new SVGRenderer(result, importExportViewport);
|
942
942
|
|
943
|
-
const origTransform =
|
943
|
+
const origTransform = importExportViewport.canvasToScreenTransform;
|
944
944
|
// Render with (0,0) at (0,0) — we'll handle translation with
|
945
945
|
// the viewBox property.
|
946
946
|
importExportViewport.resetTransform(Mat33.identity);
|
@@ -992,33 +992,12 @@ export class Editor {
|
|
992
992
|
|
993
993
|
// Returns the size of the visible region of the output SVG
|
994
994
|
public getImportExportRect(): Rect2 {
|
995
|
-
return this.
|
995
|
+
return this.image.getImportExportViewport().visibleRect;
|
996
996
|
}
|
997
997
|
|
998
998
|
// Resize the output SVG to match `imageRect`.
|
999
999
|
public setImportExportRect(imageRect: Rect2): Command {
|
1000
|
-
|
1001
|
-
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
1002
|
-
|
1003
|
-
return new class extends Command {
|
1004
|
-
public apply(editor: Editor) {
|
1005
|
-
const viewport = editor.importExportViewport;
|
1006
|
-
viewport.updateScreenSize(imageRect.size);
|
1007
|
-
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
1008
|
-
editor.queueRerender();
|
1009
|
-
}
|
1010
|
-
|
1011
|
-
public unapply(editor: Editor) {
|
1012
|
-
const viewport = editor.importExportViewport;
|
1013
|
-
viewport.updateScreenSize(origSize);
|
1014
|
-
viewport.resetTransform(origTransform);
|
1015
|
-
editor.queueRerender();
|
1016
|
-
}
|
1017
|
-
|
1018
|
-
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
1019
|
-
return localizationTable.resizeOutputCommand(imageRect);
|
1020
|
-
}
|
1021
|
-
};
|
1000
|
+
return this.image.setImportExportRect(imageRect);
|
1022
1001
|
}
|
1023
1002
|
|
1024
1003
|
/**
|
package/src/EditorImage.ts
CHANGED
@@ -6,21 +6,80 @@ import Rect2 from './math/Rect2';
|
|
6
6
|
import { EditorLocalization } from './localization';
|
7
7
|
import RenderingCache from './rendering/caching/RenderingCache';
|
8
8
|
import SerializableCommand from './commands/SerializableCommand';
|
9
|
+
import EventDispatcher from './EventDispatcher';
|
10
|
+
import { Vec2 } from './math/Vec2';
|
11
|
+
import Command from './commands/Command';
|
12
|
+
import Mat33 from './math/Mat33';
|
9
13
|
|
10
14
|
// @internal Sort by z-index, low to high
|
11
15
|
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
12
16
|
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
13
17
|
};
|
14
18
|
|
19
|
+
export enum EditorImageEventType {
|
20
|
+
ExportViewportChanged
|
21
|
+
}
|
22
|
+
|
23
|
+
export type EditorImageNotifier = EventDispatcher<EditorImageEventType, { image: EditorImage }>;
|
24
|
+
|
15
25
|
// Handles lookup/storage of elements in the image
|
16
26
|
export default class EditorImage {
|
17
27
|
private root: ImageNode;
|
18
28
|
private componentsById: Record<string, AbstractComponent>;
|
19
29
|
|
30
|
+
/** Viewport for the exported/imported image. */
|
31
|
+
private importExportViewport: Viewport;
|
32
|
+
|
33
|
+
// @internal
|
34
|
+
public readonly notifier: EditorImageNotifier;
|
35
|
+
|
20
36
|
// @internal
|
21
37
|
public constructor() {
|
22
38
|
this.root = new ImageNode();
|
23
39
|
this.componentsById = {};
|
40
|
+
|
41
|
+
this.notifier = new EventDispatcher();
|
42
|
+
this.importExportViewport = new Viewport(() => {
|
43
|
+
this.notifier.dispatch(EditorImageEventType.ExportViewportChanged, {
|
44
|
+
image: this,
|
45
|
+
});
|
46
|
+
});
|
47
|
+
|
48
|
+
// Default to a 500x500 image
|
49
|
+
this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* @returns a `Viewport` for rendering the image when importing/exporting.
|
54
|
+
*/
|
55
|
+
public getImportExportViewport() {
|
56
|
+
return this.importExportViewport;
|
57
|
+
}
|
58
|
+
|
59
|
+
public setImportExportRect(imageRect: Rect2) {
|
60
|
+
const importExportViewport = this.getImportExportViewport();
|
61
|
+
const origSize = importExportViewport.visibleRect.size;
|
62
|
+
const origTransform = importExportViewport.canvasToScreenTransform;
|
63
|
+
|
64
|
+
return new class extends Command {
|
65
|
+
public apply(editor: Editor) {
|
66
|
+
const viewport = editor.image.getImportExportViewport();
|
67
|
+
viewport.updateScreenSize(imageRect.size);
|
68
|
+
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
69
|
+
editor.queueRerender();
|
70
|
+
}
|
71
|
+
|
72
|
+
public unapply(editor: Editor) {
|
73
|
+
const viewport = editor.image.getImportExportViewport();
|
74
|
+
viewport.updateScreenSize(origSize);
|
75
|
+
viewport.resetTransform(origTransform);
|
76
|
+
editor.queueRerender();
|
77
|
+
}
|
78
|
+
|
79
|
+
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
80
|
+
return localizationTable.resizeOutputCommand(imageRect);
|
81
|
+
}
|
82
|
+
};
|
24
83
|
}
|
25
84
|
|
26
85
|
// Returns the parent of the given element, if it exists.
|
@@ -98,10 +157,17 @@ export default class EditorImage {
|
|
98
157
|
}
|
99
158
|
|
100
159
|
private addElementDirectly(elem: AbstractComponent): ImageNode {
|
160
|
+
elem.onAddToImage(this);
|
161
|
+
|
101
162
|
this.componentsById[elem.getId()] = elem;
|
102
163
|
return this.root.addLeaf(elem);
|
103
164
|
}
|
104
165
|
|
166
|
+
private removeElementDirectly(element: AbstractComponent) {
|
167
|
+
const container = this.findParent(element);
|
168
|
+
container?.remove();
|
169
|
+
}
|
170
|
+
|
105
171
|
/**
|
106
172
|
* Returns a command that adds the given element to the `EditorImage`.
|
107
173
|
* If `applyByFlattening` is true, the content of the wet ink renderer is
|
@@ -113,6 +179,11 @@ export default class EditorImage {
|
|
113
179
|
return new EditorImage.AddElementCommand(elem, applyByFlattening);
|
114
180
|
}
|
115
181
|
|
182
|
+
/** @see EditorImage.addElement */
|
183
|
+
public addElement(elem: AbstractComponent, applyByFlattening: boolean = true) {
|
184
|
+
return EditorImage.addElement(elem, applyByFlattening);
|
185
|
+
}
|
186
|
+
|
116
187
|
// A Command that can access private [EditorImage] functionality
|
117
188
|
private static AddElementCommand = class extends SerializableCommand {
|
118
189
|
private serializedElem: any;
|
@@ -147,8 +218,7 @@ export default class EditorImage {
|
|
147
218
|
}
|
148
219
|
|
149
220
|
public unapply(editor: Editor) {
|
150
|
-
|
151
|
-
container?.remove();
|
221
|
+
editor.image.removeElementDirectly(this.element);
|
152
222
|
editor.queueRerender();
|
153
223
|
}
|
154
224
|
|
@@ -395,6 +465,8 @@ export class ImageNode {
|
|
395
465
|
return;
|
396
466
|
}
|
397
467
|
|
468
|
+
this.content?.onRemoveFromImage();
|
469
|
+
|
398
470
|
const oldChildCount = this.parent.children.length;
|
399
471
|
this.parent.children = this.parent.children.filter(node => {
|
400
472
|
return node !== this;
|
package/src/EventDispatcher.ts
CHANGED
@@ -20,6 +20,9 @@
|
|
20
20
|
|
21
21
|
type Listener<Value> = (data: Value)=> void;
|
22
22
|
type CallbackHandler<EventType> = (data: EventType)=> void;
|
23
|
+
export interface DispatcherEventListener {
|
24
|
+
remove: ()=>void;
|
25
|
+
}
|
23
26
|
|
24
27
|
// { @inheritDoc EventDispatcher! }
|
25
28
|
export default class EventDispatcher<EventKeyType extends string|symbol|number, EventMessageType> {
|
@@ -38,7 +41,7 @@ export default class EventDispatcher<EventKeyType extends string|symbol|number,
|
|
38
41
|
}
|
39
42
|
}
|
40
43
|
|
41
|
-
public on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>) {
|
44
|
+
public on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): DispatcherEventListener {
|
42
45
|
if (!this.listeners[eventName]) this.listeners[eventName] = [];
|
43
46
|
this.listeners[eventName]!.push(callback);
|
44
47
|
|
package/src/SVGLoader.ts
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import Color4 from './Color4';
|
2
2
|
import AbstractComponent from './components/AbstractComponent';
|
3
|
+
import ImageBackground, { BackgroundType, imageBackgroundCSSClassName } from './components/ImageBackground';
|
3
4
|
import ImageComponent from './components/ImageComponent';
|
4
5
|
import Stroke from './components/Stroke';
|
5
6
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
@@ -186,6 +187,12 @@ export default class SVGLoader implements ImageLoader {
|
|
186
187
|
await this.onAddComponent?.(elem);
|
187
188
|
}
|
188
189
|
|
190
|
+
private async addBackground(node: SVGPathElement) {
|
191
|
+
const fill = Color4.fromString(node.getAttribute('fill') ?? node.style.fill ?? 'black');
|
192
|
+
const elem = new ImageBackground(BackgroundType.SolidColor, fill);
|
193
|
+
await this.onAddComponent?.(elem);
|
194
|
+
}
|
195
|
+
|
189
196
|
// If given, 'supportedAttrs' will have x, y, etc. attributes that were used in computing the transform added to it,
|
190
197
|
// to prevent storing duplicate transform information when saving the component.
|
191
198
|
private getTransform(elem: SVGElement, supportedAttrs?: string[], computedStyles?: CSSStyleDeclaration): Mat33 {
|
@@ -359,7 +366,11 @@ export default class SVGLoader implements ImageLoader {
|
|
359
366
|
// Continue -- visit the node's children.
|
360
367
|
break;
|
361
368
|
case 'path':
|
362
|
-
|
369
|
+
if (node.classList.contains(imageBackgroundCSSClassName)) {
|
370
|
+
await this.addBackground(node as SVGPathElement);
|
371
|
+
} else {
|
372
|
+
await this.addPath(node as SVGPathElement);
|
373
|
+
}
|
363
374
|
break;
|
364
375
|
case 'text':
|
365
376
|
await this.addText(node as SVGTextElement);
|