js-draw 0.15.2 → 0.16.0

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 (59) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +56 -0
  2. package/CHANGELOG.md +6 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +11 -0
  5. package/dist/src/Editor.js +52 -4
  6. package/dist/src/EditorImage.d.ts +11 -11
  7. package/dist/src/EditorImage.js +54 -18
  8. package/dist/src/Viewport.d.ts +5 -0
  9. package/dist/src/Viewport.js +11 -0
  10. package/dist/src/components/AbstractComponent.d.ts +1 -0
  11. package/dist/src/components/AbstractComponent.js +5 -0
  12. package/dist/src/components/ImageBackground.d.ts +2 -1
  13. package/dist/src/components/ImageBackground.js +8 -1
  14. package/dist/src/localizations/es.js +1 -1
  15. package/dist/src/toolbar/HTMLToolbar.d.ts +25 -2
  16. package/dist/src/toolbar/HTMLToolbar.js +127 -15
  17. package/dist/src/toolbar/IconProvider.d.ts +2 -0
  18. package/dist/src/toolbar/IconProvider.js +44 -0
  19. package/dist/src/toolbar/localization.d.ts +5 -0
  20. package/dist/src/toolbar/localization.js +5 -0
  21. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +3 -1
  22. package/dist/src/toolbar/widgets/ActionButtonWidget.js +5 -1
  23. package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +1 -1
  24. package/dist/src/toolbar/widgets/BaseToolWidget.js +2 -1
  25. package/dist/src/toolbar/widgets/BaseWidget.d.ts +7 -2
  26. package/dist/src/toolbar/widgets/BaseWidget.js +23 -1
  27. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.d.ts +19 -0
  28. package/dist/src/toolbar/widgets/DocumentPropertiesWidget.js +135 -0
  29. package/dist/src/toolbar/widgets/OverflowWidget.d.ts +25 -0
  30. package/dist/src/toolbar/widgets/OverflowWidget.js +65 -0
  31. package/dist/src/toolbar/widgets/lib.d.ts +1 -0
  32. package/dist/src/toolbar/widgets/lib.js +1 -0
  33. package/dist/src/tools/PasteHandler.js +2 -2
  34. package/dist/src/tools/SelectionTool/Selection.d.ts +2 -1
  35. package/dist/src/tools/SelectionTool/Selection.js +2 -1
  36. package/package.json +1 -1
  37. package/src/Editor.loadFrom.test.ts +24 -0
  38. package/src/Editor.ts +59 -4
  39. package/src/EditorImage.ts +66 -23
  40. package/src/Viewport.ts +13 -0
  41. package/src/components/AbstractComponent.ts +6 -0
  42. package/src/components/ImageBackground.test.ts +35 -0
  43. package/src/components/ImageBackground.ts +10 -1
  44. package/src/localizations/es.ts +8 -0
  45. package/src/math/Mat33.test.ts +30 -5
  46. package/src/rendering/renderers/CanvasRenderer.ts +1 -1
  47. package/src/toolbar/HTMLToolbar.ts +164 -16
  48. package/src/toolbar/IconProvider.ts +46 -0
  49. package/src/toolbar/localization.ts +10 -0
  50. package/src/toolbar/toolbar.css +2 -0
  51. package/src/toolbar/widgets/ActionButtonWidget.ts +5 -0
  52. package/src/toolbar/widgets/BaseToolWidget.ts +3 -1
  53. package/src/toolbar/widgets/BaseWidget.ts +34 -2
  54. package/src/toolbar/widgets/DocumentPropertiesWidget.ts +185 -0
  55. package/src/toolbar/widgets/OverflowWidget.css +9 -0
  56. package/src/toolbar/widgets/OverflowWidget.ts +83 -0
  57. package/src/toolbar/widgets/lib.ts +2 -1
  58. package/src/tools/PasteHandler.ts +3 -2
  59. package/src/tools/SelectionTool/Selection.ts +2 -1
