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
package/src/Editor.ts
ADDED
@@ -0,0 +1,469 @@
|
|
1
|
+
|
2
|
+
import EditorImage from './EditorImage';
|
3
|
+
import ToolController from './tools/ToolController';
|
4
|
+
import { InputEvtType, PointerEvt, EditorNotifier, EditorEventType, ImageLoader } from './types';
|
5
|
+
import Command from './commands/Command';
|
6
|
+
import UndoRedoHistory from './UndoRedoHistory';
|
7
|
+
import Viewport from './Viewport';
|
8
|
+
import EventDispatcher from './EventDispatcher';
|
9
|
+
import { Point2, Vec2 } from './geometry/Vec2';
|
10
|
+
import Vec3 from './geometry/Vec3';
|
11
|
+
import HTMLToolbar from './toolbar/HTMLToolbar';
|
12
|
+
import { RenderablePathSpec } from './rendering/AbstractRenderer';
|
13
|
+
import Display, { RenderingMode } from './Display';
|
14
|
+
import SVGRenderer from './rendering/SVGRenderer';
|
15
|
+
import Color4 from './Color4';
|
16
|
+
import SVGLoader from './SVGLoader';
|
17
|
+
import Pointer from './Pointer';
|
18
|
+
import Mat33 from './geometry/Mat33';
|
19
|
+
import Rect2 from './geometry/Rect2';
|
20
|
+
import { defaultEditorLocalization, EditorLocalization } from './localization';
|
21
|
+
|
22
|
+
export class Editor {
|
23
|
+
// Wrapper around the viewport and toolbar
|
24
|
+
private container: HTMLElement;
|
25
|
+
private renderingRegion: HTMLElement;
|
26
|
+
|
27
|
+
public history: UndoRedoHistory;
|
28
|
+
public display: Display;
|
29
|
+
public image: EditorImage;
|
30
|
+
|
31
|
+
// Viewport for the exported/imported image
|
32
|
+
private importExportViewport: Viewport;
|
33
|
+
public localization: EditorLocalization = defaultEditorLocalization;
|
34
|
+
|
35
|
+
public viewport: Viewport;
|
36
|
+
public toolController: ToolController;
|
37
|
+
public notifier: EditorNotifier;
|
38
|
+
|
39
|
+
private loadingWarning: HTMLElement;
|
40
|
+
private accessibilityAnnounceArea: HTMLElement;
|
41
|
+
|
42
|
+
public constructor(
|
43
|
+
parent: HTMLElement,
|
44
|
+
renderingMode: RenderingMode = RenderingMode.CanvasRenderer,
|
45
|
+
|
46
|
+
// Uses a default English localization if a translation is not given.
|
47
|
+
localization?: Partial<EditorLocalization>,
|
48
|
+
) {
|
49
|
+
this.container = document.createElement('div');
|
50
|
+
this.renderingRegion = document.createElement('div');
|
51
|
+
this.container.appendChild(this.renderingRegion);
|
52
|
+
this.container.className = 'imageEditorContainer';
|
53
|
+
|
54
|
+
this.localization = {
|
55
|
+
...this.localization,
|
56
|
+
...localization,
|
57
|
+
};
|
58
|
+
|
59
|
+
this.loadingWarning = document.createElement('div');
|
60
|
+
this.loadingWarning.classList.add('loadingMessage');
|
61
|
+
this.loadingWarning.ariaLive = 'polite';
|
62
|
+
this.container.appendChild(this.loadingWarning);
|
63
|
+
|
64
|
+
this.accessibilityAnnounceArea = document.createElement('div');
|
65
|
+
this.accessibilityAnnounceArea.ariaLive = 'assertive';
|
66
|
+
this.accessibilityAnnounceArea.className = 'accessibilityAnnouncement';
|
67
|
+
this.container.appendChild(this.accessibilityAnnounceArea);
|
68
|
+
|
69
|
+
this.renderingRegion.style.touchAction = 'none';
|
70
|
+
this.renderingRegion.className = 'imageEditorRenderArea';
|
71
|
+
this.renderingRegion.setAttribute('tabIndex', '0');
|
72
|
+
this.renderingRegion.ariaLabel = this.localization.imageEditor;
|
73
|
+
|
74
|
+
this.notifier = new EventDispatcher();
|
75
|
+
this.importExportViewport = new Viewport(this.notifier);
|
76
|
+
this.viewport = new Viewport(this.notifier);
|
77
|
+
this.display = new Display(this, renderingMode, this.renderingRegion);
|
78
|
+
this.image = new EditorImage();
|
79
|
+
this.history = new UndoRedoHistory(this, this.announceRedoCallback, this.announceUndoCallback);
|
80
|
+
this.toolController = new ToolController(this, this.localization);
|
81
|
+
|
82
|
+
parent.appendChild(this.container);
|
83
|
+
|
84
|
+
// Default to a 500x500 image
|
85
|
+
this.importExportViewport.updateScreenSize(Vec2.of(500, 500));
|
86
|
+
|
87
|
+
this.viewport.updateScreenSize(
|
88
|
+
Vec2.of(this.display.width, this.display.height)
|
89
|
+
);
|
90
|
+
|
91
|
+
this.registerListeners();
|
92
|
+
this.rerender();
|
93
|
+
this.hideLoadingWarning();
|
94
|
+
}
|
95
|
+
|
96
|
+
// [fractionLoaded] should be a number from 0 to 1, where 1 represents completely loaded.
|
97
|
+
public showLoadingWarning(fractionLoaded: number) {
|
98
|
+
const loadingPercent = Math.round(fractionLoaded * 100);
|
99
|
+
this.loadingWarning.innerText = this.localization.loading(loadingPercent);
|
100
|
+
this.loadingWarning.style.display = 'block';
|
101
|
+
}
|
102
|
+
|
103
|
+
public hideLoadingWarning() {
|
104
|
+
this.loadingWarning.style.display = 'none';
|
105
|
+
|
106
|
+
this.announceForAccessibility(this.localization.doneLoading);
|
107
|
+
}
|
108
|
+
|
109
|
+
public announceForAccessibility(message: string) {
|
110
|
+
this.accessibilityAnnounceArea.innerText = message;
|
111
|
+
}
|
112
|
+
|
113
|
+
public addToolbar(): HTMLToolbar {
|
114
|
+
return new HTMLToolbar(this, this.container, this.localization);
|
115
|
+
}
|
116
|
+
|
117
|
+
private registerListeners() {
|
118
|
+
const pointers: Record<number, Pointer> = {};
|
119
|
+
const getPointerList = () => {
|
120
|
+
const nowTime = (new Date()).getTime();
|
121
|
+
|
122
|
+
const res: Pointer[] = [];
|
123
|
+
for (const id in pointers) {
|
124
|
+
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
125
|
+
if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
|
126
|
+
res.push(pointers[id]);
|
127
|
+
}
|
128
|
+
}
|
129
|
+
return res;
|
130
|
+
};
|
131
|
+
|
132
|
+
// May be required to prevent text selection on iOS/Safari:
|
133
|
+
// See https://stackoverflow.com/a/70992717/17055750
|
134
|
+
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
|
135
|
+
|
136
|
+
this.renderingRegion.addEventListener('pointerdown', evt => {
|
137
|
+
const pointer = Pointer.ofEvent(evt, true, this.viewport);
|
138
|
+
pointers[pointer.id] = pointer;
|
139
|
+
|
140
|
+
this.renderingRegion.setPointerCapture(pointer.id);
|
141
|
+
const event: PointerEvt = {
|
142
|
+
kind: InputEvtType.PointerDownEvt,
|
143
|
+
current: pointer,
|
144
|
+
allPointers: getPointerList(),
|
145
|
+
};
|
146
|
+
this.toolController.dispatchInputEvent(event);
|
147
|
+
|
148
|
+
return true;
|
149
|
+
});
|
150
|
+
|
151
|
+
this.renderingRegion.addEventListener('pointermove', evt => {
|
152
|
+
const pointer = Pointer.ofEvent(
|
153
|
+
evt, pointers[evt.pointerId]?.down ?? false, this.viewport
|
154
|
+
);
|
155
|
+
if (pointer.down) {
|
156
|
+
pointers[pointer.id] = pointer;
|
157
|
+
|
158
|
+
if (this.toolController.dispatchInputEvent({
|
159
|
+
kind: InputEvtType.PointerMoveEvt,
|
160
|
+
current: pointer,
|
161
|
+
allPointers: getPointerList(),
|
162
|
+
})) {
|
163
|
+
evt.preventDefault();
|
164
|
+
}
|
165
|
+
}
|
166
|
+
});
|
167
|
+
|
168
|
+
const pointerEnd = (evt: PointerEvent) => {
|
169
|
+
const pointer = Pointer.ofEvent(evt, false, this.viewport);
|
170
|
+
if (!pointers[pointer.id]) {
|
171
|
+
return;
|
172
|
+
}
|
173
|
+
|
174
|
+
pointers[pointer.id] = pointer;
|
175
|
+
this.renderingRegion.releasePointerCapture(pointer.id);
|
176
|
+
if (this.toolController.dispatchInputEvent({
|
177
|
+
kind: InputEvtType.PointerUpEvt,
|
178
|
+
current: pointer,
|
179
|
+
allPointers: getPointerList(),
|
180
|
+
})) {
|
181
|
+
evt.preventDefault();
|
182
|
+
}
|
183
|
+
delete pointers[pointer.id];
|
184
|
+
};
|
185
|
+
|
186
|
+
this.renderingRegion.addEventListener('pointerup', evt => {
|
187
|
+
pointerEnd(evt);
|
188
|
+
});
|
189
|
+
|
190
|
+
this.renderingRegion.addEventListener('pointercancel', evt => {
|
191
|
+
pointerEnd(evt);
|
192
|
+
});
|
193
|
+
|
194
|
+
this.renderingRegion.addEventListener('keydown', evt => {
|
195
|
+
if (this.toolController.dispatchInputEvent({
|
196
|
+
kind: InputEvtType.KeyPressEvent,
|
197
|
+
key: evt.key,
|
198
|
+
})) {
|
199
|
+
evt.preventDefault();
|
200
|
+
}
|
201
|
+
});
|
202
|
+
|
203
|
+
this.container.addEventListener('wheel', evt => {
|
204
|
+
let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
|
205
|
+
|
206
|
+
if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
207
|
+
delta = delta.times(15);
|
208
|
+
} else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
209
|
+
delta = delta.times(100);
|
210
|
+
}
|
211
|
+
|
212
|
+
if (evt.ctrlKey) {
|
213
|
+
delta = Vec3.of(0, 0, evt.deltaY);
|
214
|
+
}
|
215
|
+
|
216
|
+
const pos = Vec2.of(evt.clientX, evt.clientY);
|
217
|
+
if (this.toolController.dispatchInputEvent({
|
218
|
+
kind: InputEvtType.WheelEvt,
|
219
|
+
delta,
|
220
|
+
screenPos: pos,
|
221
|
+
})) {
|
222
|
+
evt.preventDefault();
|
223
|
+
return true;
|
224
|
+
}
|
225
|
+
return false;
|
226
|
+
});
|
227
|
+
|
228
|
+
this.notifier.on(EditorEventType.DisplayResized, _event => {
|
229
|
+
this.viewport.updateScreenSize(
|
230
|
+
Vec2.of(this.display.width, this.display.height)
|
231
|
+
);
|
232
|
+
});
|
233
|
+
|
234
|
+
window.addEventListener('resize', () => {
|
235
|
+
this.notifier.dispatch(EditorEventType.DisplayResized, {
|
236
|
+
kind: EditorEventType.DisplayResized,
|
237
|
+
newSize: Vec2.of(
|
238
|
+
this.display.width,
|
239
|
+
this.display.height
|
240
|
+
),
|
241
|
+
});
|
242
|
+
this.queueRerender();
|
243
|
+
});
|
244
|
+
}
|
245
|
+
|
246
|
+
public dispatch(command: Command, addToHistory: boolean = true) {
|
247
|
+
if (addToHistory) {
|
248
|
+
// .push applies [command] to this
|
249
|
+
this.history.push(command);
|
250
|
+
} else {
|
251
|
+
command.apply(this);
|
252
|
+
}
|
253
|
+
|
254
|
+
this.announceForAccessibility(command.description(this.localization));
|
255
|
+
}
|
256
|
+
|
257
|
+
// Apply a large transformation in chunks.
|
258
|
+
// If [apply] is false, the commands are unapplied.
|
259
|
+
// Triggers a re-render after each [updateChunkSize]-sized group of commands
|
260
|
+
// has been applied.
|
261
|
+
private async asyncApplyOrUnapplyCommands(
|
262
|
+
commands: Command[], apply: boolean, updateChunkSize: number
|
263
|
+
) {
|
264
|
+
for (let i = 0; i < commands.length; i += updateChunkSize) {
|
265
|
+
this.showLoadingWarning(i / commands.length);
|
266
|
+
|
267
|
+
for (let j = i; j < commands.length && j < i + updateChunkSize; j++) {
|
268
|
+
const cmd = commands[j];
|
269
|
+
|
270
|
+
if (apply) {
|
271
|
+
cmd.apply(this);
|
272
|
+
} else {
|
273
|
+
cmd.unapply(this);
|
274
|
+
}
|
275
|
+
}
|
276
|
+
|
277
|
+
// Re-render to show progress, but only if we're not done.
|
278
|
+
if (i + updateChunkSize < commands.length) {
|
279
|
+
await new Promise(resolve => {
|
280
|
+
this.rerender();
|
281
|
+
requestAnimationFrame(resolve);
|
282
|
+
});
|
283
|
+
}
|
284
|
+
}
|
285
|
+
this.hideLoadingWarning();
|
286
|
+
}
|
287
|
+
|
288
|
+
public asyncApplyCommands(commands: Command[], chunkSize: number) {
|
289
|
+
return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
|
290
|
+
}
|
291
|
+
|
292
|
+
public asyncUnapplyCommands(commands: Command[], chunkSize: number) {
|
293
|
+
return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
|
294
|
+
}
|
295
|
+
|
296
|
+
private announceUndoCallback = (command: Command) => {
|
297
|
+
this.announceForAccessibility(this.localization.undoAnnouncement(command.description(this.localization)));
|
298
|
+
};
|
299
|
+
|
300
|
+
private announceRedoCallback = (command: Command) => {
|
301
|
+
this.announceForAccessibility(this.localization.redoAnnouncement(command.description(this.localization)));
|
302
|
+
};
|
303
|
+
|
304
|
+
private rerenderQueued: boolean = false;
|
305
|
+
public queueRerender() {
|
306
|
+
if (!this.rerenderQueued) {
|
307
|
+
this.rerenderQueued = true;
|
308
|
+
requestAnimationFrame(() => {
|
309
|
+
this.rerender();
|
310
|
+
this.rerenderQueued = false;
|
311
|
+
});
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
public rerender(showImageBounds: boolean = true) {
|
316
|
+
this.display.startRerender();
|
317
|
+
|
318
|
+
// Draw a rectangle around the region that will be visible on save
|
319
|
+
const renderer = this.display.getDryInkRenderer();
|
320
|
+
|
321
|
+
if (showImageBounds) {
|
322
|
+
const exportRectFill = { fill: Color4.fromHex('#44444455') };
|
323
|
+
const exportRectStrokeWidth = 12;
|
324
|
+
renderer.drawRect(
|
325
|
+
this.importExportViewport.visibleRect,
|
326
|
+
exportRectStrokeWidth,
|
327
|
+
exportRectFill
|
328
|
+
);
|
329
|
+
}
|
330
|
+
|
331
|
+
this.image.render(renderer, this.viewport);
|
332
|
+
this.rerenderQueued = false;
|
333
|
+
}
|
334
|
+
|
335
|
+
public drawWetInk(...path: RenderablePathSpec[]) {
|
336
|
+
for (const part of path) {
|
337
|
+
this.display.getWetInkRenderer().drawPath(part);
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
public clearWetInk() {
|
342
|
+
this.display.getWetInkRenderer().clear();
|
343
|
+
}
|
344
|
+
|
345
|
+
public createHTMLOverlay(overlay: HTMLElement) {
|
346
|
+
overlay.classList.add('overlay');
|
347
|
+
this.container.appendChild(overlay);
|
348
|
+
|
349
|
+
return {
|
350
|
+
remove: () => overlay.remove(),
|
351
|
+
};
|
352
|
+
}
|
353
|
+
|
354
|
+
public addStyleSheet(content: string): HTMLStyleElement {
|
355
|
+
const styleSheet = document.createElement('style');
|
356
|
+
styleSheet.innerText = content;
|
357
|
+
this.container.appendChild(styleSheet);
|
358
|
+
|
359
|
+
return styleSheet;
|
360
|
+
}
|
361
|
+
|
362
|
+
// Dispatch a pen event to the currently selected tool.
|
363
|
+
// Intented for unit tests.
|
364
|
+
public sendPenEvent(
|
365
|
+
eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
|
366
|
+
point: Point2,
|
367
|
+
allPointers?: Pointer[]
|
368
|
+
) {
|
369
|
+
const mainPointer = Pointer.ofCanvasPoint(
|
370
|
+
point, eventType !== InputEvtType.PointerUpEvt, this.viewport
|
371
|
+
);
|
372
|
+
this.toolController.dispatchInputEvent({
|
373
|
+
kind: eventType,
|
374
|
+
allPointers: allPointers ?? [
|
375
|
+
mainPointer,
|
376
|
+
],
|
377
|
+
current: mainPointer,
|
378
|
+
});
|
379
|
+
}
|
380
|
+
|
381
|
+
public toSVG(): SVGElement {
|
382
|
+
const importExportViewport = this.importExportViewport;
|
383
|
+
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
384
|
+
const result = document.createElementNS(svgNameSpace, 'svg');
|
385
|
+
const renderer = new SVGRenderer(result, importExportViewport);
|
386
|
+
|
387
|
+
const origTransform = importExportViewport.canvasToScreenTransform;
|
388
|
+
// Reset the transform to ensure that (0, 0) is (0, 0)
|
389
|
+
importExportViewport.resetTransform(Mat33.identity);
|
390
|
+
|
391
|
+
// Render **all** elements.
|
392
|
+
this.image.renderAll(renderer);
|
393
|
+
|
394
|
+
importExportViewport.resetTransform(origTransform);
|
395
|
+
|
396
|
+
// Just show the main region
|
397
|
+
const rect = importExportViewport.visibleRect;
|
398
|
+
result.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.w} ${rect.h}`);
|
399
|
+
result.setAttribute('width', `${rect.w}`);
|
400
|
+
result.setAttribute('height', `${rect.h}`);
|
401
|
+
|
402
|
+
// Ensure the image can be identified as an SVG if downloaded.
|
403
|
+
// See https://jwatt.org/svg/authoring/
|
404
|
+
result.setAttribute('version', '1.1');
|
405
|
+
result.setAttribute('baseProfile', 'full');
|
406
|
+
result.setAttribute('xmlns', svgNameSpace);
|
407
|
+
|
408
|
+
|
409
|
+
return result;
|
410
|
+
}
|
411
|
+
|
412
|
+
public async loadFrom(loader: ImageLoader) {
|
413
|
+
this.showLoadingWarning(0);
|
414
|
+
const imageRect = await loader.start((component) => {
|
415
|
+
(new EditorImage.AddElementCommand(component)).apply(this);
|
416
|
+
}, (countProcessed: number, totalToProcess: number) => {
|
417
|
+
if (countProcessed % 100 === 0) {
|
418
|
+
this.showLoadingWarning(countProcessed / totalToProcess);
|
419
|
+
this.rerender(false);
|
420
|
+
return new Promise(resolve => {
|
421
|
+
requestAnimationFrame(() => resolve());
|
422
|
+
});
|
423
|
+
}
|
424
|
+
|
425
|
+
return null;
|
426
|
+
});
|
427
|
+
this.hideLoadingWarning();
|
428
|
+
|
429
|
+
this.setImportExportRect(imageRect).apply(this);
|
430
|
+
}
|
431
|
+
|
432
|
+
// Returns the size of the visible region of the output SVG
|
433
|
+
public getImportExportRect(): Rect2 {
|
434
|
+
return this.importExportViewport.visibleRect;
|
435
|
+
}
|
436
|
+
|
437
|
+
// Resize the output SVG
|
438
|
+
public setImportExportRect(imageRect: Rect2): Command {
|
439
|
+
const origSize = this.importExportViewport.visibleRect.size;
|
440
|
+
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
441
|
+
|
442
|
+
return {
|
443
|
+
apply(editor) {
|
444
|
+
const viewport = editor.importExportViewport;
|
445
|
+
viewport.updateScreenSize(imageRect.size);
|
446
|
+
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
447
|
+
editor.queueRerender();
|
448
|
+
},
|
449
|
+
unapply(editor) {
|
450
|
+
const viewport = editor.importExportViewport;
|
451
|
+
viewport.updateScreenSize(origSize);
|
452
|
+
viewport.resetTransform(origTransform);
|
453
|
+
editor.queueRerender();
|
454
|
+
},
|
455
|
+
description(localizationTable) {
|
456
|
+
return localizationTable.resizeOutputCommand(imageRect);
|
457
|
+
},
|
458
|
+
};
|
459
|
+
}
|
460
|
+
|
461
|
+
// Alias for loadFrom(SVGLoader.fromString).
|
462
|
+
// This is particularly useful when accessing a bundled version of the editor.
|
463
|
+
public async loadFromSVG(svgData: string) {
|
464
|
+
const loader = SVGLoader.fromString(svgData);
|
465
|
+
await this.loadFrom(loader);
|
466
|
+
}
|
467
|
+
}
|
468
|
+
|
469
|
+
export default Editor;
|
@@ -0,0 +1,90 @@
|
|
1
|
+
/* @jest-environment jsdom */
|
2
|
+
|
3
|
+
import EditorImage from './EditorImage';
|
4
|
+
import Stroke from './components/Stroke';
|
5
|
+
import { Vec2 } from './geometry/Vec2';
|
6
|
+
import Path, { PathCommandType } from './geometry/Path';
|
7
|
+
import Color4 from './Color4';
|
8
|
+
import Editor from './Editor';
|
9
|
+
import { RenderingMode } from './Display';
|
10
|
+
import DummyRenderer from './rendering/DummyRenderer';
|
11
|
+
import { RenderingStyle } from './rendering/AbstractRenderer';
|
12
|
+
|
13
|
+
describe('EditorImage', () => {
|
14
|
+
const testStroke = new Stroke([
|
15
|
+
{
|
16
|
+
startPoint: Vec2.of(0, 0),
|
17
|
+
commands: [
|
18
|
+
{
|
19
|
+
kind: PathCommandType.MoveTo,
|
20
|
+
point: Vec2.of(3, 3),
|
21
|
+
},
|
22
|
+
],
|
23
|
+
style: {
|
24
|
+
fill: Color4.red,
|
25
|
+
},
|
26
|
+
},
|
27
|
+
]);
|
28
|
+
const testFill: RenderingStyle = { fill: Color4.black };
|
29
|
+
const addTestStrokeCommand = new EditorImage.AddElementCommand(testStroke);
|
30
|
+
|
31
|
+
it('elements added to the image should be findable', () => {
|
32
|
+
const editor = new Editor(document.body, RenderingMode.DummyRenderer);
|
33
|
+
const image = editor.image;
|
34
|
+
|
35
|
+
// We haven't activated the command, so testStroke's parent should be null.
|
36
|
+
expect(image.findParent(testStroke)).toBeNull();
|
37
|
+
addTestStrokeCommand.apply(editor);
|
38
|
+
expect(image.findParent(testStroke)).not.toBeNull();
|
39
|
+
});
|
40
|
+
|
41
|
+
it('should render an element added to the image', () => {
|
42
|
+
const editor = new Editor(document.body, RenderingMode.DummyRenderer);
|
43
|
+
const renderer = editor.display.getDryInkRenderer();
|
44
|
+
if (!(renderer instanceof DummyRenderer)) {
|
45
|
+
throw new Error('Wrong display type!');
|
46
|
+
}
|
47
|
+
|
48
|
+
const emptyDocumentPathCount = renderer.renderedPathCount;
|
49
|
+
expect(renderer.objectNestingLevel).toBe(0);
|
50
|
+
editor.dispatch(addTestStrokeCommand);
|
51
|
+
editor.rerender();
|
52
|
+
expect(renderer.renderedPathCount - emptyDocumentPathCount).toBe(1);
|
53
|
+
|
54
|
+
// Should not be within objects after finished rendering
|
55
|
+
expect(renderer.objectNestingLevel).toBe(0);
|
56
|
+
});
|
57
|
+
|
58
|
+
it('should have a 1-deep tree if two non-overlapping strokes are added', () => {
|
59
|
+
const editor = new Editor(document.body, RenderingMode.DummyRenderer);
|
60
|
+
const image = editor.image;
|
61
|
+
|
62
|
+
const leftmostStroke = new Stroke([
|
63
|
+
Path.fromString('M0,0L1,1L0,1').toRenderable(testFill),
|
64
|
+
]);
|
65
|
+
|
66
|
+
// Lowercase ls: lineTo(Δx, Δy) instead of lineTo(x, y)
|
67
|
+
const rightmostStroke = new Stroke([
|
68
|
+
Path.fromString('M-10,0 l1,1 l0,-1').toRenderable(testFill),
|
69
|
+
]);
|
70
|
+
|
71
|
+
expect(!leftmostStroke.getBBox().intersects(rightmostStroke.getBBox()));
|
72
|
+
|
73
|
+
(new EditorImage.AddElementCommand(leftmostStroke)).apply(editor);
|
74
|
+
|
75
|
+
// The first node should be at the image's root.
|
76
|
+
let firstParent = image.findParent(leftmostStroke);
|
77
|
+
expect(firstParent).not.toBe(null);
|
78
|
+
expect(firstParent?.getParent()).toBe(null);
|
79
|
+
expect(firstParent?.getBBox()?.corners).toMatchObject(leftmostStroke.getBBox()?.corners);
|
80
|
+
|
81
|
+
(new EditorImage.AddElementCommand(rightmostStroke)).apply(editor);
|
82
|
+
|
83
|
+
firstParent = image.findParent(leftmostStroke);
|
84
|
+
const secondParent = image.findParent(rightmostStroke);
|
85
|
+
|
86
|
+
expect(firstParent).not.toStrictEqual(secondParent);
|
87
|
+
expect(firstParent?.getParent()).toStrictEqual(secondParent?.getParent());
|
88
|
+
expect(firstParent?.getParent()?.getParent()).toBeNull();
|
89
|
+
});
|
90
|
+
});
|