js-draw 0.1.4 → 0.1.7
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 +1 -0
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.js +6 -2
- package/dist/src/Editor.d.ts +1 -0
- package/dist/src/Editor.js +20 -9
- package/dist/src/EditorImage.d.ts +8 -13
- package/dist/src/EditorImage.js +51 -29
- package/dist/src/SVGLoader.js +6 -2
- package/dist/src/Viewport.d.ts +10 -2
- package/dist/src/Viewport.js +8 -6
- package/dist/src/commands/Command.d.ts +9 -8
- package/dist/src/commands/Command.js +15 -14
- package/dist/src/commands/Duplicate.d.ts +14 -0
- package/dist/src/commands/Duplicate.js +34 -0
- package/dist/src/commands/Erase.d.ts +5 -2
- package/dist/src/commands/Erase.js +28 -9
- package/dist/src/commands/SerializableCommand.d.ts +13 -0
- package/dist/src/commands/SerializableCommand.js +28 -0
- package/dist/src/commands/localization.d.ts +2 -0
- package/dist/src/commands/localization.js +2 -0
- package/dist/src/components/AbstractComponent.d.ts +15 -2
- package/dist/src/components/AbstractComponent.js +122 -26
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
- package/dist/src/components/Stroke.d.ts +5 -0
- package/dist/src/components/Stroke.js +32 -1
- package/dist/src/components/Text.d.ts +11 -4
- package/dist/src/components/Text.js +57 -3
- package/dist/src/components/UnknownSVGObject.d.ts +2 -0
- package/dist/src/components/UnknownSVGObject.js +12 -1
- package/dist/src/components/util/describeComponentList.d.ts +4 -0
- package/dist/src/components/util/describeComponentList.js +14 -0
- package/dist/src/geometry/Path.d.ts +4 -1
- package/dist/src/geometry/Path.js +4 -0
- package/dist/src/localization.d.ts +2 -1
- package/dist/src/localization.js +2 -1
- package/dist/src/rendering/Display.d.ts +5 -0
- package/dist/src/rendering/Display.js +32 -0
- package/dist/src/rendering/RenderingStyle.d.ts +24 -0
- package/dist/src/rendering/RenderingStyle.js +32 -0
- package/dist/src/rendering/localization.d.ts +5 -0
- package/dist/src/rendering/localization.js +4 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
- package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +25 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -1
- package/dist/src/toolbar/HTMLToolbar.js +52 -534
- package/dist/src/toolbar/icons.d.ts +5 -0
- package/dist/src/toolbar/icons.js +186 -13
- package/dist/src/toolbar/localization.d.ts +4 -0
- package/dist/src/toolbar/localization.js +4 -0
- package/dist/src/toolbar/makeColorInput.d.ts +5 -0
- package/dist/src/toolbar/makeColorInput.js +81 -0
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
- package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
- package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
- package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
- package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
- package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
- package/dist/src/toolbar/widgets/PenWidget.js +131 -0
- package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
- package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
- package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
- package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/PipetteTool.d.ts +20 -0
- package/dist/src/tools/PipetteTool.js +40 -0
- package/dist/src/tools/SelectionTool.d.ts +2 -0
- package/dist/src/tools/SelectionTool.js +41 -23
- package/dist/src/tools/TextTool.d.ts +1 -0
- package/dist/src/tools/TextTool.js +5 -4
- package/dist/src/tools/ToolController.d.ts +3 -1
- package/dist/src/tools/ToolController.js +4 -0
- package/dist/src/tools/localization.d.ts +2 -1
- package/dist/src/tools/localization.js +3 -2
- package/dist/src/types.d.ts +7 -2
- package/dist/src/types.js +1 -0
- package/jest.config.js +2 -0
- package/package.json +6 -6
- package/src/Color4.ts +9 -3
- package/src/Editor.css +10 -0
- package/src/Editor.ts +24 -12
- package/src/EditorImage.test.ts +4 -4
- package/src/EditorImage.ts +61 -20
- package/src/SVGLoader.ts +9 -3
- package/src/Viewport.ts +7 -6
- package/src/commands/Command.ts +21 -19
- package/src/commands/Duplicate.ts +49 -0
- package/src/commands/Erase.ts +34 -13
- package/src/commands/SerializableCommand.ts +41 -0
- package/src/commands/localization.ts +5 -0
- package/src/components/AbstractComponent.ts +168 -26
- package/src/components/SVGGlobalAttributesObject.ts +34 -2
- package/src/components/Stroke.test.ts +53 -0
- package/src/components/Stroke.ts +37 -2
- package/src/components/Text.test.ts +38 -0
- package/src/components/Text.ts +80 -5
- package/src/components/UnknownSVGObject.test.ts +10 -0
- package/src/components/UnknownSVGObject.ts +15 -1
- package/src/components/builders/FreehandLineBuilder.ts +2 -1
- package/src/components/util/describeComponentList.ts +18 -0
- package/src/geometry/Path.ts +8 -1
- package/src/localization.ts +3 -1
- package/src/rendering/Display.ts +43 -1
- package/src/rendering/RenderingStyle.test.ts +68 -0
- package/src/rendering/RenderingStyle.ts +46 -0
- package/src/rendering/caching/RenderingCache.test.ts +1 -1
- package/src/rendering/localization.ts +10 -0
- package/src/rendering/renderers/AbstractRenderer.ts +1 -15
- package/src/rendering/renderers/CanvasRenderer.ts +2 -1
- package/src/rendering/renderers/DummyRenderer.ts +2 -1
- package/src/rendering/renderers/SVGRenderer.ts +2 -1
- package/src/rendering/renderers/TextOnlyRenderer.ts +52 -0
- package/src/toolbar/HTMLToolbar.ts +58 -660
- package/src/toolbar/icons.ts +205 -13
- package/src/toolbar/localization.ts +10 -2
- package/src/toolbar/makeColorInput.ts +105 -0
- package/src/toolbar/toolbar.css +116 -78
- package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
- package/src/toolbar/widgets/BaseWidget.ts +175 -0
- package/src/toolbar/widgets/EraserWidget.ts +16 -0
- package/src/toolbar/widgets/HandToolWidget.ts +186 -0
- package/src/toolbar/widgets/PenWidget.ts +165 -0
- package/src/toolbar/widgets/SelectionWidget.ts +72 -0
- package/src/toolbar/widgets/TextToolWidget.ts +90 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/PipetteTool.ts +56 -0
- package/src/tools/SelectionTool.test.ts +2 -4
- package/src/tools/SelectionTool.ts +47 -27
- package/src/tools/TextTool.ts +7 -3
- package/src/tools/ToolController.ts +10 -6
- package/src/tools/UndoRedoShortcut.test.ts +1 -1
- package/src/tools/localization.ts +6 -3
- package/src/types.ts +12 -1
@@ -0,0 +1,28 @@
|
|
1
|
+
import Command from './Command';
|
2
|
+
export default class SerializableCommand extends Command {
|
3
|
+
constructor(commandTypeId) {
|
4
|
+
super();
|
5
|
+
this.commandTypeId = commandTypeId;
|
6
|
+
if (!(commandTypeId in SerializableCommand.deserializationCallbacks)) {
|
7
|
+
throw new Error(`Command ${commandTypeId} must have a registered deserialization callback. To do this, call SerializableCommand.register.`);
|
8
|
+
}
|
9
|
+
}
|
10
|
+
serialize() {
|
11
|
+
return JSON.stringify({
|
12
|
+
data: this.serializeToString(),
|
13
|
+
commandType: this.commandTypeId,
|
14
|
+
});
|
15
|
+
}
|
16
|
+
static deserialize(data, editor) {
|
17
|
+
const json = JSON.parse(data);
|
18
|
+
const commandType = json.commandType;
|
19
|
+
if (!(commandType in SerializableCommand.deserializationCallbacks)) {
|
20
|
+
throw new Error(`Unrecognised command type ${commandType}!`);
|
21
|
+
}
|
22
|
+
return SerializableCommand.deserializationCallbacks[commandType](json.data, editor);
|
23
|
+
}
|
24
|
+
static register(commandTypeId, deserialize) {
|
25
|
+
SerializableCommand.deserializationCallbacks[commandTypeId] = deserialize;
|
26
|
+
}
|
27
|
+
}
|
28
|
+
SerializableCommand.deserializationCallbacks = {};
|
@@ -8,12 +8,14 @@ export interface CommandLocalization {
|
|
8
8
|
zoomedOut: string;
|
9
9
|
zoomedIn: string;
|
10
10
|
erasedNoElements: string;
|
11
|
+
duplicatedNoElements: string;
|
11
12
|
elements: string;
|
12
13
|
updatedViewport: string;
|
13
14
|
transformedElements: (elemCount: number) => string;
|
14
15
|
resizeOutputCommand: (newSize: Rect2) => string;
|
15
16
|
addElementAction: (elemDescription: string) => string;
|
16
17
|
eraseAction: (elemDescription: string, numElems: number) => string;
|
18
|
+
duplicateAction: (elemDescription: string, count: number) => string;
|
17
19
|
selectedElements: (count: number) => string;
|
18
20
|
}
|
19
21
|
export declare const defaultCommandLocalization: CommandLocalization;
|
@@ -4,8 +4,10 @@ export const defaultCommandLocalization = {
|
|
4
4
|
resizeOutputCommand: (newSize) => `Resized image to ${newSize.w}x${newSize.h}`,
|
5
5
|
addElementAction: (componentDescription) => `Added ${componentDescription}`,
|
6
6
|
eraseAction: (componentDescription, numElems) => `Erased ${numElems} ${componentDescription}`,
|
7
|
+
duplicateAction: (componentDescription, numElems) => `Duplicated ${numElems} ${componentDescription}`,
|
7
8
|
elements: 'Elements',
|
8
9
|
erasedNoElements: 'Erased nothing',
|
10
|
+
duplicatedNoElements: 'Duplicated nothing',
|
9
11
|
rotatedBy: (degrees) => `Rotated by ${Math.abs(degrees)} degrees ${degrees < 0 ? 'clockwise' : 'counter-clockwise'}`,
|
10
12
|
movedLeft: 'Moved left',
|
11
13
|
movedUp: 'Moved up',
|
@@ -4,14 +4,20 @@ import Mat33 from '../geometry/Mat33';
|
|
4
4
|
import Rect2 from '../geometry/Rect2';
|
5
5
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
|
-
declare type LoadSaveData =
|
7
|
+
declare type LoadSaveData = (string[] | Record<symbol, string | number>);
|
8
8
|
export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
|
9
|
+
declare type DeserializeCallback = (data: string) => AbstractComponent;
|
9
10
|
export default abstract class AbstractComponent {
|
11
|
+
private readonly componentKind;
|
10
12
|
protected lastChangedTime: number;
|
11
13
|
protected abstract contentBBox: Rect2;
|
12
14
|
private zIndex;
|
15
|
+
private id;
|
13
16
|
private static zIndexCounter;
|
14
|
-
protected constructor();
|
17
|
+
protected constructor(componentKind: string);
|
18
|
+
getId(): string;
|
19
|
+
private static deserializationCallbacks;
|
20
|
+
static registerComponent(componentKind: string, deserialize: DeserializeCallback | null): void;
|
15
21
|
private loadSaveData;
|
16
22
|
attachLoadSaveData(key: string, data: LoadSaveData): void;
|
17
23
|
getLoadSaveData(): LoadSaveDataTable;
|
@@ -19,8 +25,15 @@ export default abstract class AbstractComponent {
|
|
19
25
|
getBBox(): Rect2;
|
20
26
|
abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
21
27
|
abstract intersects(lineSegment: LineSegment2): boolean;
|
28
|
+
protected abstract serializeToString(): string | null;
|
22
29
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
23
30
|
transformBy(affineTransfm: Mat33): Command;
|
31
|
+
private static TransformElementCommand;
|
24
32
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
33
|
+
protected abstract createClone(): AbstractComponent;
|
34
|
+
clone(): AbstractComponent;
|
35
|
+
serialize(): string;
|
36
|
+
private static isNotDeserializable;
|
37
|
+
static deserialize(data: string): AbstractComponent;
|
25
38
|
}
|
26
39
|
export {};
|
@@ -1,10 +1,30 @@
|
|
1
|
+
var _a;
|
2
|
+
import SerializableCommand from '../commands/SerializableCommand';
|
1
3
|
import EditorImage from '../EditorImage';
|
4
|
+
import Mat33 from '../geometry/Mat33';
|
2
5
|
export default class AbstractComponent {
|
3
|
-
constructor(
|
6
|
+
constructor(
|
7
|
+
// A unique identifier for the type of component
|
8
|
+
componentKind) {
|
9
|
+
this.componentKind = componentKind;
|
4
10
|
// Get and manage data attached by a loader.
|
5
11
|
this.loadSaveData = {};
|
6
12
|
this.lastChangedTime = (new Date()).getTime();
|
7
13
|
this.zIndex = AbstractComponent.zIndexCounter++;
|
14
|
+
// Create a unique ID.
|
15
|
+
this.id = `${new Date().getTime()}-${Math.random()}`;
|
16
|
+
if (AbstractComponent.deserializationCallbacks[componentKind] === undefined) {
|
17
|
+
throw new Error(`Component ${componentKind} has not been registered using AbstractComponent.registerComponent`);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
getId() {
|
21
|
+
return this.id;
|
22
|
+
}
|
23
|
+
// Store the deserialization callback (or lack of it) for [componentKind].
|
24
|
+
// If components are registered multiple times (as may be done in automated tests),
|
25
|
+
// the most recent deserialization callback is used.
|
26
|
+
static registerComponent(componentKind, deserialize) {
|
27
|
+
this.deserializationCallbacks[componentKind] = deserialize !== null && deserialize !== void 0 ? deserialize : null;
|
8
28
|
}
|
9
29
|
attachLoadSaveData(key, data) {
|
10
30
|
if (!this.loadSaveData[key]) {
|
@@ -24,37 +44,113 @@ export default class AbstractComponent {
|
|
24
44
|
// Returns a command that, when applied, transforms this by [affineTransfm] and
|
25
45
|
// updates the editor.
|
26
46
|
transformBy(affineTransfm) {
|
27
|
-
|
47
|
+
return new AbstractComponent.TransformElementCommand(affineTransfm, this);
|
48
|
+
}
|
49
|
+
clone() {
|
50
|
+
const clone = this.createClone();
|
51
|
+
for (const attachmentKey in this.loadSaveData) {
|
52
|
+
for (const val of this.loadSaveData[attachmentKey]) {
|
53
|
+
clone.attachLoadSaveData(attachmentKey, val);
|
54
|
+
}
|
55
|
+
}
|
56
|
+
return clone;
|
57
|
+
}
|
58
|
+
serialize() {
|
59
|
+
const data = this.serializeToString();
|
60
|
+
if (data === null) {
|
61
|
+
throw new Error(`${this} cannot be serialized.`);
|
62
|
+
}
|
63
|
+
return JSON.stringify({
|
64
|
+
name: this.componentKind,
|
65
|
+
zIndex: this.zIndex,
|
66
|
+
id: this.id,
|
67
|
+
loadSaveData: this.loadSaveData,
|
68
|
+
data,
|
69
|
+
});
|
70
|
+
}
|
71
|
+
// Returns true if [data] is not deserializable. May return false even if [data]
|
72
|
+
// is not deserializable.
|
73
|
+
static isNotDeserializable(data) {
|
74
|
+
const json = JSON.parse(data);
|
75
|
+
if (typeof json !== 'object') {
|
76
|
+
return true;
|
77
|
+
}
|
78
|
+
if (!this.deserializationCallbacks[json === null || json === void 0 ? void 0 : json.name]) {
|
79
|
+
return true;
|
80
|
+
}
|
81
|
+
if (!json.data) {
|
82
|
+
return true;
|
83
|
+
}
|
84
|
+
return false;
|
85
|
+
}
|
86
|
+
static deserialize(data) {
|
87
|
+
if (AbstractComponent.isNotDeserializable(data)) {
|
88
|
+
throw new Error(`Element with data ${data} cannot be deserialized.`);
|
89
|
+
}
|
90
|
+
const json = JSON.parse(data);
|
91
|
+
const instance = this.deserializationCallbacks[json.name](json.data);
|
92
|
+
instance.zIndex = json.zIndex;
|
93
|
+
instance.id = json.id;
|
94
|
+
// TODO: What should we do with json.loadSaveData?
|
95
|
+
// If we attach it to [instance], we create a potential security risk — loadSaveData
|
96
|
+
// is often used to store unrecognised attributes so they can be preserved on output.
|
97
|
+
// ...but what if we're deserializing data sent across the network?
|
98
|
+
return instance;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
// Topmost z-index
|
102
|
+
AbstractComponent.zIndexCounter = 0;
|
103
|
+
AbstractComponent.deserializationCallbacks = {};
|
104
|
+
AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
|
105
|
+
constructor(affineTransfm, component) {
|
106
|
+
super('transform-element');
|
107
|
+
this.affineTransfm = affineTransfm;
|
108
|
+
this.component = component;
|
109
|
+
this.origZIndex = component.zIndex;
|
110
|
+
}
|
111
|
+
updateTransform(editor, newTransfm) {
|
28
112
|
// Any parent should have only one direct child.
|
29
|
-
const parent = editor.image.findParent(this);
|
113
|
+
const parent = editor.image.findParent(this.component);
|
30
114
|
let hadParent = false;
|
31
115
|
if (parent) {
|
32
116
|
parent.remove();
|
33
117
|
hadParent = true;
|
34
118
|
}
|
35
|
-
this.applyTransformation(newTransfm);
|
119
|
+
this.component.applyTransformation(newTransfm);
|
36
120
|
// Add the element back to the document.
|
37
121
|
if (hadParent) {
|
38
|
-
|
122
|
+
EditorImage.addElement(this.component).apply(editor);
|
39
123
|
}
|
40
|
-
}
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
}
|
59
|
-
|
60
|
-
|
124
|
+
}
|
125
|
+
apply(editor) {
|
126
|
+
this.component.zIndex = AbstractComponent.zIndexCounter++;
|
127
|
+
this.updateTransform(editor, this.affineTransfm);
|
128
|
+
editor.queueRerender();
|
129
|
+
}
|
130
|
+
unapply(editor) {
|
131
|
+
this.component.zIndex = this.origZIndex;
|
132
|
+
this.updateTransform(editor, this.affineTransfm.inverse());
|
133
|
+
editor.queueRerender();
|
134
|
+
}
|
135
|
+
description(localizationTable) {
|
136
|
+
return localizationTable.transformedElements(1);
|
137
|
+
}
|
138
|
+
serializeToString() {
|
139
|
+
return JSON.stringify({
|
140
|
+
id: this.component.getId(),
|
141
|
+
transfm: this.affineTransfm.toArray(),
|
142
|
+
});
|
143
|
+
}
|
144
|
+
},
|
145
|
+
(() => {
|
146
|
+
SerializableCommand.register('transform-element', (data, editor) => {
|
147
|
+
const json = JSON.parse(data);
|
148
|
+
const elem = editor.image.lookupElement(json.id);
|
149
|
+
if (!elem) {
|
150
|
+
throw new Error(`Unable to retrieve non-existent element, ${elem}`);
|
151
|
+
}
|
152
|
+
const transform = json.transfm;
|
153
|
+
return new AbstractComponent.TransformElementCommand(new Mat33(...transform), elem);
|
154
|
+
});
|
155
|
+
})(),
|
156
|
+
_a);
|
@@ -4,12 +4,17 @@ import Rect2 from '../geometry/Rect2';
|
|
4
4
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import AbstractComponent from './AbstractComponent';
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
|
+
declare type GlobalAttrsList = Array<[string, string | null]>;
|
7
8
|
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
8
9
|
private readonly attrs;
|
9
10
|
protected contentBBox: Rect2;
|
10
|
-
constructor(attrs:
|
11
|
+
constructor(attrs: GlobalAttrsList);
|
11
12
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
12
13
|
intersects(_lineSegment: LineSegment2): boolean;
|
13
14
|
protected applyTransformation(_affineTransfm: Mat33): void;
|
15
|
+
protected createClone(): SVGGlobalAttributesObject;
|
14
16
|
description(localization: ImageComponentLocalization): string;
|
17
|
+
protected serializeToString(): string | null;
|
18
|
+
static deserializeFromString(data: string): AbstractComponent;
|
15
19
|
}
|
20
|
+
export {};
|
@@ -1,10 +1,11 @@
|
|
1
1
|
import Rect2 from '../geometry/Rect2';
|
2
2
|
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
3
3
|
import AbstractComponent from './AbstractComponent';
|
4
|
+
const componentKind = 'svg-global-attributes';
|
4
5
|
// Stores global SVG attributes (e.g. namespace identifiers.)
|
5
6
|
export default class SVGGlobalAttributesObject extends AbstractComponent {
|
6
7
|
constructor(attrs) {
|
7
|
-
super();
|
8
|
+
super(componentKind);
|
8
9
|
this.attrs = attrs;
|
9
10
|
this.contentBBox = Rect2.empty;
|
10
11
|
}
|
@@ -22,7 +23,28 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
|
|
22
23
|
}
|
23
24
|
applyTransformation(_affineTransfm) {
|
24
25
|
}
|
26
|
+
createClone() {
|
27
|
+
return new SVGGlobalAttributesObject(this.attrs);
|
28
|
+
}
|
25
29
|
description(localization) {
|
26
30
|
return localization.svgObject;
|
27
31
|
}
|
32
|
+
serializeToString() {
|
33
|
+
return JSON.stringify(this.attrs);
|
34
|
+
}
|
35
|
+
static deserializeFromString(data) {
|
36
|
+
const json = JSON.parse(data);
|
37
|
+
const attrs = [];
|
38
|
+
const numericAndSpaceContentExp = /^[ \t\n0-9.-eE]+$/;
|
39
|
+
// Don't deserialize all attributes, just those that should be safe.
|
40
|
+
for (const [key, val] of json) {
|
41
|
+
if (key === 'viewBox' || key === 'width' || key === 'height') {
|
42
|
+
if (val && numericAndSpaceContentExp.exec(val)) {
|
43
|
+
attrs.push([key, val]);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
return new SVGGlobalAttributesObject(attrs);
|
48
|
+
}
|
28
49
|
}
|
50
|
+
AbstractComponent.registerComponent(componentKind, SVGGlobalAttributesObject.deserializeFromString);
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import LineSegment2 from '../geometry/LineSegment2';
|
2
2
|
import Mat33 from '../geometry/Mat33';
|
3
|
+
import Path from '../geometry/Path';
|
3
4
|
import Rect2 from '../geometry/Rect2';
|
4
5
|
import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
|
5
6
|
import AbstractComponent from './AbstractComponent';
|
@@ -12,5 +13,9 @@ export default class Stroke extends AbstractComponent {
|
|
12
13
|
render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
|
13
14
|
private bboxForPart;
|
14
15
|
protected applyTransformation(affineTransfm: Mat33): void;
|
16
|
+
getPath(): Path;
|
15
17
|
description(localization: ImageComponentLocalization): string;
|
18
|
+
protected createClone(): AbstractComponent;
|
19
|
+
protected serializeToString(): string | null;
|
20
|
+
static deserializeFromString(data: string): Stroke;
|
16
21
|
}
|
@@ -1,10 +1,11 @@
|
|
1
1
|
import Path from '../geometry/Path';
|
2
2
|
import Rect2 from '../geometry/Rect2';
|
3
|
+
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
3
4
|
import AbstractComponent from './AbstractComponent';
|
4
5
|
export default class Stroke extends AbstractComponent {
|
5
6
|
constructor(parts) {
|
6
7
|
var _a;
|
7
|
-
super();
|
8
|
+
super('stroke');
|
8
9
|
this.parts = parts.map(section => {
|
9
10
|
const path = Path.fromRenderable(section);
|
10
11
|
const pathBBox = this.bboxForPart(path.bbox, section.style);
|
@@ -73,7 +74,37 @@ export default class Stroke extends AbstractComponent {
|
|
73
74
|
};
|
74
75
|
});
|
75
76
|
}
|
77
|
+
getPath() {
|
78
|
+
var _a;
|
79
|
+
return (_a = this.parts.reduce((accumulator, current) => {
|
80
|
+
var _a;
|
81
|
+
return (_a = accumulator === null || accumulator === void 0 ? void 0 : accumulator.union(current.path)) !== null && _a !== void 0 ? _a : current.path;
|
82
|
+
}, null)) !== null && _a !== void 0 ? _a : Path.empty;
|
83
|
+
}
|
76
84
|
description(localization) {
|
77
85
|
return localization.stroke;
|
78
86
|
}
|
87
|
+
createClone() {
|
88
|
+
return new Stroke(this.parts);
|
89
|
+
}
|
90
|
+
serializeToString() {
|
91
|
+
return JSON.stringify(this.parts.map(part => {
|
92
|
+
return {
|
93
|
+
style: styleToJSON(part.style),
|
94
|
+
path: part.path.serialize(),
|
95
|
+
};
|
96
|
+
}));
|
97
|
+
}
|
98
|
+
static deserializeFromString(data) {
|
99
|
+
const json = JSON.parse(data);
|
100
|
+
if (typeof json !== 'object' || typeof json.length !== 'number') {
|
101
|
+
throw new Error(`${data} is missing required field, parts, or parts is of the wrong type.`);
|
102
|
+
}
|
103
|
+
const pathSpec = json.map((part) => {
|
104
|
+
const style = styleFromJSON(part.style);
|
105
|
+
return Path.fromString(part.path).toRenderable(style);
|
106
|
+
});
|
107
|
+
return new Stroke(pathSpec);
|
108
|
+
}
|
79
109
|
}
|
110
|
+
AbstractComponent.registerComponent('stroke', Stroke.deserializeFromString);
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import LineSegment2 from '../geometry/LineSegment2';
|
2
2
|
import Mat33 from '../geometry/Mat33';
|
3
3
|
import Rect2 from '../geometry/Rect2';
|
4
|
-
import AbstractRenderer
|
4
|
+
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
|
+
import RenderingStyle from '../rendering/RenderingStyle';
|
5
6
|
import AbstractComponent from './AbstractComponent';
|
6
7
|
import { ImageComponentLocalization } from './localization';
|
7
8
|
export interface TextStyle {
|
@@ -11,12 +12,14 @@ export interface TextStyle {
|
|
11
12
|
fontVariant?: string;
|
12
13
|
renderingStyle: RenderingStyle;
|
13
14
|
}
|
15
|
+
declare type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
|
14
16
|
export default class Text extends AbstractComponent {
|
15
|
-
protected textObjects: Array<string | Text>;
|
17
|
+
protected readonly textObjects: Array<string | Text>;
|
16
18
|
private transform;
|
17
|
-
private style;
|
19
|
+
private readonly style;
|
20
|
+
private readonly getTextDimens;
|
18
21
|
protected contentBBox: Rect2;
|
19
|
-
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
|
22
|
+
constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle, getTextDimens?: GetTextDimensCallback);
|
20
23
|
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
|
21
24
|
private static textMeasuringCtx;
|
22
25
|
private static getTextDimens;
|
@@ -25,6 +28,10 @@ export default class Text extends AbstractComponent {
|
|
25
28
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
26
29
|
intersects(lineSegment: LineSegment2): boolean;
|
27
30
|
protected applyTransformation(affineTransfm: Mat33): void;
|
31
|
+
protected createClone(): AbstractComponent;
|
28
32
|
private getText;
|
29
33
|
description(localizationTable: ImageComponentLocalization): string;
|
34
|
+
protected serializeToString(): string;
|
35
|
+
static deserializeFromString(data: string, getTextDimens?: GetTextDimensCallback): Text;
|
30
36
|
}
|
37
|
+
export {};
|
@@ -1,12 +1,18 @@
|
|
1
1
|
import LineSegment2 from '../geometry/LineSegment2';
|
2
|
+
import Mat33 from '../geometry/Mat33';
|
2
3
|
import Rect2 from '../geometry/Rect2';
|
4
|
+
import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
3
5
|
import AbstractComponent from './AbstractComponent';
|
6
|
+
const componentTypeId = 'text';
|
4
7
|
export default class Text extends AbstractComponent {
|
5
|
-
constructor(textObjects, transform, style
|
6
|
-
|
8
|
+
constructor(textObjects, transform, style,
|
9
|
+
// If not given, an HtmlCanvasElement is used to determine text boundaries.
|
10
|
+
getTextDimens = Text.getTextDimens) {
|
11
|
+
super(componentTypeId);
|
7
12
|
this.textObjects = textObjects;
|
8
13
|
this.transform = transform;
|
9
14
|
this.style = style;
|
15
|
+
this.getTextDimens = getTextDimens;
|
10
16
|
this.recomputeBBox();
|
11
17
|
}
|
12
18
|
static applyTextStyles(ctx, style) {
|
@@ -34,7 +40,7 @@ export default class Text extends AbstractComponent {
|
|
34
40
|
}
|
35
41
|
computeBBoxOfPart(part) {
|
36
42
|
if (typeof part === 'string') {
|
37
|
-
const textBBox =
|
43
|
+
const textBBox = this.getTextDimens(part, this.style);
|
38
44
|
return textBBox.transformedBoundingBox(this.transform);
|
39
45
|
}
|
40
46
|
else {
|
@@ -93,6 +99,9 @@ export default class Text extends AbstractComponent {
|
|
93
99
|
this.transform = affineTransfm.rightMul(this.transform);
|
94
100
|
this.recomputeBBox();
|
95
101
|
}
|
102
|
+
createClone() {
|
103
|
+
return new Text(this.textObjects, this.transform, this.style);
|
104
|
+
}
|
96
105
|
getText() {
|
97
106
|
const result = [];
|
98
107
|
for (const textObject of this.textObjects) {
|
@@ -108,4 +117,49 @@ export default class Text extends AbstractComponent {
|
|
108
117
|
description(localizationTable) {
|
109
118
|
return localizationTable.text(this.getText());
|
110
119
|
}
|
120
|
+
serializeToString() {
|
121
|
+
const serializableStyle = Object.assign(Object.assign({}, this.style), { renderingStyle: styleToJSON(this.style.renderingStyle) });
|
122
|
+
const textObjects = this.textObjects.map(text => {
|
123
|
+
if (typeof text === 'string') {
|
124
|
+
return {
|
125
|
+
text,
|
126
|
+
};
|
127
|
+
}
|
128
|
+
else {
|
129
|
+
return {
|
130
|
+
json: text.serializeToString(),
|
131
|
+
};
|
132
|
+
}
|
133
|
+
});
|
134
|
+
return JSON.stringify({
|
135
|
+
textObjects,
|
136
|
+
transform: this.transform.toArray(),
|
137
|
+
style: serializableStyle,
|
138
|
+
});
|
139
|
+
}
|
140
|
+
static deserializeFromString(data, getTextDimens = Text.getTextDimens) {
|
141
|
+
const json = JSON.parse(data);
|
142
|
+
const style = {
|
143
|
+
renderingStyle: styleFromJSON(json.style.renderingStyle),
|
144
|
+
size: json.style.size,
|
145
|
+
fontWeight: json.style.fontWeight,
|
146
|
+
fontVariant: json.style.fontVariant,
|
147
|
+
fontFamily: json.style.fontFamily,
|
148
|
+
};
|
149
|
+
const textObjects = json.textObjects.map((data) => {
|
150
|
+
var _a;
|
151
|
+
if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {
|
152
|
+
return data.text;
|
153
|
+
}
|
154
|
+
return Text.deserializeFromString(data.json);
|
155
|
+
});
|
156
|
+
json.transform = json.transform.filter((elem) => typeof elem === 'number');
|
157
|
+
if (json.transform.length !== 9) {
|
158
|
+
throw new Error(`Unable to deserialize transform, ${json.transform}.`);
|
159
|
+
}
|
160
|
+
const transformData = json.transform;
|
161
|
+
const transform = new Mat33(...transformData);
|
162
|
+
return new Text(textObjects, transform, style, getTextDimens);
|
163
|
+
}
|
111
164
|
}
|
165
|
+
AbstractComponent.registerComponent(componentTypeId, (data) => Text.deserializeFromString(data));
|
@@ -11,5 +11,7 @@ export default class UnknownSVGObject extends AbstractComponent {
|
|
11
11
|
render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
|
12
12
|
intersects(lineSegment: LineSegment2): boolean;
|
13
13
|
protected applyTransformation(_affineTransfm: Mat33): void;
|
14
|
+
protected createClone(): AbstractComponent;
|
14
15
|
description(localization: ImageComponentLocalization): string;
|
16
|
+
protected serializeToString(): string | null;
|
15
17
|
}
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import Rect2 from '../geometry/Rect2';
|
2
2
|
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
3
3
|
import AbstractComponent from './AbstractComponent';
|
4
|
+
const componentId = 'unknown-svg-object';
|
4
5
|
export default class UnknownSVGObject extends AbstractComponent {
|
5
6
|
constructor(svgObject) {
|
6
|
-
super();
|
7
|
+
super(componentId);
|
7
8
|
this.svgObject = svgObject;
|
8
9
|
this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
|
9
10
|
}
|
@@ -19,7 +20,17 @@ export default class UnknownSVGObject extends AbstractComponent {
|
|
19
20
|
}
|
20
21
|
applyTransformation(_affineTransfm) {
|
21
22
|
}
|
23
|
+
createClone() {
|
24
|
+
return new UnknownSVGObject(this.svgObject.cloneNode(true));
|
25
|
+
}
|
22
26
|
description(localization) {
|
23
27
|
return localization.svgObject;
|
24
28
|
}
|
29
|
+
serializeToString() {
|
30
|
+
return JSON.stringify({
|
31
|
+
html: this.svgObject.outerHTML,
|
32
|
+
});
|
33
|
+
}
|
25
34
|
}
|
35
|
+
// null: Do not deserialize UnknownSVGObjects.
|
36
|
+
AbstractComponent.registerComponent(componentId, null);
|
@@ -0,0 +1,14 @@
|
|
1
|
+
// Returns the description of all given elements, if identical, otherwise,
|
2
|
+
// returns null.
|
3
|
+
export default (localizationTable, elems) => {
|
4
|
+
if (elems.length === 0) {
|
5
|
+
return null;
|
6
|
+
}
|
7
|
+
const description = elems[0].description(localizationTable);
|
8
|
+
for (const elem of elems) {
|
9
|
+
if (elem.description(localizationTable) !== description) {
|
10
|
+
return null;
|
11
|
+
}
|
12
|
+
}
|
13
|
+
return description;
|
14
|
+
};
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Bezier } from 'bezier-js';
|
2
|
-
import {
|
2
|
+
import { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
|
3
|
+
import RenderingStyle from '../rendering/RenderingStyle';
|
3
4
|
import LineSegment2 from './LineSegment2';
|
4
5
|
import Mat33 from './Mat33';
|
5
6
|
import Rect2 from './Rect2';
|
@@ -50,7 +51,9 @@ export default class Path {
|
|
50
51
|
static fromRenderable(renderable: RenderablePathSpec): Path;
|
51
52
|
toRenderable(fill: RenderingStyle): RenderablePathSpec;
|
52
53
|
toString(): string;
|
54
|
+
serialize(): string;
|
53
55
|
static toString(startPoint: Point2, parts: PathCommand[]): string;
|
54
56
|
static fromString(pathString: string): Path;
|
57
|
+
static empty: Path;
|
55
58
|
}
|
56
59
|
export {};
|
@@ -203,6 +203,9 @@ export default class Path {
|
|
203
203
|
toString() {
|
204
204
|
return Path.toString(this.startPoint, this.parts);
|
205
205
|
}
|
206
|
+
serialize() {
|
207
|
+
return this.toString();
|
208
|
+
}
|
206
209
|
static toString(startPoint, parts) {
|
207
210
|
const result = [];
|
208
211
|
const toRoundedString = (num) => {
|
@@ -445,3 +448,4 @@ export default class Path {
|
|
445
448
|
return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
|
446
449
|
}
|
447
450
|
}
|
451
|
+
Path.empty = new Path(Vec2.zero, []);
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import { CommandLocalization } from './commands/localization';
|
2
2
|
import { ImageComponentLocalization } from './components/localization';
|
3
|
+
import { TextRendererLocalization } from './rendering/localization';
|
3
4
|
import { ToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { ToolLocalization } from './tools/localization';
|
5
|
-
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
|
6
|
+
export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
|
6
7
|
undoAnnouncement: (actionDescription: string) => string;
|
7
8
|
redoAnnouncement: (actionDescription: string) => string;
|
8
9
|
doneLoading: string;
|
package/dist/src/localization.js
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import { defaultCommandLocalization } from './commands/localization';
|
2
2
|
import { defaultComponentLocalization } from './components/localization';
|
3
|
+
import { defaultTextRendererLocalization } from './rendering/localization';
|
3
4
|
import { defaultToolbarLocalization } from './toolbar/localization';
|
4
5
|
import { defaultToolLocalization } from './tools/localization';
|
5
|
-
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|
6
|
+
export const defaultEditorLocalization = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultToolbarLocalization), defaultToolLocalization), defaultCommandLocalization), defaultComponentLocalization), defaultTextRendererLocalization), { loading: (percentage) => `Loading ${percentage}%...`, imageEditor: 'Image Editor', doneLoading: 'Done loading', undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`, redoAnnouncement: (commandDescription) => `Redid ${commandDescription}` });
|