js-draw 0.14.0 → 0.15.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 (88) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.yml +8 -0
  2. package/CHANGELOG.md +8 -1
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Color4.d.ts +4 -0
  5. package/dist/src/Color4.js +22 -0
  6. package/dist/src/Editor.d.ts +2 -1
  7. package/dist/src/Editor.js +10 -1
  8. package/dist/src/EditorImage.d.ts +1 -0
  9. package/dist/src/EditorImage.js +11 -0
  10. package/dist/src/commands/UnresolvedCommand.d.ts +14 -0
  11. package/dist/src/commands/UnresolvedCommand.js +22 -0
  12. package/dist/src/commands/uniteCommands.js +4 -2
  13. package/dist/src/components/AbstractComponent.d.ts +0 -1
  14. package/dist/src/components/AbstractComponent.js +30 -50
  15. package/dist/src/components/RestylableComponent.d.ts +24 -0
  16. package/dist/src/components/RestylableComponent.js +80 -0
  17. package/dist/src/components/Stroke.d.ts +8 -1
  18. package/dist/src/components/Stroke.js +49 -1
  19. package/dist/src/components/TextComponent.d.ts +10 -10
  20. package/dist/src/components/TextComponent.js +46 -13
  21. package/dist/src/components/lib.d.ts +2 -1
  22. package/dist/src/components/lib.js +2 -1
  23. package/dist/src/components/localization.d.ts +1 -0
  24. package/dist/src/components/localization.js +1 -0
  25. package/dist/src/rendering/TextRenderingStyle.d.ts +23 -0
  26. package/dist/src/rendering/TextRenderingStyle.js +20 -0
  27. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
  28. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
  29. package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
  30. package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
  31. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
  32. package/dist/src/toolbar/IconProvider.d.ts +2 -1
  33. package/dist/src/toolbar/IconProvider.js +10 -0
  34. package/dist/src/toolbar/localization.d.ts +1 -0
  35. package/dist/src/toolbar/localization.js +1 -0
  36. package/dist/src/toolbar/widgets/BaseWidget.js +10 -4
  37. package/dist/src/toolbar/widgets/InsertImageWidget.js +2 -1
  38. package/dist/src/toolbar/widgets/SelectionToolWidget.js +77 -1
  39. package/dist/src/tools/Pen.js +2 -2
  40. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.d.ts +8 -0
  41. package/dist/src/tools/SelectionTool/SelectAllShortcutHandler.js +22 -0
  42. package/dist/src/tools/SelectionTool/Selection.js +1 -1
  43. package/dist/src/tools/SelectionTool/SelectionTool.js +4 -9
  44. package/dist/src/tools/TextTool.d.ts +1 -1
  45. package/dist/src/tools/ToolController.js +2 -0
  46. package/dist/src/tools/lib.d.ts +1 -0
  47. package/dist/src/tools/lib.js +1 -0
  48. package/dist/src/tools/localization.d.ts +1 -0
  49. package/dist/src/tools/localization.js +1 -0
  50. package/package.json +1 -1
  51. package/src/Color4.test.ts +4 -0
  52. package/src/Color4.ts +26 -0
  53. package/src/Editor.toSVG.test.ts +1 -1
  54. package/src/Editor.ts +12 -1
  55. package/src/EditorImage.ts +13 -0
  56. package/src/SVGLoader.ts +2 -1
  57. package/src/commands/UnresolvedCommand.ts +37 -0
  58. package/src/commands/uniteCommands.ts +5 -2
  59. package/src/components/AbstractComponent.ts +36 -61
  60. package/src/components/RestylableComponent.ts +142 -0
  61. package/src/components/Stroke.test.ts +68 -0
  62. package/src/components/Stroke.ts +68 -2
  63. package/src/components/TextComponent.test.ts +56 -2
  64. package/src/components/TextComponent.ts +63 -25
  65. package/src/components/lib.ts +4 -1
  66. package/src/components/localization.ts +3 -0
  67. package/src/math/Rect2.test.ts +18 -6
  68. package/src/rendering/TextRenderingStyle.ts +38 -0
  69. package/src/rendering/renderers/AbstractRenderer.ts +1 -1
  70. package/src/rendering/renderers/CanvasRenderer.ts +2 -1
  71. package/src/rendering/renderers/DummyRenderer.ts +1 -1
  72. package/src/rendering/renderers/SVGRenderer.ts +1 -1
  73. package/src/rendering/renderers/TextOnlyRenderer.ts +1 -1
  74. package/src/toolbar/IconProvider.ts +12 -1
  75. package/src/toolbar/localization.ts +2 -0
  76. package/src/toolbar/widgets/BaseWidget.ts +12 -4
  77. package/src/toolbar/widgets/InsertImageWidget.ts +2 -1
  78. package/src/toolbar/widgets/SelectionToolWidget.ts +95 -1
  79. package/src/tools/PanZoom.test.ts +2 -1
  80. package/src/tools/PasteHandler.ts +1 -1
  81. package/src/tools/Pen.ts +2 -2
  82. package/src/tools/SelectionTool/SelectAllShortcutHandler.ts +28 -0
  83. package/src/tools/SelectionTool/Selection.ts +1 -1
  84. package/src/tools/SelectionTool/SelectionTool.ts +4 -9
  85. package/src/tools/TextTool.ts +2 -1
  86. package/src/tools/ToolController.ts +2 -0
  87. package/src/tools/lib.ts +1 -0
  88. package/src/tools/localization.ts +2 -0
