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,243 @@
|
|
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 _element, _applyByFlattening, _a;
|
13
|
+
import Rect2 from './geometry/Rect2';
|
14
|
+
// Handles lookup/storage of elements in the image
|
15
|
+
export default class EditorImage {
|
16
|
+
constructor() {
|
17
|
+
this.root = new ImageNode();
|
18
|
+
}
|
19
|
+
addElement(elem) {
|
20
|
+
return this.root.addLeaf(elem);
|
21
|
+
}
|
22
|
+
// Returns the parent of the given element, if it exists.
|
23
|
+
findParent(elem) {
|
24
|
+
const candidates = this.root.getLeavesInRegion(elem.getBBox());
|
25
|
+
for (const candidate of candidates) {
|
26
|
+
if (candidate.getContent() === elem) {
|
27
|
+
return candidate;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
return null;
|
31
|
+
}
|
32
|
+
sortLeaves(leaves) {
|
33
|
+
leaves.sort((a, b) => a.getContent().zIndex - b.getContent().zIndex);
|
34
|
+
}
|
35
|
+
render(renderer, viewport, minFraction = 0.001) {
|
36
|
+
// Don't render components that are < 0.1% of the viewport.
|
37
|
+
const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
|
38
|
+
this.sortLeaves(leaves);
|
39
|
+
for (const leaf of leaves) {
|
40
|
+
// Leaves by definition have content
|
41
|
+
leaf.getContent().render(renderer, viewport.visibleRect);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
// Renders all nodes, even ones not within the viewport
|
45
|
+
renderAll(renderer) {
|
46
|
+
const leaves = this.root.getLeaves();
|
47
|
+
this.sortLeaves(leaves);
|
48
|
+
for (const leaf of leaves) {
|
49
|
+
leaf.getContent().render(renderer, leaf.getBBox());
|
50
|
+
}
|
51
|
+
}
|
52
|
+
getElementsIntersectingRegion(region) {
|
53
|
+
const leaves = this.root.getLeavesInRegion(region);
|
54
|
+
this.sortLeaves(leaves);
|
55
|
+
return leaves.map(leaf => leaf.getContent());
|
56
|
+
}
|
57
|
+
}
|
58
|
+
// A Command that can access private [EditorImage] functionality
|
59
|
+
EditorImage.AddElementCommand = (_a = class {
|
60
|
+
// If [applyByFlattening], then the rendered content of this element
|
61
|
+
// is present on the display's wet ink canvas. As such, no re-render is necessary
|
62
|
+
// the first time this command is applied (the surfaces are joined instead).
|
63
|
+
constructor(element, applyByFlattening = false) {
|
64
|
+
_element.set(this, void 0);
|
65
|
+
_applyByFlattening.set(this, false);
|
66
|
+
__classPrivateFieldSet(this, _element, element, "f");
|
67
|
+
__classPrivateFieldSet(this, _applyByFlattening, applyByFlattening, "f");
|
68
|
+
}
|
69
|
+
apply(editor) {
|
70
|
+
editor.image.addElement(__classPrivateFieldGet(this, _element, "f"));
|
71
|
+
if (!__classPrivateFieldGet(this, _applyByFlattening, "f")) {
|
72
|
+
editor.queueRerender();
|
73
|
+
}
|
74
|
+
else {
|
75
|
+
__classPrivateFieldSet(this, _applyByFlattening, false, "f");
|
76
|
+
editor.display.flatten();
|
77
|
+
}
|
78
|
+
}
|
79
|
+
unapply(editor) {
|
80
|
+
const container = editor.image.findParent(__classPrivateFieldGet(this, _element, "f"));
|
81
|
+
container === null || container === void 0 ? void 0 : container.remove();
|
82
|
+
editor.queueRerender();
|
83
|
+
}
|
84
|
+
description(localization) {
|
85
|
+
return localization.addElementAction(__classPrivateFieldGet(this, _element, "f").description(localization));
|
86
|
+
}
|
87
|
+
},
|
88
|
+
_element = new WeakMap(),
|
89
|
+
_applyByFlattening = new WeakMap(),
|
90
|
+
_a);
|
91
|
+
export class ImageNode {
|
92
|
+
constructor(parent = null) {
|
93
|
+
this.parent = parent;
|
94
|
+
this.targetChildCount = 30;
|
95
|
+
this.children = [];
|
96
|
+
this.bbox = Rect2.empty;
|
97
|
+
this.content = null;
|
98
|
+
}
|
99
|
+
getContent() {
|
100
|
+
return this.content;
|
101
|
+
}
|
102
|
+
getParent() {
|
103
|
+
return this.parent;
|
104
|
+
}
|
105
|
+
getChildrenInRegion(region) {
|
106
|
+
return this.children.filter(child => {
|
107
|
+
return child.getBBox().intersects(region);
|
108
|
+
});
|
109
|
+
}
|
110
|
+
// / Returns a list of `ImageNode`s with content (and thus no children).
|
111
|
+
getLeavesInRegion(region, minFractionOfRegion = 0) {
|
112
|
+
const result = [];
|
113
|
+
// Don't render if too small
|
114
|
+
if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
|
115
|
+
return [];
|
116
|
+
}
|
117
|
+
if (this.content !== null && this.getBBox().intersects(region)) {
|
118
|
+
result.push(this);
|
119
|
+
}
|
120
|
+
const children = this.getChildrenInRegion(region);
|
121
|
+
for (const child of children) {
|
122
|
+
result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
|
123
|
+
}
|
124
|
+
return result;
|
125
|
+
}
|
126
|
+
// Returns a list of leaves with this as an ancestor.
|
127
|
+
// Like getLeavesInRegion, but does not check whether ancestors are in a given rectangle
|
128
|
+
getLeaves() {
|
129
|
+
if (this.content) {
|
130
|
+
return [this];
|
131
|
+
}
|
132
|
+
const result = [];
|
133
|
+
for (const child of this.children) {
|
134
|
+
result.push(...child.getLeaves());
|
135
|
+
}
|
136
|
+
return result;
|
137
|
+
}
|
138
|
+
addLeaf(leaf) {
|
139
|
+
if (this.content === null && this.children.length === 0) {
|
140
|
+
this.content = leaf;
|
141
|
+
this.recomputeBBox(true);
|
142
|
+
return this;
|
143
|
+
}
|
144
|
+
if (this.content !== null) {
|
145
|
+
console.assert(this.children.length === 0);
|
146
|
+
const contentNode = new ImageNode(this);
|
147
|
+
contentNode.content = this.content;
|
148
|
+
this.content = null;
|
149
|
+
this.children.push(contentNode);
|
150
|
+
contentNode.recomputeBBox(false);
|
151
|
+
}
|
152
|
+
// If this node is contained within the leaf, make this and the leaf
|
153
|
+
// share a parent.
|
154
|
+
const leafBBox = leaf.getBBox();
|
155
|
+
if (leafBBox.containsRect(this.getBBox())) {
|
156
|
+
// Create a node for this' children and for the new content..
|
157
|
+
const nodeForNewLeaf = new ImageNode(this);
|
158
|
+
const nodeForChildren = new ImageNode(this);
|
159
|
+
nodeForChildren.children = this.children;
|
160
|
+
this.children = [nodeForNewLeaf, nodeForChildren];
|
161
|
+
nodeForChildren.recomputeBBox(true);
|
162
|
+
return nodeForNewLeaf.addLeaf(leaf);
|
163
|
+
}
|
164
|
+
const containingNodes = this.children.filter(child => child.getBBox().containsRect(leafBBox));
|
165
|
+
// Does the leaf already fit within one of the children?
|
166
|
+
if (containingNodes.length > 0 && this.children.length >= this.targetChildCount) {
|
167
|
+
// Sort the containers in ascending order by area
|
168
|
+
containingNodes.sort((a, b) => a.getBBox().area - b.getBBox().area);
|
169
|
+
// Choose the smallest child that contains the new element.
|
170
|
+
const result = containingNodes[0].addLeaf(leaf);
|
171
|
+
result.rebalance();
|
172
|
+
return result;
|
173
|
+
}
|
174
|
+
const newNode = new ImageNode(this);
|
175
|
+
this.children.push(newNode);
|
176
|
+
newNode.content = leaf;
|
177
|
+
newNode.recomputeBBox(true);
|
178
|
+
return newNode;
|
179
|
+
}
|
180
|
+
getBBox() {
|
181
|
+
return this.bbox;
|
182
|
+
}
|
183
|
+
// Recomputes this' bounding box. If [bubbleUp], also recompute
|
184
|
+
// this' ancestors bounding boxes
|
185
|
+
recomputeBBox(bubbleUp) {
|
186
|
+
var _a;
|
187
|
+
const oldBBox = this.bbox;
|
188
|
+
if (this.content !== null) {
|
189
|
+
this.bbox = this.content.getBBox();
|
190
|
+
}
|
191
|
+
else {
|
192
|
+
this.bbox = Rect2.empty;
|
193
|
+
for (const child of this.children) {
|
194
|
+
this.bbox = this.bbox.union(child.getBBox());
|
195
|
+
}
|
196
|
+
}
|
197
|
+
if (bubbleUp && !oldBBox.eq(this.bbox)) {
|
198
|
+
(_a = this.parent) === null || _a === void 0 ? void 0 : _a.recomputeBBox(true);
|
199
|
+
}
|
200
|
+
}
|
201
|
+
rebalance() {
|
202
|
+
// If the current node is its parent's only child,
|
203
|
+
if (this.parent && this.parent.children.length === 1) {
|
204
|
+
console.assert(this.parent.content === null);
|
205
|
+
console.assert(this.parent.children[0] === this);
|
206
|
+
// Remove this' parent, if this' parent isn't the root.
|
207
|
+
const oldParent = this.parent;
|
208
|
+
if (oldParent.parent !== null) {
|
209
|
+
oldParent.children = [];
|
210
|
+
this.parent = oldParent.parent;
|
211
|
+
this.parent.children.push(this);
|
212
|
+
oldParent.parent = null;
|
213
|
+
this.parent.recomputeBBox(false);
|
214
|
+
}
|
215
|
+
else if (this.content === null) {
|
216
|
+
// Remove this and transfer this' children to the parent.
|
217
|
+
this.parent.children = this.children;
|
218
|
+
this.parent = null;
|
219
|
+
}
|
220
|
+
}
|
221
|
+
}
|
222
|
+
// Remove this node and all of its children
|
223
|
+
remove() {
|
224
|
+
if (!this.parent) {
|
225
|
+
this.content = null;
|
226
|
+
this.children = [];
|
227
|
+
return;
|
228
|
+
}
|
229
|
+
const oldChildCount = this.parent.children.length;
|
230
|
+
this.parent.children = this.parent.children.filter(node => {
|
231
|
+
return node !== this;
|
232
|
+
});
|
233
|
+
console.assert(this.parent.children.length === oldChildCount - 1);
|
234
|
+
this.parent.children.forEach(child => {
|
235
|
+
child.rebalance();
|
236
|
+
});
|
237
|
+
this.parent.recomputeBBox(true);
|
238
|
+
// Invalidate/disconnect this.
|
239
|
+
this.content = null;
|
240
|
+
this.parent = null;
|
241
|
+
this.children = [];
|
242
|
+
}
|
243
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
declare type CallbackHandler<EventType> = (data: EventType) => void;
|
2
|
+
export default class EventDispatcher<EventKeyType extends string | symbol | number, EventMessageType> {
|
3
|
+
private listeners;
|
4
|
+
constructor();
|
5
|
+
dispatch(eventName: EventKeyType, event: EventMessageType): void;
|
6
|
+
on(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): {
|
7
|
+
remove: () => boolean;
|
8
|
+
};
|
9
|
+
off(eventName: EventKeyType, callback: CallbackHandler<EventMessageType>): void;
|
10
|
+
}
|
11
|
+
export {};
|
@@ -0,0 +1,39 @@
|
|
1
|
+
// Code shared with Joplin
|
2
|
+
// EventKeyType is used to distinguish events (e.g. a 'ClickEvent' vs a 'TouchEvent')
|
3
|
+
// while EventMessageType is the type of the data sent with an event (can be `void`)
|
4
|
+
export default class EventDispatcher {
|
5
|
+
constructor() {
|
6
|
+
this.listeners = {};
|
7
|
+
}
|
8
|
+
dispatch(eventName, event) {
|
9
|
+
const listenerList = this.listeners[eventName];
|
10
|
+
if (listenerList) {
|
11
|
+
for (let i = 0; i < listenerList.length; i++) {
|
12
|
+
listenerList[i](event);
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
on(eventName, callback) {
|
17
|
+
if (!this.listeners[eventName])
|
18
|
+
this.listeners[eventName] = [];
|
19
|
+
this.listeners[eventName].push(callback);
|
20
|
+
return {
|
21
|
+
// Retuns false if the listener has already been removed, true otherwise.
|
22
|
+
remove: () => {
|
23
|
+
const originalListeners = this.listeners[eventName];
|
24
|
+
this.off(eventName, callback);
|
25
|
+
return originalListeners.length !== this.listeners[eventName].length;
|
26
|
+
},
|
27
|
+
};
|
28
|
+
}
|
29
|
+
// Equivalent to calling .remove() on the object returned by .on
|
30
|
+
off(eventName, callback) {
|
31
|
+
const listeners = this.listeners[eventName];
|
32
|
+
if (!listeners)
|
33
|
+
return;
|
34
|
+
// Replace the current list of listeners with a new, shortened list.
|
35
|
+
// This allows any iterators over this.listeners to continue iterating
|
36
|
+
// without skipping elements.
|
37
|
+
this.listeners[eventName] = listeners.filter(otherCallback => otherCallback !== callback);
|
38
|
+
}
|
39
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { Point2 } from './geometry/Vec2';
|
2
|
+
import Viewport from './Viewport';
|
3
|
+
export declare enum PointerDevice {
|
4
|
+
Pen = 0,
|
5
|
+
Eraser = 1,
|
6
|
+
Touch = 2,
|
7
|
+
Mouse = 3,
|
8
|
+
Other = 4
|
9
|
+
}
|
10
|
+
export default class Pointer {
|
11
|
+
readonly screenPos: Point2;
|
12
|
+
readonly canvasPos: Point2;
|
13
|
+
readonly pressure: number | null;
|
14
|
+
readonly isPrimary: boolean;
|
15
|
+
readonly down: boolean;
|
16
|
+
readonly device: PointerDevice;
|
17
|
+
readonly id: number;
|
18
|
+
readonly timeStamp: number;
|
19
|
+
private constructor();
|
20
|
+
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport): Pointer;
|
21
|
+
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
|
22
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import { Vec2 } from './geometry/Vec2';
|
2
|
+
export var PointerDevice;
|
3
|
+
(function (PointerDevice) {
|
4
|
+
PointerDevice[PointerDevice["Pen"] = 0] = "Pen";
|
5
|
+
PointerDevice[PointerDevice["Eraser"] = 1] = "Eraser";
|
6
|
+
PointerDevice[PointerDevice["Touch"] = 2] = "Touch";
|
7
|
+
PointerDevice[PointerDevice["Mouse"] = 3] = "Mouse";
|
8
|
+
PointerDevice[PointerDevice["Other"] = 4] = "Other";
|
9
|
+
})(PointerDevice || (PointerDevice = {}));
|
10
|
+
// Provides a snapshot containing information about a pointer. A Pointer
|
11
|
+
// object is immutable --- it will not be updated when the pointer's information changes.
|
12
|
+
export default class Pointer {
|
13
|
+
constructor(
|
14
|
+
// The (x, y) position of the pointer relative to the top-left corner
|
15
|
+
// of the visible canvas.
|
16
|
+
screenPos,
|
17
|
+
// Position of the pointer relative to the top left corner of the drawing
|
18
|
+
// surface.
|
19
|
+
canvasPos, pressure, isPrimary, down, device,
|
20
|
+
// Unique ID for the pointer
|
21
|
+
id,
|
22
|
+
// Numeric timestamp (milliseconds, as from (new Date).getTime())
|
23
|
+
timeStamp) {
|
24
|
+
this.screenPos = screenPos;
|
25
|
+
this.canvasPos = canvasPos;
|
26
|
+
this.pressure = pressure;
|
27
|
+
this.isPrimary = isPrimary;
|
28
|
+
this.down = down;
|
29
|
+
this.device = device;
|
30
|
+
this.id = id;
|
31
|
+
this.timeStamp = timeStamp;
|
32
|
+
}
|
33
|
+
static ofEvent(evt, isDown, viewport) {
|
34
|
+
var _a, _b;
|
35
|
+
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
|
36
|
+
const pointerTypeToDevice = {
|
37
|
+
'mouse': PointerDevice.Mouse,
|
38
|
+
'pen': PointerDevice.Pen,
|
39
|
+
'touch': PointerDevice.Touch,
|
40
|
+
};
|
41
|
+
let device = (_a = pointerTypeToDevice[evt.pointerType]) !== null && _a !== void 0 ? _a : PointerDevice.Other;
|
42
|
+
const eraserButtonMask = 0x20;
|
43
|
+
if (device === PointerDevice.Pen && (evt.buttons & eraserButtonMask) !== 0) {
|
44
|
+
device = PointerDevice.Eraser;
|
45
|
+
}
|
46
|
+
const timeStamp = (new Date()).getTime();
|
47
|
+
const canvasPos = viewport.screenToCanvas(screenPos);
|
48
|
+
return new Pointer(screenPos, canvasPos, (_b = evt.pressure) !== null && _b !== void 0 ? _b : null, evt.isPrimary, isDown, device, evt.pointerId, timeStamp);
|
49
|
+
}
|
50
|
+
// Create a new Pointer from a point on the canvas.
|
51
|
+
// Intended for unit tests.
|
52
|
+
static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
|
53
|
+
const screenPos = viewport.canvasToScreen(canvasPos);
|
54
|
+
const timeStamp = (new Date()).getTime();
|
55
|
+
return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
|
56
|
+
}
|
57
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import Rect2 from './geometry/Rect2';
|
2
|
+
import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
|
3
|
+
export declare const defaultSVGViewRect: Rect2;
|
4
|
+
export default class SVGLoader implements ImageLoader {
|
5
|
+
private source;
|
6
|
+
private onFinish?;
|
7
|
+
private onAddComponent;
|
8
|
+
private onProgress;
|
9
|
+
private processedCount;
|
10
|
+
private totalToProcess;
|
11
|
+
private rootViewBox;
|
12
|
+
private constructor();
|
13
|
+
private getStyle;
|
14
|
+
private strokeDataFromElem;
|
15
|
+
private addPath;
|
16
|
+
private addUnknownNode;
|
17
|
+
private updateViewBox;
|
18
|
+
private visit;
|
19
|
+
start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener): Promise<Rect2>;
|
20
|
+
static fromString(text: string): SVGLoader;
|
21
|
+
}
|
@@ -0,0 +1,204 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
10
|
+
import Color4 from './Color4';
|
11
|
+
import Stroke from './components/Stroke';
|
12
|
+
import UnknownSVGObject from './components/UnknownSVGObject';
|
13
|
+
import Path from './geometry/Path';
|
14
|
+
import Rect2 from './geometry/Rect2';
|
15
|
+
// Size of a loaded image if no size is specified.
|
16
|
+
export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
|
17
|
+
export default class SVGLoader {
|
18
|
+
constructor(source, onFinish) {
|
19
|
+
this.source = source;
|
20
|
+
this.onFinish = onFinish;
|
21
|
+
this.onAddComponent = null;
|
22
|
+
this.onProgress = null;
|
23
|
+
this.processedCount = 0;
|
24
|
+
this.totalToProcess = 0;
|
25
|
+
}
|
26
|
+
getStyle(node) {
|
27
|
+
var _a, _b, _c;
|
28
|
+
const style = {
|
29
|
+
fill: Color4.transparent,
|
30
|
+
};
|
31
|
+
const fillAttribute = (_a = node.getAttribute('fill')) !== null && _a !== void 0 ? _a : node.style.fill;
|
32
|
+
if (fillAttribute) {
|
33
|
+
try {
|
34
|
+
style.fill = Color4.fromString(fillAttribute);
|
35
|
+
}
|
36
|
+
catch (e) {
|
37
|
+
console.error('Unknown fill color,', fillAttribute);
|
38
|
+
}
|
39
|
+
}
|
40
|
+
const strokeAttribute = (_b = node.getAttribute('stroke')) !== null && _b !== void 0 ? _b : node.style.stroke;
|
41
|
+
const strokeWidthAttr = (_c = node.getAttribute('stroke-width')) !== null && _c !== void 0 ? _c : node.style.strokeWidth;
|
42
|
+
if (strokeAttribute) {
|
43
|
+
try {
|
44
|
+
let width = parseFloat(strokeWidthAttr !== null && strokeWidthAttr !== void 0 ? strokeWidthAttr : '1');
|
45
|
+
if (!isFinite(width)) {
|
46
|
+
width = 0;
|
47
|
+
}
|
48
|
+
style.stroke = {
|
49
|
+
width,
|
50
|
+
color: Color4.fromString(strokeAttribute),
|
51
|
+
};
|
52
|
+
}
|
53
|
+
catch (e) {
|
54
|
+
console.error('Error parsing stroke data:', e);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
return style;
|
58
|
+
}
|
59
|
+
strokeDataFromElem(node) {
|
60
|
+
var _a;
|
61
|
+
const result = [];
|
62
|
+
const pathData = (_a = node.getAttribute('d')) !== null && _a !== void 0 ? _a : '';
|
63
|
+
const style = this.getStyle(node);
|
64
|
+
// Break the path into chunks at each moveTo ('M') command:
|
65
|
+
const parts = pathData.split('M');
|
66
|
+
let isFirst = true;
|
67
|
+
for (const part of parts) {
|
68
|
+
if (part !== '') {
|
69
|
+
// We split the path by moveTo commands, so add the 'M' back in
|
70
|
+
// if it was present.
|
71
|
+
const current = !isFirst ? `M${part}` : part;
|
72
|
+
const path = Path.fromString(current);
|
73
|
+
const spec = path.toRenderable(style);
|
74
|
+
result.push(spec);
|
75
|
+
}
|
76
|
+
isFirst = false;
|
77
|
+
}
|
78
|
+
return result;
|
79
|
+
}
|
80
|
+
// Adds a stroke with a single path
|
81
|
+
addPath(node) {
|
82
|
+
var _a;
|
83
|
+
let elem;
|
84
|
+
try {
|
85
|
+
const strokeData = this.strokeDataFromElem(node);
|
86
|
+
elem = new Stroke(strokeData);
|
87
|
+
}
|
88
|
+
catch (e) {
|
89
|
+
console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
|
90
|
+
elem = new UnknownSVGObject(node);
|
91
|
+
}
|
92
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, elem);
|
93
|
+
}
|
94
|
+
addUnknownNode(node) {
|
95
|
+
var _a;
|
96
|
+
const component = new UnknownSVGObject(node);
|
97
|
+
(_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
|
98
|
+
}
|
99
|
+
updateViewBox(node) {
|
100
|
+
const viewBoxAttr = node.getAttribute('viewBox');
|
101
|
+
if (this.rootViewBox || !viewBoxAttr) {
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
const components = viewBoxAttr.split(/[ \t,]/);
|
105
|
+
const x = parseFloat(components[0]);
|
106
|
+
const y = parseFloat(components[1]);
|
107
|
+
const width = parseFloat(components[2]);
|
108
|
+
const height = parseFloat(components[3]);
|
109
|
+
if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
|
110
|
+
return;
|
111
|
+
}
|
112
|
+
this.rootViewBox = new Rect2(x, y, width, height);
|
113
|
+
}
|
114
|
+
visit(node) {
|
115
|
+
var _a;
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
117
|
+
this.totalToProcess += node.childElementCount;
|
118
|
+
switch (node.tagName.toLowerCase()) {
|
119
|
+
case 'g':
|
120
|
+
// Continue -- visit the node's children.
|
121
|
+
break;
|
122
|
+
case 'path':
|
123
|
+
this.addPath(node);
|
124
|
+
break;
|
125
|
+
case 'svg':
|
126
|
+
this.updateViewBox(node);
|
127
|
+
break;
|
128
|
+
default:
|
129
|
+
console.warn('Unknown SVG element,', node);
|
130
|
+
if (!(node instanceof SVGElement)) {
|
131
|
+
console.warn('Element', node, 'is not an SVGElement! Continuing anyway.');
|
132
|
+
}
|
133
|
+
this.addUnknownNode(node);
|
134
|
+
return;
|
135
|
+
}
|
136
|
+
for (const child of node.children) {
|
137
|
+
yield this.visit(child);
|
138
|
+
}
|
139
|
+
this.processedCount++;
|
140
|
+
yield ((_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, this.processedCount, this.totalToProcess));
|
141
|
+
});
|
142
|
+
}
|
143
|
+
start(onAddComponent, onProgress) {
|
144
|
+
var _a;
|
145
|
+
return __awaiter(this, void 0, void 0, function* () {
|
146
|
+
this.onAddComponent = onAddComponent;
|
147
|
+
this.onProgress = onProgress;
|
148
|
+
// Estimate the number of tags to process.
|
149
|
+
this.totalToProcess = this.source.childElementCount;
|
150
|
+
this.processedCount = 0;
|
151
|
+
this.rootViewBox = null;
|
152
|
+
yield this.visit(this.source);
|
153
|
+
const viewBox = this.rootViewBox;
|
154
|
+
let result = defaultSVGViewRect;
|
155
|
+
if (viewBox) {
|
156
|
+
result = Rect2.of(viewBox);
|
157
|
+
}
|
158
|
+
(_a = this.onFinish) === null || _a === void 0 ? void 0 : _a.call(this);
|
159
|
+
return result;
|
160
|
+
});
|
161
|
+
}
|
162
|
+
// TODO: Handling unsafe data! Tripple-check that this is secure!
|
163
|
+
static fromString(text) {
|
164
|
+
var _a, _b;
|
165
|
+
const sandbox = document.createElement('iframe');
|
166
|
+
sandbox.src = 'about:blank';
|
167
|
+
sandbox.setAttribute('sandbox', 'allow-same-origin');
|
168
|
+
sandbox.setAttribute('csp', 'default-src \'about:blank\'');
|
169
|
+
sandbox.style.display = 'none';
|
170
|
+
// Required to access the frame's DOM. See https://stackoverflow.com/a/17777943/17055750
|
171
|
+
document.body.appendChild(sandbox);
|
172
|
+
if (!sandbox.hasAttribute('sandbox')) {
|
173
|
+
sandbox.remove();
|
174
|
+
throw new Error('SVG loading iframe is not sandboxed.');
|
175
|
+
}
|
176
|
+
// Try running JavaScript within the iframe
|
177
|
+
const sandboxDoc = (_b = (_a = sandbox.contentWindow) === null || _a === void 0 ? void 0 : _a.document) !== null && _b !== void 0 ? _b : sandbox.contentDocument;
|
178
|
+
if (sandboxDoc == null)
|
179
|
+
throw new Error('Unable to open a sandboxed iframe!');
|
180
|
+
sandboxDoc.open();
|
181
|
+
sandboxDoc.write(`
|
182
|
+
<!DOCTYPE html>
|
183
|
+
<html>
|
184
|
+
<head>
|
185
|
+
<title>SVG Loading Sandbox</title>
|
186
|
+
</head>
|
187
|
+
<body>
|
188
|
+
<script>
|
189
|
+
console.error('JavaScript should not be able to run here!');
|
190
|
+
throw new Error(
|
191
|
+
'The SVG sandbox is broken! Please double-check the sandboxing setting.'
|
192
|
+
);
|
193
|
+
</script>
|
194
|
+
</body>
|
195
|
+
</html>
|
196
|
+
`);
|
197
|
+
sandboxDoc.close();
|
198
|
+
const svgElem = sandboxDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
199
|
+
svgElem.innerHTML = text;
|
200
|
+
return new SVGLoader(svgElem, () => {
|
201
|
+
sandbox.remove();
|
202
|
+
});
|
203
|
+
}
|
204
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import Color4 from './Color4';
|
2
|
+
import { RenderablePathSpec } from './rendering/AbstractRenderer';
|
3
|
+
import { Point2 } from './geometry/Vec2';
|
4
|
+
import Rect2 from './geometry/Rect2';
|
5
|
+
import Stroke from './components/Stroke';
|
6
|
+
export interface StrokeDataPoint {
|
7
|
+
pos: Point2;
|
8
|
+
width: number;
|
9
|
+
time: number;
|
10
|
+
color: Color4;
|
11
|
+
}
|
12
|
+
export default class StrokeBuilder {
|
13
|
+
private startPoint;
|
14
|
+
private minFitAllowed;
|
15
|
+
private maxFitAllowed;
|
16
|
+
private segments;
|
17
|
+
private buffer;
|
18
|
+
private lastPoint;
|
19
|
+
private lastExitingVec;
|
20
|
+
private currentCurve;
|
21
|
+
private curveStartWidth;
|
22
|
+
private curveEndWidth;
|
23
|
+
private momentum;
|
24
|
+
private bbox;
|
25
|
+
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number);
|
26
|
+
getBBox(): Rect2;
|
27
|
+
private getRenderingStyle;
|
28
|
+
preview(): RenderablePathSpec[];
|
29
|
+
build(): Stroke;
|
30
|
+
private roundPoint;
|
31
|
+
private finalizeCurrentCurve;
|
32
|
+
private currentSegmentToPath;
|
33
|
+
private computeExitingVec;
|
34
|
+
addPoint(newPoint: StrokeDataPoint): void;
|
35
|
+
}
|