@@ -0,0 +1,65 @@
1
+ import BaseWidget from './BaseWidget';
2
+ export default class OverflowWidget extends BaseWidget {
3
+ constructor(editor, localizationTable) {
4
+ var _a;
5
+ super(editor, 'overflow-widget', localizationTable);
6
+ this.overflowChildren = [];
7
+ // Make the dropdown openable
8
+ this.container.classList.add('dropdownShowable');
9
+ (_a = this.overflowContainer) !== null && _a !== void 0 ? _a : (this.overflowContainer = document.createElement('div'));
10
+ }
11
+ getTitle() {
12
+ return this.localizationTable.toggleOverflow;
13
+ }
14
+ createIcon() {
15
+ return this.editor.icons.makeOverflowIcon();
16
+ }
17
+ handleClick() {
18
+ this.setDropdownVisible(!this.isDropdownVisible());
19
+ }
20
+ fillDropdown(dropdown) {
21
+ var _a;
22
+ (_a = this.overflowContainer) !== null && _a !== void 0 ? _a : (this.overflowContainer = document.createElement('div'));
23
+ if (this.overflowContainer.parentElement) {
24
+ this.overflowContainer.remove();
25
+ }
26
+ this.overflowContainer.classList.add('toolbar-overflow-widget-overflow-list');
27
+ dropdown.appendChild(this.overflowContainer);
28
+ return true;
29
+ }
30
+ /**
31
+ * Removes all `BaseWidget`s from this and returns them.
32
+ */
33
+ clearChildren() {
34
+ this.overflowContainer.replaceChildren();
35
+ const overflowChildren = this.overflowChildren;
36
+ this.overflowChildren = [];
37
+ return overflowChildren;
38
+ }
39
+ getChildWidgets() {
40
+ return [...this.overflowChildren];
41
+ }
42
+ hasAsChild(widget) {
43
+ for (const otherWidget of this.overflowChildren) {
44
+ if (widget === otherWidget) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Adds `widget` to this.
52
+ * `widget`'s previous parent is still responsible
53
+ * for serializing/deserializing its state.
54
+ */
55
+ addToOverflow(widget) {
56
+ this.overflowChildren.push(widget);
57
+ widget.addTo(this.overflowContainer);
58
+ widget.setIsToplevel(false);
59
+ }
60
+ // This always returns false.
61
+ // Don't try to move the overflow menu to itself.
62
+ canBeInOverflowMenu() {
63
+ return false;
64
+ }
65
+ }
@@ -7,3 +7,4 @@ export { default as HandToolWidget } from './HandToolWidget';
7
7
  export { default as SelectionToolWidget } from './SelectionToolWidget';
8
8
  export { default as EraserToolWidget } from './EraserToolWidget';
9
9
  export { default as InsertImageWidget } from './InsertImageWidget';
10
+ export { default as DocumentPropertiesWidget } from './DocumentPropertiesWidget';
@@ -7,3 +7,4 @@ export { default as HandToolWidget } from './HandToolWidget';
7
7
  export { default as SelectionToolWidget } from './SelectionToolWidget';
8
8
  export { default as EraserToolWidget } from './EraserToolWidget';
9
9
  export { default as InsertImageWidget } from './InsertImageWidget';
10
+ export { default as DocumentPropertiesWidget } from './DocumentPropertiesWidget';
@@ -7,9 +7,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { TextComponent } from '../components/lib';
10
+ import TextComponent from '../components/TextComponent';
11
11
  import SVGLoader from '../SVGLoader';
12
- import { Mat33 } from '../math/lib';
12
+ import Mat33 from '../math/Mat33';
13
13
  import BaseTool from './BaseTool';
14
14
  import TextTool from './TextTool';
15
15
  import Color4 from '../Color4';
@@ -3,7 +3,8 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  import Editor from '../../Editor';
6
- import { Mat33, Rect2 } from '../../math/lib';
6
+ import Mat33 from '../../math/Mat33';
7
+ import Rect2 from '../../math/Rect2';
7
8
  import { Point2 } from '../../math/Vec2';
8
9
  import Pointer from '../../Pointer';
9
10
  import AbstractComponent from '../../components/AbstractComponent';
@@ -13,7 +13,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
13
13
  };
14
14
  var _a;
15
15
  import SerializableCommand from '../../commands/SerializableCommand';
16
- import { Mat33, Rect2 } from '../../math/lib';
16
+ import Mat33 from '../../math/Mat33';
17
+ import Rect2 from '../../math/Rect2';
17
18
  import { Vec2 } from '../../math/Vec2';
18
19
  import SelectionHandle, { HandleShape, handleSize } from './SelectionHandle';
19
20
  import { cssPrefix } from './SelectionTool';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.15.2",
3
+ "version": "0.16.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
@@ -0,0 +1,24 @@
1
+ import Color4 from './Color4';
2
+ import { imageBackgroundCSSClassName } from './components/ImageBackground';
3
+ import { RestyleableComponent } from './lib';
4
+ import SVGLoader from './SVGLoader';
5
+ import createEditor from './testing/createEditor';
6
+
7
+ describe('Editor.loadFrom', () => {
8
+ it('should remove existing BackgroundComponents when loading new BackgroundComponents', async () => {
9
+ const editor = createEditor();
10
+ await editor.dispatch(editor.setBackgroundColor(Color4.red));
11
+
12
+ let backgroundComponents = editor.image.getBackgroundComponents();
13
+ expect(backgroundComponents).toHaveLength(1);
14
+ expect((backgroundComponents[0] as RestyleableComponent).getStyle().color).objEq(Color4.red);
15
+
16
+ await editor.loadFrom(SVGLoader.fromString(`<svg viewBox='0 0 100 100'>
17
+ <path class='${imageBackgroundCSSClassName}' d='m0,0 L100,0 L100,100 L0,100 z' fill='#000'/>
18
+ </svg>`, true));
19
+
20
+ backgroundComponents = editor.image.getBackgroundComponents();
21
+ expect(backgroundComponents).toHaveLength(1);
22
+ expect((backgroundComponents[0] as RestyleableComponent).getStyle().color).objEq(Color4.black);
23
+ });
24
+ });
package/src/Editor.ts CHANGED
@@ -26,6 +26,8 @@ import fileToBase64 from './util/fileToBase64';
26
26
  import uniteCommands from './commands/uniteCommands';
27
27
  import SelectionTool from './tools/SelectionTool/SelectionTool';
28
28
  import AbstractComponent from './components/AbstractComponent';
29
+ import Erase from './commands/Erase';
30
+ import ImageBackground, { BackgroundType } from './components/ImageBackground';
29
31
 
30
32
  type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
31
33
  type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
@@ -355,9 +357,10 @@ export class Editor {
355
357
  this.viewport.updateScreenSize(
356
358
  Vec2.of(this.display.width, this.display.height)
357
359
  );
360
+ this.queueRerender();
358
361
  });
