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.
Files changed (143) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +2 -2
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Color4.js +6 -2
  6. package/dist/src/Editor.d.ts +1 -0
  7. package/dist/src/Editor.js +20 -9
  8. package/dist/src/EditorImage.d.ts +8 -13
  9. package/dist/src/EditorImage.js +51 -29
  10. package/dist/src/SVGLoader.js +6 -2
  11. package/dist/src/Viewport.d.ts +10 -2
  12. package/dist/src/Viewport.js +8 -6
  13. package/dist/src/commands/Command.d.ts +9 -8
  14. package/dist/src/commands/Command.js +15 -14
  15. package/dist/src/commands/Duplicate.d.ts +14 -0
  16. package/dist/src/commands/Duplicate.js +34 -0
  17. package/dist/src/commands/Erase.d.ts +5 -2
  18. package/dist/src/commands/Erase.js +28 -9
  19. package/dist/src/commands/SerializableCommand.d.ts +13 -0
  20. package/dist/src/commands/SerializableCommand.js +28 -0
  21. package/dist/src/commands/localization.d.ts +2 -0
  22. package/dist/src/commands/localization.js +2 -0
  23. package/dist/src/components/AbstractComponent.d.ts +15 -2
  24. package/dist/src/components/AbstractComponent.js +122 -26
  25. package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
  26. package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
  27. package/dist/src/components/Stroke.d.ts +5 -0
  28. package/dist/src/components/Stroke.js +32 -1
  29. package/dist/src/components/Text.d.ts +11 -4
  30. package/dist/src/components/Text.js +57 -3
  31. package/dist/src/components/UnknownSVGObject.d.ts +2 -0
  32. package/dist/src/components/UnknownSVGObject.js +12 -1
  33. package/dist/src/components/util/describeComponentList.d.ts +4 -0
  34. package/dist/src/components/util/describeComponentList.js +14 -0
  35. package/dist/src/geometry/Path.d.ts +4 -1
  36. package/dist/src/geometry/Path.js +4 -0
  37. package/dist/src/localization.d.ts +2 -1
  38. package/dist/src/localization.js +2 -1
  39. package/dist/src/rendering/Display.d.ts +5 -0
  40. package/dist/src/rendering/Display.js +32 -0
  41. package/dist/src/rendering/RenderingStyle.d.ts +24 -0
  42. package/dist/src/rendering/RenderingStyle.js +32 -0
  43. package/dist/src/rendering/localization.d.ts +5 -0
  44. package/dist/src/rendering/localization.js +4 -0
  45. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
  46. package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
  47. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  48. package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
  49. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
  50. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +25 -0
  51. package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
  52. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -1
  53. package/dist/src/toolbar/HTMLToolbar.js +52 -534
  54. package/dist/src/toolbar/icons.d.ts +5 -0
  55. package/dist/src/toolbar/icons.js +186 -13
  56. package/dist/src/toolbar/localization.d.ts +4 -0
  57. package/dist/src/toolbar/localization.js +4 -0
  58. package/dist/src/toolbar/makeColorInput.d.ts +5 -0
  59. package/dist/src/toolbar/makeColorInput.js +81 -0
  60. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
  61. package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
  62. package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
  63. package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
  64. package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
  65. package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
  66. package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
  67. package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
  68. package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
  69. package/dist/src/toolbar/widgets/PenWidget.js +131 -0
  70. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
  71. package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
  72. package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
  73. package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
  74. package/dist/src/tools/Pen.js +1 -1
  75. package/dist/src/tools/PipetteTool.d.ts +20 -0
  76. package/dist/src/tools/PipetteTool.js +40 -0
  77. package/dist/src/tools/SelectionTool.d.ts +2 -0
  78. package/dist/src/tools/SelectionTool.js +41 -23
  79. package/dist/src/tools/TextTool.d.ts +1 -0
  80. package/dist/src/tools/TextTool.js +5 -4
  81. package/dist/src/tools/ToolController.d.ts +3 -1
  82. package/dist/src/tools/ToolController.js +4 -0
  83. package/dist/src/tools/localization.d.ts +2 -1
  84. package/dist/src/tools/localization.js +3 -2
  85. package/dist/src/types.d.ts +7 -2
  86. package/dist/src/types.js +1 -0
  87. package/jest.config.js +2 -0
  88. package/package.json +6 -6
  89. package/src/Color4.ts +9 -3
  90. package/src/Editor.css +10 -0
  91. package/src/Editor.ts +24 -12
  92. package/src/EditorImage.test.ts +4 -4
  93. package/src/EditorImage.ts +61 -20
  94. package/src/SVGLoader.ts +9 -3
  95. package/src/Viewport.ts +7 -6
  96. package/src/commands/Command.ts +21 -19
  97. package/src/commands/Duplicate.ts +49 -0
  98. package/src/commands/Erase.ts +34 -13
  99. package/src/commands/SerializableCommand.ts +41 -0
  100. package/src/commands/localization.ts +5 -0
  101. package/src/components/AbstractComponent.ts +168 -26
  102. package/src/components/SVGGlobalAttributesObject.ts +34 -2
  103. package/src/components/Stroke.test.ts +53 -0
  104. package/src/components/Stroke.ts +37 -2
  105. package/src/components/Text.test.ts +38 -0
  106. package/src/components/Text.ts +80 -5
  107. package/src/components/UnknownSVGObject.test.ts +10 -0
  108. package/src/components/UnknownSVGObject.ts +15 -1
  109. package/src/components/builders/FreehandLineBuilder.ts +2 -1
  110. package/src/components/util/describeComponentList.ts +18 -0
  111. package/src/geometry/Path.ts +8 -1
  112. package/src/localization.ts +3 -1
  113. package/src/rendering/Display.ts +43 -1
  114. package/src/rendering/RenderingStyle.test.ts +68 -0
  115. package/src/rendering/RenderingStyle.ts +46 -0
  116. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  117. package/src/rendering/localization.ts +10 -0
  118. package/src/rendering/renderers/AbstractRenderer.ts +1 -15
  119. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  120. package/src/rendering/renderers/DummyRenderer.ts +2 -1
  121. package/src/rendering/renderers/SVGRenderer.ts +2 -1
  122. package/src/rendering/renderers/TextOnlyRenderer.ts +52 -0
  123. package/src/toolbar/HTMLToolbar.ts +58 -660
  124. package/src/toolbar/icons.ts +205 -13
  125. package/src/toolbar/localization.ts +10 -2
  126. package/src/toolbar/makeColorInput.ts +105 -0
  127. package/src/toolbar/toolbar.css +116 -78
  128. package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
  129. package/src/toolbar/widgets/BaseWidget.ts +175 -0
  130. package/src/toolbar/widgets/EraserWidget.ts +16 -0
  131. package/src/toolbar/widgets/HandToolWidget.ts +186 -0
  132. package/src/toolbar/widgets/PenWidget.ts +165 -0
  133. package/src/toolbar/widgets/SelectionWidget.ts +72 -0
  134. package/src/toolbar/widgets/TextToolWidget.ts +90 -0
  135. package/src/tools/Pen.ts +1 -1
  136. package/src/tools/PipetteTool.ts +56 -0
  137. package/src/tools/SelectionTool.test.ts +2 -4
  138. package/src/tools/SelectionTool.ts +47 -27
  139. package/src/tools/TextTool.ts +7 -3
  140. package/src/tools/ToolController.ts +10 -6
  141. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  142. package/src/tools/localization.ts +6 -3
  143. package/src/types.ts +12 -1