@@ -7,4 +7,5 @@ export { default as AbstractComponent } from './AbstractComponent';
7
7
  import Stroke from './Stroke';
8
8
  import TextComponent from './TextComponent';
9
9
  import ImageComponent from './ImageComponent';
10
- export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
10
+ import RestyleableComponent, { createRestyleComponentCommand } from './RestylableComponent';
11
+ export { Stroke, TextComponent as Text, RestyleableComponent, createRestyleComponentCommand, TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -7,4 +7,5 @@ export { default as AbstractComponent } from './AbstractComponent';
7
7
  import Stroke from './Stroke';
8
8
  import TextComponent from './TextComponent';
9
9
  import ImageComponent from './ImageComponent';
10
- export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
10
+ import { createRestyleComponentCommand } from './RestylableComponent';
11
+ export { Stroke, TextComponent as Text, createRestyleComponentCommand, TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -4,5 +4,6 @@ export interface ImageComponentLocalization {
4
4
  imageNode: (description: string) => string;
5
5
  stroke: string;
6
6
  svgObject: string;
7
+ restyledElements: string;
7
8
  }
8
9
  export declare const defaultComponentLocalization: ImageComponentLocalization;
@@ -2,6 +2,7 @@ export const defaultComponentLocalization = {
2
2
  unlabeledImageNode: 'Unlabeled image node',
3
3
  stroke: 'Stroke',
4
4
  svgObject: 'SVG Object',
5
+ restyledElements: 'Restyled elements',
5
6
  text: (text) => `Text object: ${text}`,
6
7
  imageNode: (description) => `Image: ${description}`,
7
8
  };
@@ -0,0 +1,23 @@
1
+ import RenderingStyle from './RenderingStyle';
2
+ export interface TextStyle {
3
+ size: number;
4
+ fontFamily: string;
5
+ fontWeight?: string;
6
+ fontVariant?: string;
7
+ renderingStyle: RenderingStyle;
8
+ }
9
+ export default TextStyle;
10
+ export declare const textStyleFromJSON: (json: any) => TextStyle;
11
+ export declare const textStyleToJSON: (style: TextStyle) => {
12
+ renderingStyle: {
13
+ fill: string;
14
+ stroke: {
15
+ color: string;
16
+ width: number;
17
+ } | undefined;
18
+ };
19
+ size: number;
20
+ fontFamily: string;
21
+ fontWeight?: string | undefined;
22
+ fontVariant?: string | undefined;
23
+ };
@@ -0,0 +1,20 @@
1
+ import { styleFromJSON, styleToJSON } from './RenderingStyle';
2
+ export const textStyleFromJSON = (json) => {
3
+ if (typeof json === 'string') {
4
+ json = JSON.parse(json);
5
+ }
6
+ if (typeof (json.fontFamily) !== 'string') {
7
+ throw new Error('Serialized textStyle missing string fontFamily attribute!');
8
+ }
9
+ const style = {
10
+ renderingStyle: styleFromJSON(json.renderingStyle),
11
+ size: json.size,
12
+ fontWeight: json.fontWeight,
13
+ fontVariant: json.fontVariant,
14
+ fontFamily: json.fontFamily,
15
+ };
16
+ return style;
17
+ };
18
+ export const textStyleToJSON = (style) => {
19
+ return Object.assign(Object.assign({}, style), { renderingStyle: styleToJSON(style.renderingStyle) });
20
+ };
@@ -1,11 +1,11 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
- import { TextStyle } from '../../components/TextComponent';
3
2
  import Mat33 from '../../math/Mat33';
4
3
  import Path, { PathCommand } from '../../math/Path';
5
4
  import Rect2 from '../../math/Rect2';
6
5
  import { Point2, Vec2 } from '../../math/Vec2';
7
6
  import Viewport from '../../Viewport';
8
7
  import RenderingStyle from '../RenderingStyle';
8
+ import TextStyle from '../TextRenderingStyle';
9
9
  export interface RenderablePathSpec {
10
10
  startPoint: Point2;
11
11
  commands: PathCommand[];
@@ -1,10 +1,10 @@
1
- import { TextStyle } from '../../components/TextComponent';
2
1
  import Mat33 from '../../math/Mat33';
3
2
  import Rect2 from '../../math/Rect2';
4
3
  import { Point2, Vec2 } from '../../math/Vec2';
5
4
  import Vec3 from '../../math/Vec3';
6
5
  import Viewport from '../../Viewport';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
9
  export default class CanvasRenderer extends AbstractRenderer {
10
10
  private ctx;
@@ -1,10 +1,10 @@
1
- import { TextStyle } from '../../components/TextComponent';
2
1
  import Mat33 from '../../math/Mat33';
3
2
  import Rect2 from '../../math/Rect2';
4
3
  import { Point2, Vec2 } from '../../math/Vec2';
5
4
  import Vec3 from '../../math/Vec3';
6
5
  import Viewport from '../../Viewport';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
9
9
  export default class DummyRenderer extends AbstractRenderer {
10
10
  clearedCount: number;
@@ -1,10 +1,10 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
- import { TextStyle } from '../../components/TextComponent';
3
2
  import Mat33 from '../../math/Mat33';
4
3
  import Rect2 from '../../math/Rect2';
5
4
  import { Point2, Vec2 } from '../../math/Vec2';
6
5
  import Viewport from '../../Viewport';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
9
9
  export declare const renderedStylesheetId = "js-draw-style-sheet";
10
10
  export default class SVGRenderer extends AbstractRenderer {
@@ -1,10 +1,10 @@
1
- import { TextStyle } from '../../components/TextComponent';
2
1
  import Mat33 from '../../math/Mat33';
3
2
  import Rect2 from '../../math/Rect2';
4
3
  import Vec3 from '../../math/Vec3';
5
4
  import Viewport from '../../Viewport';
6
5
  import { TextRendererLocalization } from '../localization';
7
6
  import RenderingStyle from '../RenderingStyle';
7
+ import TextStyle from '../TextRenderingStyle';
8
8
  import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
9
9
  export default class TextOnlyRenderer extends AbstractRenderer {
10
10
  private localizationTable;
@@ -1,6 +1,6 @@
1
1
  import Color4 from '../Color4';
2
2
  import { ComponentBuilderFactory } from '../components/builders/types';
3
- import { TextStyle } from '../components/TextComponent';
3
+ import TextStyle from '../rendering/TextRenderingStyle';
4
4
  import Pen from '../tools/Pen';
5
5
  export type IconType = HTMLImageElement | SVGElement;
6
6
  /**
@@ -51,6 +51,7 @@ export default class IconProvider {
51
51
  makePenIcon(strokeSize: number, color: string | Color4, rounded?: boolean): IconType;
52
52
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
53
53
  makePipetteIcon(color?: Color4): IconType;
54
+ makeFormatSelectionIcon(): IconType;
54
55
  makeResizeViewportIcon(): IconType;
55
56
  makeDuplicateSelectionIcon(): IconType;
56
57
  makePasteIcon(): IconType;
@@ -537,6 +537,16 @@ export default class IconProvider {
537
537
  icon.setAttribute('viewBox', '0 0 100 100');
538
538
  return icon;
539
539
  }
540
+ makeFormatSelectionIcon() {
541
+ return this.makeIconFromPath(`
542
+ M 5 10
543
+ L 5 20 L 10 20 L 10 15 L 20 15 L 20 40 L 15 40 L 15 45 L 35 45 L 35 40 L 30 40 L 30 15 L 40 15 L 40 20 L 45 20 L 45 15 L 45 10 L 5 10 z
544
+ M 90 10 C 90 10 86.5 13.8 86 14 C 86 14 76.2 24.8 76 25 L 60 25 L 60 65 C 75 70 85 70 90 65 L 90 25 L 80 25 L 76.7 25 L 90 10 z
545
+ M 60 25 L 55 25 L 50 30 L 60 25 z
546
+ M 10 55 L 10 90 L 41 90 L 41 86 L 45 86 L 45 55 L 10 55 z
547
+ M 42 87 L 42 93 L 48 93 L 48 87 L 42 87 z
548
+ `);
549
+ }
540
550
  makeResizeViewportIcon() {
541
551
  return this.makeIconFromPath(`
542
552
  M 75 5 75 10 90 10 90 25 95 25 95 5 75 5 z
@@ -26,6 +26,7 @@ export interface ToolbarLocalization {
26
26
  duplicateSelection: string;
27
27
  pickColorFromScreen: string;
28
28
  clickToPickColorAnnouncement: string;
29
+ reformatSelection: string;
29
30
  undo: string;
30
31
  redo: string;
31
32
  zoom: string;
@@ -5,6 +5,7 @@ export const defaultToolbarLocalization = {
5
5
  handTool: 'Pan',
6
6
  zoom: 'Zoom',
7
7
  image: 'Image',
8
+ reformatSelection: 'Format selection',
8
9
  inputAltText: 'Alt text: ',
9
10
  chooseFile: 'Choose file: ',
10
11
  submit: 'Submit',
@@ -88,24 +88,30 @@ export default class BaseWidget {
88
88
  }
89
89
  // If we didn't do anything with the event, send it to the editor.
90
90
  if (!handled) {
91
- this.editor.toolController.dispatchInputEvent({
91
+ handled = this.editor.toolController.dispatchInputEvent({
92
92
  kind: InputEvtType.KeyPressEvent,
93
93
  key: evt.key,
94
- ctrlKey: evt.ctrlKey,
94
+ ctrlKey: evt.ctrlKey || evt.metaKey,
95
95
  altKey: evt.altKey,
96
96
  });
97
97
  }
98
+ if (handled) {
99
+ evt.preventDefault();
100
+ }
98
101
  };
99
102
  button.onkeyup = evt => {
100
103
  if (evt.key in clickTriggers) {
101
104
  return;
102
105
  }
103
- this.editor.toolController.dispatchInputEvent({
106
+ const handled = this.editor.toolController.dispatchInputEvent({
104
107
  kind: InputEvtType.KeyUpEvent,
105
108
  key: evt.key,
106
- ctrlKey: evt.ctrlKey,
109
+ ctrlKey: evt.ctrlKey || evt.metaKey,
107
110
  altKey: evt.altKey,
108
111
  });
112
+ if (handled) {
113
+ evt.preventDefault();
114
+ }
109
115
  };
110
116
  button.onclick = () => {
111
117
  if (!this.disabled) {
@@ -10,7 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import ImageComponent from '../../components/ImageComponent';
11
11
  import Erase from '../../commands/Erase';
12
12
  import EditorImage from '../../EditorImage';
13
- import { SelectionTool, uniteCommands } from '../../lib';
13
+ import uniteCommands from '../../commands/uniteCommands';
14
+ import SelectionTool from '../../tools/SelectionTool/SelectionTool';
14
15
  import Mat33 from '../../math/Mat33';
15
16
  import fileToBase64 from '../../util/fileToBase64';
16
17
  import ActionButtonWidget from './ActionButtonWidget';
@@ -7,9 +7,82 @@ 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 Color4 from '../../Color4';
11
+ import { isRestylableComponent } from '../../components/RestylableComponent';
12
+ import uniteCommands from '../../commands/uniteCommands';
10
13
  import { EditorEventType } from '../../types';
14
+ import makeColorInput from '../makeColorInput';
11
15
  import ActionButtonWidget from './ActionButtonWidget';
12
16
  import BaseToolWidget from './BaseToolWidget';
17
+ import BaseWidget from './BaseWidget';
18
+ class RestyleSelectionWidget extends BaseWidget {
19
+ constructor(editor, selectionTool, localizationTable) {
20
+ super(editor, 'restyle-selection', localizationTable);
21
+ this.selectionTool = selectionTool;
22
+ this.updateFormatData = () => { };
23
+ // Allow showing the dropdown even if this widget isn't selected yet
24
+ this.container.classList.add('dropdownShowable');
25
+ this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
26
+ if (toolEvt.kind !== EditorEventType.ToolUpdated) {
27
+ throw new Error('Invalid event type!');
28
+ }
29
+ if (toolEvt.tool === this.selectionTool) {
30
+ this.updateFormatData();
31
+ }
32
+ });
33
+ }
34
+ getTitle() {
35
+ return this.localizationTable.reformatSelection;
36
+ }
37
+ createIcon() {
38
+ return this.editor.icons.makeFormatSelectionIcon();
39
+ }
40
+ handleClick() {
41
+ this.setDropdownVisible(!this.isDropdownVisible());
42
+ }
43
+ fillDropdown(dropdown) {
44
+ const container = document.createElement('div');
45
+ const colorRow = document.createElement('div');
46
+ const colorLabel = document.createElement('label');
47
+ const [colorInput, colorInputContainer, setColorInputValue] = makeColorInput(this.editor, color => {
48
+ const selection = this.selectionTool.getSelection();
49
+ if (selection) {
50
+ const updateStyleCommands = [];
51
+ for (const elem of selection.getSelectedObjects()) {
52
+ if (isRestylableComponent(elem)) {
53
+ updateStyleCommands.push(elem.updateStyle({ color }));
54
+ }
55
+ }
56
+ const unitedCommand = uniteCommands(updateStyleCommands);
57
+ this.editor.dispatch(unitedCommand);
58
+ }
59
+ });
60
+ colorLabel.innerText = this.localizationTable.colorLabel;
61
+ this.updateFormatData = () => {
62
+ const selection = this.selectionTool.getSelection();
63
+ if (selection) {
64
+ colorInput.disabled = false;
65
+ const colors = [];
66
+ for (const elem of selection.getSelectedObjects()) {
67
+ if (isRestylableComponent(elem)) {
68
+ const color = elem.getStyle().color;
69
+ if (color) {
70
+ colors.push(color);
71
+ }
72
+ }
73
+ }
74
+ setColorInputValue(Color4.average(colors));
75
+ }
76
+ else {
77
+ colorInput.disabled = true;
78
+ }
79
+ };
80
+ colorRow.replaceChildren(colorLabel, colorInputContainer);
81
+ container.replaceChildren(colorRow);
82
+ dropdown.replaceChildren(container);
83
+ return true;
84
+ }
85
+ }
13
86
  export default class SelectionToolWidget extends BaseToolWidget {
14
87
  constructor(editor, tool, localization) {
15
88
  super(editor, tool, 'selection-tool-widget', localization);
@@ -26,13 +99,16 @@ export default class SelectionToolWidget extends BaseToolWidget {
26
99
  const selection = this.tool.getSelection();
27
100
  this.editor.dispatch(yield selection.duplicateSelectedObjects());
28
101
  }), localization);
102
+ const restyleButton = new RestyleSelectionWidget(editor, this.tool, localization);
29
103
  this.addSubWidget(resizeButton);
30
104
  this.addSubWidget(deleteButton);
31
105
  this.addSubWidget(duplicateButton);
106
+ this.addSubWidget(restyleButton);
32
107
  const updateDisabled = (disabled) => {
33
108
  resizeButton.setDisabled(disabled);
34
109
  deleteButton.setDisabled(disabled);
35
110
  duplicateButton.setDisabled(disabled);
111
+ restyleButton.setDisabled(disabled);
36
112
  };
37
113
  updateDisabled(true);
38
114
  // Enable/disable actions based on whether items are selected
@@ -42,7 +118,7 @@ export default class SelectionToolWidget extends BaseToolWidget {
42
118
  }
43
119
  if (toolEvt.tool === this.tool) {
44
120
  const selection = this.tool.getSelection();
45
- const hasSelection = selection && selection.region.area > 0;
121
+ const hasSelection = selection && selection.getSelectedItemCount() > 0;
46
122
  updateDisabled(!hasSelection);
47
123
  }
48
124
  });
@@ -152,7 +152,7 @@ export default class Pen extends BaseTool {
152
152
  this.setThickness(newThickness);
153
153
  return true;
154
154
  }
155
- if (key === 'control') {
155
+ if (key === 'control' || key === 'meta') {
156
156
  this.ctrlKeyPressed = true;
157
157
  return true;
158
158
  }
@@ -164,7 +164,7 @@ export default class Pen extends BaseTool {
164
164
  }
165
165
  onKeyUp({ key }) {
166
166
  key = key.toLowerCase();
167
- if (key === 'control') {
167
+ if (key === 'control' || key === 'meta') {
168
168
  this.ctrlKeyPressed = false;
169
169
  return true;
170
170
  }
@@ -0,0 +1,8 @@
1
+ import Editor from '../../Editor';
2
+ import { KeyPressEvent } from '../../types';
3
+ import BaseTool from '../BaseTool';
4
+ export default class SelectAllShortcutHandler extends BaseTool {
5
+ private editor;
6
+ constructor(editor: Editor);
7
+ onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean;
8
+ }
@@ -0,0 +1,22 @@
1
+ import BaseTool from '../BaseTool';
2
+ import SelectionTool from './SelectionTool';
3
+ // Handles ctrl+a: Select all
4
+ export default class SelectAllShortcutHandler extends BaseTool {
5
+ constructor(editor) {
6
+ super(editor.notifier, editor.localization.selectAllTool);
7
+ this.editor = editor;
8
+ }
9
+ // @internal
10
+ onKeyPress({ key, ctrlKey }) {
11
+ if (ctrlKey && key === 'a') {
12
+ const selectionTools = this.editor.toolController.getMatchingTools(SelectionTool);
13
+ if (selectionTools.length > 0) {
14
+ const selectionTool = selectionTools[0];
15
+ selectionTool.setEnabled(true);
16
+ selectionTool.setSelection(this.editor.image.getAllElements());
17
+ return true;
18
+ }
19
+ }
20
+ return false;
21
+ }
22
+ }
@@ -440,7 +440,7 @@ Selection.ApplyTransformationCommand = class extends SerializableCommand {
440
440
  this.resolveToElems(editor);
441
441
  (_b = this.selection) === null || _b === void 0 ? void 0 : _b.setTransform(this.fullTransform.inverse(), false);
442
442
  (_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
443
- yield editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
443
+ yield editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize, true);
444
444
  (_d = this.selection) === null || _d === void 0 ? void 0 : _d.setTransform(Mat33.identity);
445
445
  (_e = this.selection) === null || _e === void 0 ? void 0 : _e.recomputeRegion();
446
446
  (_f = this.selection) === null || _f === void 0 ? void 0 : _f.updateUI();
@@ -171,7 +171,7 @@ export default class SelectionTool extends BaseTool {
171
171
  this.expandingSelectionBox = false;
172
172
  }
173
173
  onKeyPress(event) {
174
- if (event.key === 'Control') {
174
+ if (event.key === 'Control' || event.key === 'Meta') {
175
175
  this.ctrlKeyPressed = true;
176
176
  return true;
177
177
  }
@@ -181,8 +181,7 @@ export default class SelectionTool extends BaseTool {
181
181
  return true;
182
182
  }
183
183
  else if (event.key === 'a' && event.ctrlKey) {
184
- // Handle ctrl+A on key up.
185
- // Return early to prevent 'a' from moving the selection/view.
184
+ this.setSelection(this.editor.image.getAllElements());
186
185
  return true;
187
186
  }
188
187
  else if (event.ctrlKey) {
@@ -268,7 +267,7 @@ export default class SelectionTool extends BaseTool {
268
267
  return handled;
269
268
  }
270
269
  onKeyUp(evt) {
271
- if (evt.key === 'Control') {
270
+ if (evt.key === 'Control' || evt.key === 'Meta') {
272
271
  this.ctrlKeyPressed = false;
273
272
  return true;
274
273
  }
@@ -283,10 +282,6 @@ export default class SelectionTool extends BaseTool {
283
282
  });
284
283
  return true;
285
284
  }
286
- else if (evt.key === 'a') {
287
- this.setSelection(this.editor.image.getAllElements());
288
- return true;
289
- }
290
285
  }
291
286
  if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
292
287
  this.selectionBox.finalizeTransform();
@@ -394,5 +389,5 @@ SelectionTool.handleableKeys = [
394
389
  'e', 'j', 'ArrowDown',
395
390
  'r', 'R',
396
391
  'i', 'I', 'o', 'O',
397
- 'Control',
392
+ 'Control', 'Meta',
398
393
  ];
@@ -1,9 +1,9 @@
1
1
  import Color4 from '../Color4';
2
- import { TextStyle } from '../components/TextComponent';
3
2
  import Editor from '../Editor';
4
3
  import { PointerEvt } from '../types';
5
4
  import BaseTool from './BaseTool';
6
5
  import { ToolLocalization } from './localization';
6
+ import TextStyle from '../rendering/TextRenderingStyle';
7
7
  export default class TextTool extends BaseTool {
8
8
  private editor;
9
9
  private localizationTable;
@@ -13,6 +13,7 @@ import PasteHandler from './PasteHandler';
13
13
  import ToolbarShortcutHandler from './ToolbarShortcutHandler';
14
14
  import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
15
15
  import FindTool from './FindTool';
16
+ import SelectAllShortcutHandler from './SelectionTool/SelectAllShortcutHandler';
16
17
  export default class ToolController {
17
18
  /** @internal */
18
19
  constructor(editor, localization) {
@@ -43,6 +44,7 @@ export default class ToolController {
43
44
  new ToolSwitcherShortcut(editor),
44
45
  new FindTool(editor),
45
46
  new PasteHandler(editor),
47
+ new SelectAllShortcutHandler(editor),
46
48
  ];
47
49
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
48
50
  panZoomTool.setEnabled(true);
@@ -10,6 +10,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
10
10
  export { default as PenTool, PenStyle } from './Pen';
11
11
  export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
13
+ export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
13
14
  export { default as EraserTool } from './Eraser';
14
15
  export { default as PasteHandler } from './PasteHandler';
15
16
  export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
@@ -10,6 +10,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
10
10
  export { default as PenTool } from './Pen';
11
11
  export { default as TextTool } from './TextTool';
12
12
  export { default as SelectionTool } from './SelectionTool/SelectionTool';
13
+ export { default as SelectAllShortcutHandler } from './SelectionTool/SelectAllShortcutHandler';
13
14
  export { default as EraserTool } from './Eraser';
14
15
  export { default as PasteHandler } from './PasteHandler';
15
16
  export { default as ToolbarShortcutHandler } from './ToolbarShortcutHandler';
@@ -2,6 +2,7 @@ export interface ToolLocalization {
2
2
  keyboardPanZoom: string;
3
3
  penTool: (penId: number) => string;
4
4
  selectionTool: string;
5
+ selectAllTool: string;
5
6
  eraserTool: string;
6
7
  touchPanTool: string;
7
8
  twoFingerPanZoomTool: string;
@@ -1,6 +1,7 @@
1
1
  export const defaultToolLocalization = {
2
2
  penTool: (penId) => `Pen ${penId}`,
3
3
  selectionTool: 'Selection',
4
+ selectAllTool: 'Select all shortcut',
4
5
  eraserTool: 'Eraser',
5
6
  touchPanTool: 'Touch panning',
6
7
  twoFingerPanZoomTool: 'Panning and zooming',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -27,4 +27,8 @@ describe('Color4', () => {
27
27
  Color4.ofRGB(0.7, 0.3, 0)
28
28
  );
29
29
  });
30
+
31
+ it('should mix red with nothing and get red', () => {
32
+ expect(Color4.average([ Color4.red ])).objEq(Color4.red);
33
+ });
30
34
  });
package/src/Color4.ts CHANGED
@@ -151,6 +151,32 @@ export default class Color4 {
151
151
  );
152
152
  }
153
153
 
154
+ /**
155
+ * @returns the component-wise average of `colors`, or `Color4.transparent` if `colors` is empty.
156
+ */
157
+ public static average(colors: Color4[]) {
158
+ let averageA = 0;
159
+ let averageR = 0;
160
+ let averageG = 0;
161
+ let averageB = 0;
162
+
163
+ for (const color of colors) {
164
+ averageA += color.a;
165
+ averageR += color.r;
166
+ averageG += color.g;
167
+ averageB += color.b;
168
+ }
169
+
170
+ if (colors.length > 0) {
171
+ averageA /= colors.length;
172
+ averageR /= colors.length;
173
+ averageG /= colors.length;
174
+ averageB /= colors.length;
175
+ }
176
+
177
+ return new Color4(averageR, averageG, averageB, averageA);
178
+ }
179
+
154
180
  private hexString: string|null = null;
155
181
 
156
182
  /**
@@ -1,5 +1,5 @@
1
- import { TextStyle } from './components/TextComponent';
2
1
  import { Color4, Mat33, Rect2, TextComponent, EditorImage, Vec2 } from './lib';
2
+ import TextStyle from './rendering/TextRenderingStyle';
3
3
  import SVGLoader from './SVGLoader';
4
4
  import createEditor from './testing/createEditor';
5
5
 
package/src/Editor.ts CHANGED
@@ -704,8 +704,14 @@ export class Editor {
704
704
  return this.asyncApplyOrUnapplyCommands(commands, true, chunkSize);
705
705
  }
706
706
 
707
+ // If `unapplyInReverseOrder`, commands are reversed before unapplying.
707
708
  // @see {@link #asyncApplyOrUnapplyCommands }
708
- public asyncUnapplyCommands(commands: Command[], chunkSize: number) {
709
+ public asyncUnapplyCommands(commands: Command[], chunkSize: number, unapplyInReverseOrder: boolean = false) {
710
+ if (unapplyInReverseOrder) {
711
+ commands = [ ...commands ]; // copy
712
+ commands.reverse();
713
+ }
714
+
709
715
  return this.asyncApplyOrUnapplyCommands(commands, false, chunkSize);
710
716
  }
711
717
 
@@ -745,6 +751,11 @@ export class Editor {
745
751
  });
746
752
  }
747
753
 
754
+ // @internal
755
+ public isRerenderQueued() {
756
+ return this.rerenderQueued;
757
+ }
758
+
748
759
  public rerender(showImageBounds: boolean = true) {
749
760
  this.display.startRerender();
750
761
 
@@ -34,6 +34,19 @@ export default class EditorImage {
34
34
  return null;
35
35
  }
36
36
 
37
+ // Forces a re-render of `elem` when the image is next re-rendered as a whole.
38
+ // Does nothing if `elem` is not in this.
39
+ public queueRerenderOf(elem: AbstractComponent) {
40
+ // TODO: Make more efficient (e.g. increase IDs of all parents,
41
+ // make cache take into account last modified time instead of IDs, etc.)
42
+ const parent = this.findParent(elem);
43
+
44
+ if (parent) {
45
+ parent.remove();
46
+ this.addElementDirectly(elem);
47
+ }
48
+ }
49
+
37
50
  /** @internal */
38
51
  public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
39
52
  cache.render(screenRenderer, this.root, viewport);