359
362
 
360
- window.addEventListener('resize', () => {
363
+ const handleResize = () => {
361
364
  this.notifier.dispatch(EditorEventType.DisplayResized, {
362
365
  kind: EditorEventType.DisplayResized,
363
366
  newSize: Vec2.of(
@@ -365,8 +368,14 @@ export class Editor {
365
368
  this.display.height
366
369
  ),
367
370
  });
368
- this.queueRerender();
369
- });
371
+ };
372
+
373
+ if ('ResizeObserver' in (window as any)) {
374
+ const resizeObserver = new ResizeObserver(handleResize);
375
+ resizeObserver.observe(this.container);
376
+ } else {
377
+ addEventListener('resize', handleResize);
378
+ }
370
379
 
371
380
  this.accessibilityControlArea.addEventListener('input', () => {
372
381
  this.accessibilityControlArea.value = '';
@@ -935,7 +944,7 @@ export class Editor {
935
944
  }
936
945
 
937
946
  public toSVG(): SVGElement {
938
- const importExportViewport = this.image.getImportExportViewport();
947
+ const importExportViewport = this.image.getImportExportViewport().getTemporaryClone();
939
948
  const svgNameSpace = 'http://www.w3.org/2000/svg';
940
949
  const result = document.createElementNS(svgNameSpace, 'svg');
941
950
  const renderer = new SVGRenderer(result, importExportViewport);
@@ -966,10 +975,18 @@ export class Editor {
966
975
  return result;
967
976
  }
968
977
 
978
+ /**
979
+ * Load editor data from an `ImageLoader` (e.g. an {@link SVGLoader}).
980
+ *
981
+ * @see loadFromSVG
982
+ */
969
983
  public async loadFrom(loader: ImageLoader) {
970
984
  this.showLoadingWarning(0);
971
985
  this.display.setDraftMode(true);
972
986
 
987
+ const originalBackgrounds = this.image.getBackgroundComponents();
988
+ const eraseBackgroundCommand = new Erase(originalBackgrounds);
989
+
973
990
  await loader.start(async (component) => {
974
991
  await this.dispatchNoAnnounce(EditorImage.addElement(component));
975
992
  }, (countProcessed: number, totalToProcess: number) => {
@@ -984,12 +1001,50 @@ export class Editor {
984
1001
  this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
985
1002
  this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
986
1003
  });
1004
+
1005
+ // Ensure that we don't have multiple overlapping BackgroundComponents. Remove
1006
+ // old BackgroundComponents.
1007
+ // Overlapping BackgroundComponents may cause changing the background color to
1008
+ // not work properly.
1009
+ if (this.image.getBackgroundComponents().length !== originalBackgrounds.length) {
1010
+ await this.dispatchNoAnnounce(eraseBackgroundCommand);
1011
+ }
1012
+
987
1013
  this.hideLoadingWarning();
988
1014
 
989
1015
  this.display.setDraftMode(false);
990
1016
  this.queueRerender();
991
1017
  }
992
1018
 
1019
+ private getTopmostBackgroundComponent(): ImageBackground|null {
1020
+ let background: ImageBackground|null = null;
1021
+
1022
+ // Find a background component, if one exists.
1023
+ // Use the last (topmost) background component if there are multiple.
1024
+ for (const component of this.image.getBackgroundComponents()) {
1025
+ if (component instanceof ImageBackground) {
1026
+ background = component;
1027
+ }
1028
+ }
1029
+
1030
+ return background;
1031
+ }
1032
+
1033
+ /**
1034
+ * Set the background color of the image.
1035
+ */
1036
+ public setBackgroundColor(color: Color4): Command {
1037
+ let background = this.getTopmostBackgroundComponent();
1038
+
1039
+ if (!background) {
1040
+ const backgroundType = color.eq(Color4.transparent) ? BackgroundType.None : BackgroundType.SolidColor;
1041
+ background = new ImageBackground(backgroundType, color);
1042
+ return this.image.addElement(background);
1043
+ } else {
1044
+ return background.updateStyle({ color });
1045
+ }
1046
+ }
1047
+
993
1048
  // Returns the size of the visible region of the output SVG
994
1049
  public getImportExportRect(): Rect2 {
995
1050
  return this.image.getImportExportViewport().visibleRect;
@@ -25,6 +25,7 @@ export type EditorImageNotifier = EventDispatcher<EditorImageEventType, { image:
25
25
  // Handles lookup/storage of elements in the image
26
26
  export default class EditorImage {
27
27
  private root: ImageNode;
28
+ private background: ImageNode;
28
29
  private componentsById: Record<string, AbstractComponent>;
29
30
 
30
31
  /** Viewport for the exported/imported image. */
@@ -36,6 +37,7 @@ export default class EditorImage {
36
37
  // @internal
37
38
  public constructor() {
38
39
  this.root = new ImageNode();
40
+ this.background = new ImageNode();
39
41
  this.componentsById = {};
40
42
 
41
43
  this.notifier = new EventDispatcher();
@@ -56,7 +58,7 @@ export default class EditorImage {
56
58
  return this.importExportViewport;
57
59
  }
58
60
 
59
- public setImportExportRect(imageRect: Rect2) {
61
+ public setImportExportRect(imageRect: Rect2): Command {
60
62
  const importExportViewport = this.getImportExportViewport();
61
63
  const origSize = importExportViewport.visibleRect.size;
62
64
  const origTransform = importExportViewport.canvasToScreenTransform;
@@ -82,15 +84,27 @@ export default class EditorImage {
82
84
  };
83
85
  }
84
86
 
85
- // Returns the parent of the given element, if it exists.
86
- public findParent(elem: AbstractComponent): ImageNode|null {
87
- const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
88
- for (const candidate of candidates) {
89
- if (candidate.getContent() === elem) {
90
- return candidate;
87
+ // Returns all components that make up the background of this image. These
88
+ // components are rendered below all other components.
89
+ public getBackgroundComponents(): AbstractComponent[] {
90
+ const result = [];
91
+
92
+ const leaves = this.background.getLeaves();
93
+ sortLeavesByZIndex(leaves);
94
+
95
+ for (const leaf of leaves) {
96
+ const content = leaf.getContent();
97
+
98
+ if (content) {
99
+ result.push(content);
91
100
  }
92
101
  }
93
- return null;
102
+ return result;
103
+ }
104
+
105
+ // Returns the parent of the given element, if it exists.
106
+ public findParent(elem: AbstractComponent): ImageNode|null {
107
+ return this.background.getChildWithContent(elem) ?? this.root.getChildWithContent(elem);
94
108
  }
95
109
 
96
110
  // Forces a re-render of `elem` when the image is next re-rendered as a whole.
@@ -108,22 +122,22 @@ export default class EditorImage {
108
122
 
109
123
  /** @internal */
110
124
  public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
125
+ this.background.render(screenRenderer, viewport.visibleRect);
111
126
  cache.render(screenRenderer, this.root, viewport);
112
127
  }
113
128
 
114
- /** @internal */
115
- public render(renderer: AbstractRenderer, viewport: Viewport) {
116
- this.root.render(renderer, viewport.visibleRect);
129
+ /**
130
+ * Renders all nodes visible from `viewport` (or all nodes if `viewport = null`)
131
+ * @internal
132
+ */
133
+ public render(renderer: AbstractRenderer, viewport: Viewport|null) {
134
+ this.background.render(renderer, viewport?.visibleRect);
135
+ this.root.render(renderer, viewport?.visibleRect);
117
136
  }
118
137
 
119
138
  /** Renders all nodes, even ones not within the viewport. @internal */
120
139
  public renderAll(renderer: AbstractRenderer) {
121
- const leaves = this.root.getLeaves();
122
- sortLeavesByZIndex(leaves);
123
-
124
- for (const leaf of leaves) {
125
- leaf.getContent()!.render(renderer, leaf.getBBox());
126
- }
140
+ this.render(renderer, null);
127
141
  }
128
142
 
129
143
  /** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
@@ -160,12 +174,23 @@ export default class EditorImage {
160
174
  elem.onAddToImage(this);
161
175
 
162
176
  this.componentsById[elem.getId()] = elem;
163
- return this.root.addLeaf(elem);
177
+
178
+ // If a background component, add to the background. Else,
179
+ // add to the normal component tree.
180
+ const parentTree = elem.isBackground() ? this.background : this.root;
181
+ return parentTree.addLeaf(elem);
164
182
  }
165
183
 
166
184
  private removeElementDirectly(element: AbstractComponent) {
167
185
  const container = this.findParent(element);
168
186
  container?.remove();
187
+
188
+ if (container) {
189
+ this.onDestroyElement(element);
190
+ return true;
191
+ }
192
+
193
+ return false;
169
194
  }
170
195
 
171
196
  /**
@@ -180,7 +205,7 @@ export default class EditorImage {
180
205
  }
181
206
 
182
207
  /** @see EditorImage.addElement */
183
- public addElement(elem: AbstractComponent, applyByFlattening: boolean = true) {
208
+ public addElement(elem: AbstractComponent, applyByFlattening?: boolean) {
184
209
  return EditorImage.addElement(elem, applyByFlattening);
185
210
  }
186
211
 
@@ -325,6 +350,19 @@ export class ImageNode {
325
350
  return result;
326
351
  }
327
352
 
353
+ // Returns the child of this with the target content or `null` if no
354
+ // such child exists.
355
+ public getChildWithContent(target: AbstractComponent): ImageNode|null {
356
+ const candidates = this.getLeavesIntersectingRegion(target.getBBox());
357
+ for (const candidate of candidates) {
358
+ if (candidate.getContent() === target) {
359
+ return candidate;
360
+ }
361
+ }
362
+
363
+ return null;
364
+ }
365
+
328
366
  // Returns a list of leaves with this as an ancestor.
329
367
  // Like getLeavesInRegion, but does not check whether ancestors are in a given rectangle
330
368
  public getLeaves(): ImageNode[] {
@@ -458,6 +496,8 @@ export class ImageNode {
458
496
 
459
497
  // Remove this node and all of its children
460
498
  public remove() {
499
+ this.content?.onRemoveFromImage();
500
+
461
501
  if (!this.parent) {
462
502
  this.content = null;
463
503
  this.children = [];
@@ -465,8 +505,6 @@ export class ImageNode {
465
505
  return;
466
506
  }
467
507
 
468
- this.content?.onRemoveFromImage();
469
-
470
508
  const oldChildCount = this.parent.children.length;
471
509
  this.parent.children = this.parent.children.filter(node => {
472
510
  return node !== this;
@@ -489,8 +527,13 @@ export class ImageNode {
489
527
  this.children = [];
490
528
  }
491
529
 
492
- public render(renderer: AbstractRenderer, visibleRect: Rect2) {
493
- const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
530
+ public render(renderer: AbstractRenderer, visibleRect?: Rect2) {
531
+ let leaves;
532
+ if (visibleRect) {
533
+ leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
534
+ } else {
535
+ leaves = this.getLeaves();
536
+ }
494
537
  sortLeavesByZIndex(leaves);
495
538
 
496
539
  for (const leaf of leaves) {
package/src/Viewport.ts CHANGED
@@ -88,6 +88,19 @@ export class Viewport {
88
88
  this.screenRect = Rect2.empty;
89
89
  }
90
90
 
91
+ /**
92
+ * @returns a temporary copy of `this` that does not notify when modified. This is
93
+ * useful when rendering with a temporarily different viewport.
94
+ */
95
+ public getTemporaryClone(): Viewport {
96
+ const result = new Viewport(() => {});
97
+ result.transform = this.transform;
98
+ result.inverseTransform = this.inverseTransform;
99
+ result.screenRect = this.screenRect;
100
+
101
+ return result;
102
+ }
103
+
91
104
  // @internal
92
105
  public updateScreenSize(screenSize: Vec2) {
93
106
  this.screenRect = this.screenRect.resizedTo(screenSize);
@@ -156,6 +156,12 @@ export default abstract class AbstractComponent {
156
156
  return true;
157
157
  }
158
158
 
159
+ // @returns true iff this component should be added to the background, rather than the
160
+ // foreground of the image.
161
+ public isBackground(): boolean {
162
+ return false;
163
+ }
164
+
159
165
  // @returns an approximation of the proportional time it takes to render this component.
160
166
  // This is intended to be a rough estimate, but, for example, a stroke with two points sould have
161
167
  // a renderingWeight approximately twice that of a stroke with one point.
@@ -0,0 +1,35 @@
1
+ import Color4 from '../Color4';
2
+ import { Path, Rect2 } from '../math/lib';
3
+ import createEditor from '../testing/createEditor';
4
+ import ImageBackground, { BackgroundType, imageBackgroundCSSClassName } from './ImageBackground';
5
+
6
+ describe('ImageBackground', () => {
7
+ it('should render to fill exported SVG', () => {
8
+ const editor = createEditor();
9
+ const background = new ImageBackground(BackgroundType.SolidColor, Color4.green);
10
+ editor.image.addElement(
11
+ background
12
+ ).apply(editor);
13
+
14
+ const expectedImportExportRect = new Rect2(-10, 10, 15, 20);
15
+ editor.setImportExportRect(expectedImportExportRect).apply(editor);
16
+ expect(editor.getImportExportRect()).objEq(expectedImportExportRect);
17
+
18
+ expect(background.getBBox()).objEq(expectedImportExportRect);
19
+
20
+ const rendered = editor.toSVG();
21
+ const renderedBackground = rendered.querySelector(`.${imageBackgroundCSSClassName}`);
22
+
23
+ if (renderedBackground === null) {
24
+ throw new Error('ImageBackground did not render in exported SVG');
25
+ }
26
+
27
+ expect(renderedBackground.tagName.toLowerCase()).toBe('path');
28
+
29
+ const pathString = renderedBackground.getAttribute('d')!;
30
+ expect(pathString).not.toBeNull();
31
+
32
+ const path = Path.fromString(pathString);
33
+ expect(path.bbox).objEq(editor.getImportExportRect());
34
+ });
35
+ });
@@ -50,7 +50,7 @@ export default class ImageBackground extends AbstractComponent implements Restyl
50
50
  }
51
51
 
52
52
  // @internal
53
- public forceStyle(style: ComponentStyle, _editor: Editor | null): void {
53
+ public forceStyle(style: ComponentStyle, editor: Editor | null): void {
54
54
  const fill = style.color;
55
55
 
56
56
  if (!fill) {
@@ -63,6 +63,11 @@ export default class ImageBackground extends AbstractComponent implements Restyl
63
63
  } else {
64
64
  this.backgroundType = BackgroundType.SolidColor;
65
65
  }
66
+
67
+ if (editor) {
68
+ editor.image.queueRerenderOf(this);
69
+ editor.queueRerender();
70
+ }
66
71
  }
67
72
 
68
73
  public onAddToImage(image: EditorImage) {
@@ -124,6 +129,10 @@ export default class ImageBackground extends AbstractComponent implements Restyl
124
129
  return false;
125
130
  }
126
131
 
132
+ public isBackground(): boolean {
133
+ return true;
134
+ }
135
+
127
136
  protected serializeToJSON() {
128
137
  return {
129
138
  mainColor: this.mainColor.toHexString(),
@@ -57,10 +57,18 @@ const localization: EditorLocalization = {
57
57
  eraserTool: 'Borrador',
58
58
  textTool: 'Texto',
59
59
  enterTextToInsert: 'Entra texto',
60
+ textSize: 'Tamaño',
60
61
  rerenderAsText: 'Redibuja la pantalla al texto',
62
+ lockRotation: 'Bloquea rotación',
61
63
  image: 'Imagen',
62
64
  imageSize: (size: number, units: string) => `Tamaño del imagen: ${size} ${units}`,
63
65
  imageLoadError: (message: string)=> `Error cargando imagen: ${message}`,
66
+ toggleOverflow: 'Más',
67
+
68
+ documentProperties: 'Fondo',
69
+ imageWidthOption: 'Ancho: ',
70
+ imageHeightOption: 'Alto: ',
71
+ backgroundColor: 'Color de fondo: '
64
72
  };
65
73
 
66
74
  export default localization;
@@ -1,5 +1,5 @@
1
1
  import Mat33 from './Mat33';
2
- import { Vec2 } from './Vec2';
2
+ import { Point2, Vec2 } from './Vec2';
3
3
  import Vec3 from './Vec3';
4
4
 
5
5
 
@@ -130,7 +130,7 @@ describe('Mat33 tests', () => {
130
130
 
131
131
  const starterTransform = new Mat33(
132
132
  -0.2588190451025205, -0.9659258262890688, 923.7645204565603,
133
- 0.9659258262890688, -0.2588190451025205, -49.829447083761465,
133
+ 0.9659258262890688, -0.2588190451025205, -49.829447083761465,
134
134
  0, 0, 1
135
135
  );
136
136
  expect(starterTransform.invertable()).toBe(true);
@@ -152,11 +152,36 @@ describe('Mat33 tests', () => {
152
152
  ).objEq(Vec2.unitX, fuzz);
153
153
  });
154
154
 
155
+ it('z-rotation matrix inverses should undo the z-rotation', () => {
156
+ const testCases: Array<[ number, Point2 ]> = [
157
+ [ Math.PI / 2, Vec2.zero ],
158
+ [ Math.PI, Vec2.of(1, 1) ],
159
+ [ -Math.PI, Vec2.of(1, 1) ],
160
+ [ -Math.PI * 2, Vec2.of(1, 1) ],
161
+ [ -Math.PI * 2, Vec2.of(123, 456) ],
162
+ [ -Math.PI / 4, Vec2.of(123, 456) ],
163
+ [ 0.1, Vec2.of(1, 2) ],
164
+ ];
165
+
166
+ const fuzz = 0.00001;
167
+ for (const [ angle, center ] of testCases) {
168
+ const mat = Mat33.zRotation(angle, center);
169
+ expect(mat.inverse().rightMul(mat)).objEq(Mat33.identity, fuzz);
170
+ expect(mat.rightMul(mat.inverse())).objEq(Mat33.identity, fuzz);
171
+ }
172
+ });
173
+
155
174
  it('z-rotation should preserve given origin', () => {
156
- const rotationOrigin = Vec2.of(75.16363373235318, 104.29870408043762);
157
- const angle = 6.205048847547065;
175
+ const testCases: Array<[ number, Point2 ]> = [
176
+ [ 6.205048847547065, Vec2.of(75.16363373235318, 104.29870408043762) ],
177
+ [ 1.234, Vec2.of(-56, 789) ],
178
+ [ -Math.PI, Vec2.of(-56, 789) ],
179
+ [ -Math.PI / 2, Vec2.of(-0.001, 1.0002) ],
180
+ ];
158
181
 
159
- expect(Mat33.zRotation(angle, rotationOrigin).transformVec2(rotationOrigin)).objEq(rotationOrigin);
182
+ for (const [angle, rotationOrigin] of testCases) {
183
+ expect(Mat33.zRotation(angle, rotationOrigin).transformVec2(rotationOrigin)).objEq(rotationOrigin);
184
+ }
160
185
  });
161
186
 
162
187
  it('should correctly apply a mapping to all components', () => {
@@ -75,7 +75,7 @@ export default class CanvasRenderer extends AbstractRenderer {
75
75
  public displaySize(): Vec2 {
76
76
  return Vec2.of(
77
77
  this.ctx.canvas.clientWidth,
78
- this.ctx.canvas.clientHeight
78
+ this.ctx.canvas.clientHeight,
79
79
  );
80
80
  }
81
81