@@ -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, { RenderingStyle } from '../rendering/renderers/AbstractRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
5
6
  import AbstractComponent from './AbstractComponent';
6
7
  import { ImageComponentLocalization } from './localization';
7
8
 
@@ -13,12 +14,21 @@ export interface TextStyle {
13
14
  renderingStyle: RenderingStyle;
14
15
  }
15
16
 
17
+ type GetTextDimensCallback = (text: string, style: TextStyle) => Rect2;
16
18
 
19
+ const componentTypeId = 'text';
17
20
  export default class Text extends AbstractComponent {
18
21
  protected contentBBox: Rect2;
19
22
 
20
- public constructor(protected textObjects: Array<string|Text>, private transform: Mat33, private style: TextStyle) {
21
- super();
23
+ public constructor(
24
+ protected readonly textObjects: Array<string|Text>,
25
+ private transform: Mat33,
26
+ private readonly style: TextStyle,
27
+
28
+ // If not given, an HtmlCanvasElement is used to determine text boundaries.
29
+ private readonly getTextDimens: GetTextDimensCallback = Text.getTextDimens,
30
+ ) {
31
+ super(componentTypeId);
22
32
  this.recomputeBBox();
23
33
  }
24
34
 
@@ -52,7 +62,7 @@ export default class Text extends AbstractComponent {
52
62
 
53
63
  private computeBBoxOfPart(part: string|Text) {
54
64
  if (typeof part === 'string') {
55
- const textBBox = Text.getTextDimens(part, this.style);
65
+ const textBBox = this.getTextDimens(part, this.style);
56
66
  return textBBox.transformedBoundingBox(this.transform);
57
67
  } else {
58
68
  const bbox = part.contentBBox.transformedBoundingBox(this.transform);
@@ -120,6 +130,10 @@ export default class Text extends AbstractComponent {
120
130
  this.recomputeBBox();
121
131
  }
122
132
 
133
+ protected createClone(): AbstractComponent {
134
+ return new Text(this.textObjects, this.transform, this.style);
135
+ }
136
+
123
137
  private getText() {
124
138
  const result: string[] = [];
125
139
 
@@ -137,4 +151,65 @@ export default class Text extends AbstractComponent {
137
151
  public description(localizationTable: ImageComponentLocalization): string {
138
152
  return localizationTable.text(this.getText());
139
153
  }
140
- }
154
+
155
+ protected serializeToString(): string {
156
+ const serializableStyle = {
157
+ ...this.style,
158
+ renderingStyle: styleToJSON(this.style.renderingStyle),
159
+ };
160
+
161
+ const textObjects = this.textObjects.map(text => {
162
+ if (typeof text === 'string') {
163
+ return {
164
+ text,
165
+ };
166
+ } else {
167
+ return {
168
+ json: text.serializeToString(),
169
+ };
170
+ }
171
+ });
172
+
173
+ return JSON.stringify({
174
+ textObjects,
175
+ transform: this.transform.toArray(),
176
+ style: serializableStyle,
177
+ });
178
+ }
179
+
180
+ public static deserializeFromString(data: string, getTextDimens: GetTextDimensCallback = Text.getTextDimens): Text {
181
+ const json = JSON.parse(data);
182
+
183
+ const style: TextStyle = {
184
+ renderingStyle: styleFromJSON(json.style.renderingStyle),
185
+ size: json.style.size,
186
+ fontWeight: json.style.fontWeight,
187
+ fontVariant: json.style.fontVariant,
188
+ fontFamily: json.style.fontFamily,
189
+ };
190
+
191
+ const textObjects: Array<string|Text> = json.textObjects.map((data: any) => {
192
+ if ((data.text ?? null) !== null) {
193
+ return data.text;
194
+ }
195
+
196
+ return Text.deserializeFromString(data.json);
197
+ });
198
+
199
+ json.transform = json.transform.filter((elem: any) => typeof elem === 'number');
200
+ if (json.transform.length !== 9) {
201
+ throw new Error(`Unable to deserialize transform, ${json.transform}.`);
202
+ }
203
+
204
+ const transformData = json.transform as [
205
+ number, number, number,
206
+ number, number, number,
207
+ number, number, number,
208
+ ];
209
+ const transform = new Mat33(...transformData);
210
+
211
+ return new Text(textObjects, transform, style, getTextDimens);
212
+ }
213
+ }
214
+
215
+ AbstractComponent.registerComponent(componentTypeId, (data: string) => Text.deserializeFromString(data));
@@ -0,0 +1,10 @@
1
+ import AbstractComponent from './AbstractComponent';
2
+ import UnknownSVGObject from './UnknownSVGObject';
3
+
4
+ describe('UnknownSVGObject', () => {
5
+ it('should not be deserializable', () => {
6
+ const obj = new UnknownSVGObject(document.createElementNS('http://www.w3.org/2000/svg', 'circle'));
7
+ const serialized = obj.serialize();
8
+ expect(() => AbstractComponent.deserialize(serialized)).toThrow(/.*cannot be deserialized.*/);
9
+ });
10
+ });
@@ -6,11 +6,12 @@ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
9
+ const componentId = 'unknown-svg-object';
9
10
  export default class UnknownSVGObject extends AbstractComponent {
10
11
  protected contentBBox: Rect2;
11
12
 
12
13
  public constructor(private svgObject: SVGElement) {
13
- super();
14
+ super(componentId);
14
15
  this.contentBBox = Rect2.of(svgObject.getBoundingClientRect());
15
16
  }
16
17
 
@@ -30,7 +31,20 @@ export default class UnknownSVGObject extends AbstractComponent {
30
31
  protected applyTransformation(_affineTransfm: Mat33): void {
31
32
  }
32
33
 
34
+ protected createClone(): AbstractComponent {
35
+ return new UnknownSVGObject(this.svgObject.cloneNode(true) as SVGElement);
36
+ }
37
+
33
38
  public description(localization: ImageComponentLocalization): string {
34
39
  return localization.svgObject;
35
40
  }
41
+
42
+ protected serializeToString(): string | null {
43
+ return JSON.stringify({
44
+ html: this.svgObject.outerHTML,
45
+ });
46
+ }
36
47
  }
48
+
49
+ // null: Do not deserialize UnknownSVGObjects.
50
+ AbstractComponent.registerComponent(componentId, null);
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
2
+ import AbstractRenderer, { RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
3
3
  import { Point2, Vec2 } from '../../geometry/Vec2';
4
4
  import Rect2 from '../../geometry/Rect2';
5
5
  import { PathCommand, PathCommandType } from '../../geometry/Path';
@@ -8,6 +8,7 @@ import Stroke from '../Stroke';
8
8
  import Viewport from '../../Viewport';
9
9
  import { StrokeDataPoint } from '../../types';
10
10
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
11
+ import RenderingStyle from '../../rendering/RenderingStyle';
11
12
 
12
13
  export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
13
14
  // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
@@ -0,0 +1,18 @@
1
+ import AbstractComponent from '../AbstractComponent';
2
+ import { ImageComponentLocalization } from '../localization';
3
+
4
+ // Returns the description of all given elements, if identical, otherwise,
5
+ // returns null.
6
+ export default (localizationTable: ImageComponentLocalization, elems: AbstractComponent[]) => {
7
+ if (elems.length === 0) {
8
+ return null;
9
+ }
10
+
11
+ const description = elems[0].description(localizationTable);
12
+ for (const elem of elems) {
13
+ if (elem.description(localizationTable) !== description) {
14
+ return null;
15
+ }
16
+ }
17
+ return description;
18
+ };
@@ -1,5 +1,6 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
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';
@@ -282,6 +283,10 @@ export default class Path {
282
283
  return Path.toString(this.startPoint, this.parts);
283
284
  }
284
285
 
286
+ public serialize(): string {
287
+ return this.toString();
288
+ }
289
+
285
290
  public static toString(startPoint: Point2, parts: PathCommand[]): string {
286
291
  const result: string[] = [];
287
292
 
@@ -555,4 +560,6 @@ export default class Path {
555
560
 
556
561
  return new Path(startPos ?? Vec2.zero, commands);
557
562
  }
563
+
564
+ public static empty: Path = new Path(Vec2.zero, []);
558
565
  }
@@ -1,10 +1,11 @@
1
1
  import { CommandLocalization, defaultCommandLocalization } from './commands/localization';
2
2
  import { defaultComponentLocalization, ImageComponentLocalization } from './components/localization';
3
+ import { defaultTextRendererLocalization, TextRendererLocalization } from './rendering/localization';
3
4
  import { defaultToolbarLocalization, ToolbarLocalization } from './toolbar/localization';
4
5
  import { defaultToolLocalization, ToolLocalization } from './tools/localization';
5
6
 
6
7
 
7
- export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization {
8
+ export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
8
9
  undoAnnouncement: (actionDescription: string)=> string;
9
10
  redoAnnouncement: (actionDescription: string)=> string;
10
11
  doneLoading: string;
@@ -17,6 +18,7 @@ export const defaultEditorLocalization: EditorLocalization = {
17
18
  ...defaultToolLocalization,
18
19
  ...defaultCommandLocalization,
19
20
  ...defaultComponentLocalization,
21
+ ...defaultTextRendererLocalization,
20
22
  loading: (percentage: number) => `Loading ${percentage}%...`,
21
23
  imageEditor: 'Image Editor',
22
24
  doneLoading: 'Done loading',
@@ -3,8 +3,10 @@ import CanvasRenderer from './renderers/CanvasRenderer';
3
3
  import { Editor } from '../Editor';
4
4
  import { EditorEventType } from '../types';
5
5
  import DummyRenderer from './renderers/DummyRenderer';
6
- import { Vec2 } from '../geometry/Vec2';
6
+ import { Point2, Vec2 } from '../geometry/Vec2';
7
7
  import RenderingCache from './caching/RenderingCache';
8
+ import TextOnlyRenderer from './renderers/TextOnlyRenderer';
9
+ import Color4 from '../Color4';
8
10
 
9
11
  export enum RenderingMode {
10
12
  DummyRenderer,
@@ -15,6 +17,7 @@ export enum RenderingMode {
15
17
  export default class Display {
16
18
  private dryInkRenderer: AbstractRenderer;
17
19
  private wetInkRenderer: AbstractRenderer;
20
+ private textRenderer: TextOnlyRenderer;
18
21
  private cache: RenderingCache;
19
22
  private resizeSurfacesCallback?: ()=> void;
20
23
  private flattenCallback?: ()=> void;
@@ -31,6 +34,9 @@ export default class Display {
31
34
  throw new Error(`Unknown rendering mode, ${mode}!`);
32
35
  }
33
36
 
37
+ this.textRenderer = new TextOnlyRenderer(editor.viewport, editor.localization);
38
+ this.initializeTextRendering();
39
+
34
40
  const cacheBlockResolution = Vec2.of(600, 600);
35
41
  this.cache = new RenderingCache({
36
42
  createRenderer: () => {
@@ -83,6 +89,10 @@ export default class Display {
83
89
  return this.cache;
84
90
  }
85
91
 
92
+ public getColorAt = (_screenPos: Point2): Color4|null => {
93
+ return null;
94
+ };
95
+
86
96
  private initializeCanvasRendering() {
87
97
  const dryInkCanvas = document.createElement('canvas');
88
98
  const wetInkCanvas = document.createElement('canvas');
@@ -127,6 +137,38 @@ export default class Display {
127
137
  this.flattenCallback = () => {
128
138
  dryInkCtx.drawImage(wetInkCanvas, 0, 0);
129
139
  };
140
+
141
+ this.getColorAt = (screenPos: Point2) => {
142
+ const pixel = dryInkCtx.getImageData(screenPos.x, screenPos.y, 1, 1);
143
+ const data = pixel?.data;
144
+
145
+ if (data) {
146
+ const color = Color4.ofRGBA(data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255);
147
+ return color;
148
+ }
149
+ return null;
150
+ };
151
+ }
152
+
153
+ private initializeTextRendering() {
154
+ const textRendererOutputContainer = document.createElement('div');
155
+ textRendererOutputContainer.classList.add('textRendererOutputContainer');
156
+
157
+ const rerenderButton = document.createElement('button');
158
+ rerenderButton.classList.add('rerenderButton');
159
+ rerenderButton.innerText = this.editor.localization.rerenderAsText;
160
+
161
+ const rerenderOutput = document.createElement('div');
162
+ rerenderOutput.ariaLive = 'polite';
163
+
164
+ rerenderButton.onclick = () => {
165
+ this.textRenderer.clear();
166
+ this.editor.image.render(this.textRenderer, this.editor.viewport);
167
+ rerenderOutput.innerText = this.textRenderer.getDescription();
168
+ };
169
+
170
+ textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
171
+ this.editor.createHTMLOverlay(textRendererOutputContainer);
130
172
  }
131
173
 
132
174
  // Clears the drawing surfaces and otherwise prepares for a rerender.
@@ -0,0 +1,68 @@
1
+
2
+ import Color4 from '../Color4';
3
+ import RenderingStyle, { styleFromJSON, stylesEqual, styleToJSON } from './RenderingStyle';
4
+
5
+
6
+ describe('RenderingStyle', () => {
7
+ it('identical styles should be equal', () => {
8
+ const redFill: RenderingStyle = {
9
+ fill: Color4.red,
10
+ };
11
+ expect(stylesEqual(redFill, redFill)).toBe(true);
12
+ expect(stylesEqual(
13
+ { fill: Color4.ofRGB(1, 0, 0.3), },
14
+ { fill: Color4.ofRGB(1, 0, 0.3), },
15
+ )).toBe(true);
16
+ expect(stylesEqual(
17
+ { fill: Color4.red },
18
+ { fill: Color4.blue },
19
+ )).toBe(false);
20
+
21
+ expect(stylesEqual(
22
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
23
+ { fill: Color4.red },
24
+ )).toBe(false);
25
+ expect(stylesEqual(
26
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
27
+ { fill: Color4.red, stroke: { width: 1, color: Color4.blue }},
28
+ )).toBe(false);
29
+ expect(stylesEqual(
30
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
31
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
32
+ )).toBe(true);
33
+ expect(stylesEqual(
34
+ { fill: Color4.red, stroke: { width: 1, color: Color4.red }},
35
+ { fill: Color4.red, stroke: { width: 2, color: Color4.red }},
36
+ )).toBe(false);
37
+ });
38
+
39
+ it('styles should be convertable to JSON', () => {
40
+ expect(styleToJSON({
41
+ fill: Color4.red,
42
+ })).toMatchObject({
43
+ fill: '#ff0000',
44
+ stroke: undefined,
45
+ });
46
+
47
+ expect(styleToJSON({
48
+ fill: Color4.blue,
49
+ stroke: {
50
+ width: 4,
51
+ color: Color4.red,
52
+ }
53
+ })).toMatchObject({
54
+ fill: '#0000ff',
55
+ stroke: {
56
+ width: 4,
57
+ color: '#ff0000'
58
+ },
59
+ });
60
+ });
61
+
62
+ it('JSON should be convertable into styles', () => {
63
+ const redFillJSON = { fill: '#ff0000', };
64
+ const redFillBlueStrokeJSON = { fill: '#ff0000', stroke: { width: 4, color: '#0000ff' }};
65
+ expect(styleToJSON(styleFromJSON(redFillJSON))).toMatchObject(redFillJSON);
66
+ expect(styleToJSON(styleFromJSON(redFillBlueStrokeJSON))).toMatchObject(redFillBlueStrokeJSON);
67
+ });
68
+ });
@@ -0,0 +1,46 @@
1
+ import Color4 from '../Color4';
2
+
3
+ interface RenderingStyle {
4
+ fill: Color4;
5
+ stroke?: {
6
+ color: Color4;
7
+ width: number;
8
+ };
9
+ }
10
+
11
+ export default RenderingStyle;
12
+
13
+ export const stylesEqual = (a: RenderingStyle, b: RenderingStyle): boolean => {
14
+ const result = a === b || (a.fill.eq(b.fill)
15
+ && (a.stroke == undefined) === (b.stroke == undefined)
16
+ && (a.stroke?.color?.eq(b.stroke?.color) ?? true)
17
+ && a.stroke?.width === b.stroke?.width);
18
+
19
+ // Map undefined/null -> false
20
+ return result ?? false;
21
+ };
22
+
23
+ // Returns an object that can be converted to a JSON string with
24
+ // JSON.stringify.
25
+ export const styleToJSON = (style: RenderingStyle) => {
26
+ const stroke = !style.stroke ? undefined : {
27
+ color: style.stroke.color.toHexString(),
28
+ width: style.stroke.width,
29
+ };
30
+
31
+ return {
32
+ fill: style.fill.toHexString(),
33
+ stroke,
34
+ };
35
+ };
36
+
37
+ export const styleFromJSON = (json: Record<string, any>) => {
38
+ const stroke = json.stroke ? {
39
+ color: Color4.fromHex(json.stroke.color),
40
+ width: json.stroke.width,
41
+ } : undefined;
42
+ return {
43
+ fill: Color4.fromHex(json.fill),
44
+ stroke,
45
+ };
46
+ };
@@ -28,7 +28,7 @@ describe('RenderingCache', () => {
28
28
  editor.image.renderWithCache(screenRenderer, cache, editor.viewport);
29
29
  expect(lastRenderer).toBeNull();
30
30
 
31
- editor.dispatch(new EditorImage.AddElementCommand(testStroke));
31
+ editor.dispatch(EditorImage.addElement(testStroke));
32
32
  editor.image.renderWithCache(screenRenderer, cache, editor.viewport);
33
33
 
34
34
  expect(allocdRenderers).toBeGreaterThanOrEqual(1);
@@ -0,0 +1,10 @@
1
+
2
+ export interface TextRendererLocalization {
3
+ textNode(content: string): string;
4
+ rerenderAsText: string;
5
+ }
6
+
7
+ export const defaultTextRendererLocalization: TextRendererLocalization = {
8
+ textNode: (content: string) => `Text: ${content}`,
9
+ rerenderAsText: 'Re-render as text',
10
+ };
@@ -1,4 +1,3 @@
1
- import Color4 from '../../Color4';
2
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
2
  import { TextStyle } from '../../components/Text';
4
3
  import Mat33 from '../../geometry/Mat33';
@@ -6,14 +5,7 @@ import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
6
5
  import Rect2 from '../../geometry/Rect2';
7
6
  import { Point2, Vec2 } from '../../geometry/Vec2';
8
7
  import Viewport from '../../Viewport';
9
-
10
- export interface RenderingStyle {
11
- fill: Color4;
12
- stroke?: {
13
- color: Color4;
14
- width: number;
15
- };
16
- }
8
+ import RenderingStyle, { stylesEqual } from '../RenderingStyle';
17
9
 
18
10
  export interface RenderablePathSpec {
19
11
  startPoint: Point2;
@@ -21,12 +13,6 @@ export interface RenderablePathSpec {
21
13
  style: RenderingStyle;
22
14
  }
23
15
 
24
- const stylesEqual = (a: RenderingStyle, b: RenderingStyle) => {
25
- return a === b || (a.fill.eq(b.fill)
26
- && a.stroke?.color?.eq(b.stroke?.color)
27
- && a.stroke?.width === b.stroke?.width);
28
- };
29
-
30
16
  export default abstract class AbstractRenderer {
31
17
  // If null, this' transformation is linked to the Viewport
32
18
  private selfTransform: Mat33|null = null;
@@ -5,7 +5,8 @@ import Rect2 from '../../geometry/Rect2';
5
5
  import { Point2, Vec2 } from '../../geometry/Vec2';
6
6
  import Vec3 from '../../geometry/Vec3';
7
7
  import Viewport from '../../Viewport';
8
- import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from './AbstractRenderer';
8
+ import RenderingStyle from '../RenderingStyle';
9
+ import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
9
10
 
10
11
  export default class CanvasRenderer extends AbstractRenderer {
11
12
  private ignoreObjectsAboveLevel: number|null = null;
@@ -6,7 +6,8 @@ import Rect2 from '../../geometry/Rect2';
6
6
  import { Point2, Vec2 } from '../../geometry/Vec2';
7
7
  import Vec3 from '../../geometry/Vec3';
8
8
  import Viewport from '../../Viewport';
9
- import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
9
+ import RenderingStyle from '../RenderingStyle';
10
+ import AbstractRenderer from './AbstractRenderer';
10
11
 
11
12
  export default class DummyRenderer extends AbstractRenderer {
12
13
  // Variables that track the state of what's been rendered
@@ -7,7 +7,8 @@ import Rect2 from '../../geometry/Rect2';
7
7
  import { Point2, Vec2 } from '../../geometry/Vec2';
8
8
  import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
9
9
  import Viewport from '../../Viewport';
10
- import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
10
+ import RenderingStyle from '../RenderingStyle';
11
+ import AbstractRenderer from './AbstractRenderer';
11
12
 
12
13
  const svgNameSpace = 'http://www.w3.org/2000/svg';
13
14
  export default class SVGRenderer extends AbstractRenderer {
@@ -0,0 +1,52 @@
1
+ import { TextStyle } from '../../components/Text';
2
+ import Mat33 from '../../geometry/Mat33';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ import { Vec2 } from '../../geometry/Vec2';
5
+ import Vec3 from '../../geometry/Vec3';
6
+ import Viewport from '../../Viewport';
7
+ import { TextRendererLocalization } from '../localization';
8
+ import RenderingStyle from '../RenderingStyle';
9
+ import AbstractRenderer from './AbstractRenderer';
10
+
11
+ // Outputs a description of what was rendered.
12
+
13
+ export default class TextOnlyRenderer extends AbstractRenderer {
14
+ private descriptionBuilder: string[] = [];
15
+ public constructor(viewport: Viewport, private localizationTable: TextRendererLocalization) {
16
+ super(viewport);
17
+ }
18
+
19
+ public displaySize(): Vec3 {
20
+ // We don't have a graphical display, export a reasonable size.
21
+ return Vec2.of(500, 500);
22
+ }
23
+
24
+ public clear(): void {
25
+ this.descriptionBuilder = [];
26
+ }
27
+
28
+ public getDescription(): string {
29
+ return this.descriptionBuilder.join('\n');
30
+ }
31
+
32
+ protected beginPath(_startPoint: Vec3): void {
33
+ }
34
+ protected endPath(_style: RenderingStyle): void {
35
+ }
36
+ protected lineTo(_point: Vec3): void {
37
+ }
38
+ protected moveTo(_point: Vec3): void {
39
+ }
40
+ protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void {
41
+ }
42
+ protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void {
43
+ }
44
+ public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
45
+ this.descriptionBuilder.push(this.localizationTable.textNode(text));
46
+ }
47
+ public isTooSmallToRender(rect: Rect2): boolean {
48
+ return rect.maxDimension < 10 / this.getSizeOfCanvasPixelOnScreen();
49
+ }
50
+ public drawPoints(..._points: Vec3[]): void {
51
+ }
52
+ }