js-draw 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +29 -29
  2. package/dist/Editor.css +65 -4
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +73 -40
  6. package/dist/cjs/Editor.js +90 -24
  7. package/dist/cjs/EditorImage.d.ts +58 -6
  8. package/dist/cjs/EditorImage.js +336 -60
  9. package/dist/cjs/SVGLoader.d.ts +10 -4
  10. package/dist/cjs/SVGLoader.js +67 -32
  11. package/dist/cjs/UndoRedoHistory.d.ts +2 -2
  12. package/dist/cjs/UndoRedoHistory.js +4 -2
  13. package/dist/cjs/Viewport.d.ts +2 -1
  14. package/dist/cjs/Viewport.js +12 -3
  15. package/dist/cjs/commands/Command.d.ts +1 -0
  16. package/dist/cjs/commands/Command.js +1 -0
  17. package/dist/cjs/commands/Erase.js +1 -1
  18. package/dist/cjs/commands/SerializableCommand.d.ts +1 -1
  19. package/dist/cjs/commands/SerializableCommand.js +16 -2
  20. package/dist/cjs/commands/localization.d.ts +2 -0
  21. package/dist/cjs/commands/localization.js +2 -0
  22. package/dist/cjs/components/AbstractComponent.d.ts +38 -0
  23. package/dist/cjs/components/AbstractComponent.js +31 -0
  24. package/dist/cjs/components/BackgroundComponent.d.ts +10 -1
  25. package/dist/cjs/components/BackgroundComponent.js +61 -6
  26. package/dist/cjs/components/SVGGlobalAttributesObject.d.ts +2 -1
  27. package/dist/cjs/components/SVGGlobalAttributesObject.js +30 -1
  28. package/dist/cjs/components/Stroke.d.ts +1 -0
  29. package/dist/cjs/components/Stroke.js +44 -0
  30. package/dist/cjs/components/UnknownSVGObject.d.ts +2 -1
  31. package/dist/cjs/components/UnknownSVGObject.js +30 -1
  32. package/dist/cjs/lib.d.ts +2 -45
  33. package/dist/cjs/lib.js +2 -45
  34. package/dist/cjs/rendering/RenderingStyle.d.ts +1 -0
  35. package/dist/cjs/rendering/renderers/AbstractRenderer.js +1 -1
  36. package/dist/cjs/rendering/renderers/SVGRenderer.js +8 -19
  37. package/dist/cjs/rendering/renderers/SVGRenderer.test.d.ts +1 -0
  38. package/dist/cjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  39. package/dist/cjs/shortcuts/KeyboardShortcutManager.js +2 -2
  40. package/dist/cjs/toolbar/localization.d.ts +1 -0
  41. package/dist/cjs/toolbar/localization.js +1 -0
  42. package/dist/cjs/toolbar/widgets/BaseWidget.js +5 -0
  43. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +54 -25
  44. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +8 -0
  45. package/dist/cjs/tools/PanZoom.js +13 -8
  46. package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
  47. package/dist/cjs/tools/ScrollbarTool.js +85 -0
  48. package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  49. package/dist/cjs/tools/ToolController.js +2 -0
  50. package/dist/cjs/types.d.ts +3 -1
  51. package/dist/cjs/util/assertions.d.ts +4 -0
  52. package/dist/cjs/util/assertions.js +12 -1
  53. package/dist/cjs/version.js +1 -1
  54. package/dist/mjs/Editor.d.ts +73 -40
  55. package/dist/mjs/Editor.mjs +90 -24
  56. package/dist/mjs/EditorImage.d.ts +58 -6
  57. package/dist/mjs/EditorImage.mjs +313 -61
  58. package/dist/mjs/SVGLoader.d.ts +10 -4
  59. package/dist/mjs/SVGLoader.mjs +66 -31
  60. package/dist/mjs/UndoRedoHistory.d.ts +2 -2
  61. package/dist/mjs/UndoRedoHistory.mjs +4 -2
  62. package/dist/mjs/Viewport.d.ts +2 -1
  63. package/dist/mjs/Viewport.mjs +12 -3
  64. package/dist/mjs/commands/Command.d.ts +1 -0
  65. package/dist/mjs/commands/Command.mjs +1 -0
  66. package/dist/mjs/commands/Erase.mjs +1 -1
  67. package/dist/mjs/commands/SerializableCommand.d.ts +1 -1
  68. package/dist/mjs/commands/SerializableCommand.mjs +16 -2
  69. package/dist/mjs/commands/localization.d.ts +2 -0
  70. package/dist/mjs/commands/localization.mjs +2 -0
  71. package/dist/mjs/components/AbstractComponent.d.ts +38 -0
  72. package/dist/mjs/components/AbstractComponent.mjs +30 -0
  73. package/dist/mjs/components/BackgroundComponent.d.ts +10 -1
  74. package/dist/mjs/components/BackgroundComponent.mjs +38 -6
  75. package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +2 -1
  76. package/dist/mjs/components/SVGGlobalAttributesObject.mjs +7 -1
  77. package/dist/mjs/components/Stroke.d.ts +1 -0
  78. package/dist/mjs/components/Stroke.mjs +44 -0
  79. package/dist/mjs/components/UnknownSVGObject.d.ts +2 -1
  80. package/dist/mjs/components/UnknownSVGObject.mjs +7 -1
  81. package/dist/mjs/lib.d.ts +2 -45
  82. package/dist/mjs/lib.mjs +2 -45
  83. package/dist/mjs/rendering/RenderingStyle.d.ts +1 -0
  84. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +1 -1
  85. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +8 -19
  86. package/dist/mjs/rendering/renderers/SVGRenderer.test.d.ts +1 -0
  87. package/dist/mjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  88. package/dist/mjs/shortcuts/KeyboardShortcutManager.mjs +2 -2
  89. package/dist/mjs/toolbar/localization.d.ts +1 -0
  90. package/dist/mjs/toolbar/localization.mjs +1 -0
  91. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +5 -0
  92. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -25
  93. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +8 -0
  94. package/dist/mjs/tools/PanZoom.mjs +13 -8
  95. package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
  96. package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
  97. package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  98. package/dist/mjs/tools/ToolController.mjs +2 -0
  99. package/dist/mjs/types.d.ts +3 -1
  100. package/dist/mjs/util/assertions.d.ts +4 -0
  101. package/dist/mjs/util/assertions.mjs +10 -0
  102. package/dist/mjs/version.mjs +1 -1
  103. package/package.json +3 -4
  104. package/src/Editor.scss +8 -0
  105. package/src/dialogs/dialogs.scss +2 -1
  106. package/src/toolbar/EdgeToolbar.scss +4 -1
  107. package/src/toolbar/widgets/DocumentPropertiesWidget.scss +12 -0
  108. package/src/toolbar/widgets/components/makeGridSelector.scss +1 -1
  109. package/src/tools/ScrollbarTool.scss +57 -0
  110. package/src/tools/{SoundUITool.css → SoundUITool.scss} +4 -0
  111. package/src/tools/tools.scss +2 -1
@@ -1,6 +1,6 @@
1
1
  import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
2
2
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
3
- import AbstractComponent from './AbstractComponent';
3
+ import AbstractComponent, { ComponentSizingMode } from './AbstractComponent';
4
4
  import { ImageComponentLocalization } from './localization';
5
5
  type GlobalAttrsList = Array<[string, string | null]>;
6
6
  export default class SVGGlobalAttributesObject extends AbstractComponent {
@@ -11,6 +11,7 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
11
11
  intersects(_lineSegment: LineSegment2): boolean;
12
12
  protected applyTransformation(_affineTransfm: Mat33): void;
13
13
  isSelectable(): boolean;
14
+ getSizingMode(): ComponentSizingMode;
14
15
  protected createClone(): SVGGlobalAttributesObject;
15
16
  description(localization: ImageComponentLocalization): string;
16
17
  protected serializeToJSON(): string | null;
@@ -6,7 +6,7 @@
6
6
  //
7
7
  import { Rect2 } from '@js-draw/math';
8
8
  import SVGRenderer from '../rendering/renderers/SVGRenderer.mjs';
9
- import AbstractComponent from './AbstractComponent.mjs';
9
+ import AbstractComponent, { ComponentSizingMode } from './AbstractComponent.mjs';
10
10
  const componentKind = 'svg-global-attributes';
11
11
  // Stores global SVG attributes (e.g. namespace identifiers.)
12
12
  export default class SVGGlobalAttributesObject extends AbstractComponent {
@@ -32,6 +32,12 @@ export default class SVGGlobalAttributesObject extends AbstractComponent {
32
32
  isSelectable() {
33
33
  return false;
34
34
  }
35
+ getSizingMode() {
36
+ // This component can be shown anywhere (it won't be
37
+ // visible to the user, it just needs to be saved with
38
+ // the image).
39
+ return ComponentSizingMode.Anywhere;
40
+ }
35
41
  createClone() {
36
42
  return new SVGGlobalAttributesObject(this.attrs);
37
43
  }
@@ -48,6 +48,7 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
48
48
  updateStyle(style: ComponentStyle): SerializableCommand;
49
49
  forceStyle(style: ComponentStyle, editor: Editor | null): void;
50
50
  intersects(line: LineSegment2): boolean;
51
+ intersectsRect(rect: Rect2): boolean;
51
52
  render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
52
53
  getProportionalRenderingTime(): number;
53
54
  private bboxForPart;
@@ -122,6 +122,50 @@ export default class Stroke extends AbstractComponent {
122
122
  }
123
123
  return false;
124
124
  }
125
+ intersectsRect(rect) {
126
+ // AbstractComponent::intersectsRect can be inexact for strokes with non-zero
127
+ // stroke radius (has many false negatives). As such, additional checks are
128
+ // done here, before passing to the superclass.
129
+ if (!rect.intersects(this.getBBox())) {
130
+ return false;
131
+ }
132
+ // The following check only checks for the positive case:
133
+ // Sample a set of points that are known to be within each part of this
134
+ // stroke. For example, the points marked with an "x" below:
135
+ // ___________________
136
+ // / \
137
+ // | x x |
138
+ // \_____________ |
139
+ // | x |
140
+ // \_____/
141
+ //
142
+ // Because we don't want the following case to result in selection,
143
+ // __________________
144
+ // /.___. \
145
+ // || x | x | <- /* The
146
+ // |·---· | .___.
147
+ // \____________ | | |
148
+ // | x | ·---·
149
+ // \_____/ denotes the input rectangle */
150
+ //
151
+ // we need to ensure that the rectangle intersects each point **and** the
152
+ // edge of the rectangle.
153
+ for (const part of this.parts) {
154
+ // As such, we need to shrink the input rectangle to verify that the original,
155
+ // unshrunken rectangle would have intersected the edge of the stroke if it
156
+ // intersects a point within the stroke.
157
+ const interiorRect = rect.grownBy(-(part.style.stroke?.width ?? 0));
158
+ if (interiorRect.area === 0) {
159
+ continue;
160
+ }
161
+ for (const point of part.path.startEndPoints()) {
162
+ if (interiorRect.containsPoint(point)) {
163
+ return true;
164
+ }
165
+ }
166
+ }
167
+ return super.intersectsRect(rect);
168
+ }
125
169
  render(canvas, visibleRect) {
126
170
  canvas.startObject(this.getBBox());
127
171
  for (const part of this.parts) {
@@ -1,6 +1,6 @@
1
1
  import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
2
2
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
3
- import AbstractComponent from './AbstractComponent';
3
+ import AbstractComponent, { ComponentSizingMode } from './AbstractComponent';
4
4
  import { ImageComponentLocalization } from './localization';
5
5
  export default class UnknownSVGObject extends AbstractComponent {
6
6
  private svgObject;
@@ -10,6 +10,7 @@ export default class UnknownSVGObject extends AbstractComponent {
10
10
  intersects(lineSegment: LineSegment2): boolean;
11
11
  protected applyTransformation(_affineTransfm: Mat33): void;
12
12
  isSelectable(): boolean;
13
+ getSizingMode(): ComponentSizingMode;
13
14
  protected createClone(): AbstractComponent;
14
15
  description(localization: ImageComponentLocalization): string;
15
16
  protected serializeToJSON(): string | null;
@@ -5,7 +5,7 @@
5
5
  //
6
6
  import { Rect2 } from '@js-draw/math';
7
7
  import SVGRenderer from '../rendering/renderers/SVGRenderer.mjs';
8
- import AbstractComponent from './AbstractComponent.mjs';
8
+ import AbstractComponent, { ComponentSizingMode } from './AbstractComponent.mjs';
9
9
  const componentId = 'unknown-svg-object';
10
10
  export default class UnknownSVGObject extends AbstractComponent {
11
11
  constructor(svgObject) {
@@ -30,6 +30,12 @@ export default class UnknownSVGObject extends AbstractComponent {
30
30
  isSelectable() {
31
31
  return false;
32
32
  }
33
+ getSizingMode() {
34
+ // This component can be shown anywhere (it won't be
35
+ // visible to the user, it just needs to be saved with
36
+ // the image).
37
+ return ComponentSizingMode.Anywhere;
38
+ }
33
39
  createClone() {
34
40
  return new UnknownSVGObject(this.svgObject.cloneNode(true));
35
41
  }
package/dist/mjs/lib.d.ts CHANGED
@@ -2,52 +2,9 @@
2
2
  * The main entrypoint for the NPM package. Everything exported by this file
3
3
  * is available through the [`js-draw` package](https://www.npmjs.com/package/js-draw).
4
4
  *
5
- * @example
6
- * ```ts,runnable
7
- * import { Editor, Vec3, Mat33, ToolbarWidgetTag } from 'js-draw';
5
+ * ## Example
8
6
  *
9
- * // Use the Material Icon pack.
10
- * import { MaterialIconProvider } from '@js-draw/material-icons';
11
- *
12
- * // Apply js-draw CSS
13
- * import 'js-draw/styles';
14
- * // If your bundler doesn't support the above, try
15
- * // import 'js-draw/bundledStyles';
16
- *
17
- * (async () => {
18
- * const editor = new Editor(document.body, {
19
- * iconProvider: new MaterialIconProvider(),
20
- * });
21
- * const toolbar = editor.addToolbar();
22
- *
23
- * // Increases the minimum height of the editor
24
- * editor.getRootElement().style.height = '600px';
25
- *
26
- * // Loads from SVG data
27
- * await editor.loadFromSVG(`
28
- * <svg viewBox="0 0 500 500" width="500" height="500" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
29
- * <style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style>
30
- * <path d="M500,500L500,0L0,0L0,500L500,500" fill="#aaa" class="js-draw-image-background"></path>
31
- * <text style="transform: matrix(1, 0, 0, 1, 57, 192); font-family: serif; font-size: 32px; fill: #111;">Testing...</text>
32
- * </svg>
33
- * `);
34
- *
35
- * // Adding tags to a toolbar button allows different styles to be applied.
36
- * // Also see addActionButton.
37
- * const buttonLabels = [ ToolbarWidgetTag.Save ];
38
- *
39
- * toolbar.addSaveButton(() => {
40
- * const saveData = editor.toSVG().outerHTML;
41
- *
42
- * // Do something with saveData
43
- * });
44
- *
45
- * toolbar.addExitButton(() => {
46
- * // Save/confirm exiting here?
47
- * editor.remove();
48
- * });
49
- * })();
50
- * ```
7
+ * [[include:doc-pages/inline-examples/main-js-draw-example.md]]
51
8
  *
52
9
  * @see
53
10
  * - {@link Editor}
package/dist/mjs/lib.mjs CHANGED
@@ -2,52 +2,9 @@
2
2
  * The main entrypoint for the NPM package. Everything exported by this file
3
3
  * is available through the [`js-draw` package](https://www.npmjs.com/package/js-draw).
4
4
  *
5
- * @example
6
- * ```ts,runnable
7
- * import { Editor, Vec3, Mat33, ToolbarWidgetTag } from 'js-draw';
5
+ * ## Example
8
6
  *
9
- * // Use the Material Icon pack.
10
- * import { MaterialIconProvider } from '@js-draw/material-icons';
11
- *
12
- * // Apply js-draw CSS
13
- * import 'js-draw/styles';
14
- * // If your bundler doesn't support the above, try
15
- * // import 'js-draw/bundledStyles';
16
- *
17
- * (async () => {
18
- * const editor = new Editor(document.body, {
19
- * iconProvider: new MaterialIconProvider(),
20
- * });
21
- * const toolbar = editor.addToolbar();
22
- *
23
- * // Increases the minimum height of the editor
24
- * editor.getRootElement().style.height = '600px';
25
- *
26
- * // Loads from SVG data
27
- * await editor.loadFromSVG(`
28
- * <svg viewBox="0 0 500 500" width="500" height="500" version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
29
- * <style id="js-draw-style-sheet">path{stroke-linecap:round;stroke-linejoin:round;}text{white-space:pre;}</style>
30
- * <path d="M500,500L500,0L0,0L0,500L500,500" fill="#aaa" class="js-draw-image-background"></path>
31
- * <text style="transform: matrix(1, 0, 0, 1, 57, 192); font-family: serif; font-size: 32px; fill: #111;">Testing...</text>
32
- * </svg>
33
- * `);
34
- *
35
- * // Adding tags to a toolbar button allows different styles to be applied.
36
- * // Also see addActionButton.
37
- * const buttonLabels = [ ToolbarWidgetTag.Save ];
38
- *
39
- * toolbar.addSaveButton(() => {
40
- * const saveData = editor.toSVG().outerHTML;
41
- *
42
- * // Do something with saveData
43
- * });
44
- *
45
- * toolbar.addExitButton(() => {
46
- * // Save/confirm exiting here?
47
- * editor.remove();
48
- * });
49
- * })();
50
- * ```
7
+ * [[include:doc-pages/inline-examples/main-js-draw-example.md]]
51
8
  *
52
9
  * @see
53
10
  * - {@link Editor}
@@ -3,6 +3,7 @@ interface RenderingStyle {
3
3
  readonly fill: Color4;
4
4
  readonly stroke?: {
5
5
  readonly color: Color4;
6
+ /** Note: The stroke `width` is twice the stroke radius. */
6
7
  readonly width: number;
7
8
  };
8
9
  }
@@ -63,7 +63,7 @@ export default class AbstractRenderer {
63
63
  drawPath(path) {
64
64
  // If we're being called outside of an object,
65
65
  // we can't delay rendering
66
- if (this.objectLevel === 0) {
66
+ if (this.objectLevel === 0 || this.currentPaths === null) {
67
67
  this.currentPaths = [path];
68
68
  this.flushPath();
69
69
  this.currentPaths = null;
@@ -129,26 +129,18 @@ export default class SVGRenderer extends AbstractRenderer {
129
129
  }
130
130
  // Apply [elemTransform] to [elem]. Uses both a `matrix` and `.x`, `.y` properties if `setXY` is true.
131
131
  // Otherwise, just uses a `matrix`.
132
- transformFrom(elemTransform, elem, inCanvasSpace = false, setXY = true) {
133
- let transform = !inCanvasSpace ? this.getCanvasToScreenTransform().rightMul(elemTransform) : elemTransform;
134
- const translation = transform.transformVec2(Vec2.zero);
135
- if (setXY) {
136
- transform = transform.rightMul(Mat33.translation(translation.times(-1)));
137
- }
132
+ transformFrom(elemTransform, elem, inCanvasSpace = false) {
133
+ const transform = !inCanvasSpace ? this.getCanvasToScreenTransform().rightMul(elemTransform) : elemTransform;
138
134
  if (!transform.eq(Mat33.identity)) {
139
- elem.style.transform = `matrix(
140
- ${transform.a1}, ${transform.b1},
141
- ${transform.a2}, ${transform.b2},
142
- ${transform.a3}, ${transform.b3}
143
- )`;
135
+ const matrixString = transform.toCSSMatrix();
136
+ elem.style.transform = matrixString;
137
+ // Most browsers round the components of CSS transforms.
138
+ // Include a higher precision copy of the element's transform.
139
+ elem.setAttribute('data-highp-transform', matrixString);
144
140
  }
145
141
  else {
146
142
  elem.style.transform = '';
147
143
  }
148
- if (setXY) {
149
- elem.setAttribute('x', `${toRoundedString(translation.x)}`);
150
- elem.setAttribute('y', `${toRoundedString(translation.y)}`);
151
- }
152
144
  }
153
145
  drawText(text, transform, style) {
154
146
  const applyTextStyles = (elem, style) => {
@@ -184,10 +176,7 @@ export default class SVGRenderer extends AbstractRenderer {
184
176
  if (!this.textContainer) {
185
177
  const container = document.createElementNS(svgNameSpace, 'text');
186
178
  container.appendChild(document.createTextNode(text));
187
- // Don't set .x/.y properties (just use .style.transform).
188
- // Child nodes aren't translated by .x/.y properties, but are by .style.transform.
189
- const setXY = false;
190
- this.transformFrom(transform, container, true, setXY);
179
+ this.transformFrom(transform, container, true);
191
180
  applyTextStyles(container, style);
192
181
  this.elem.appendChild(container);
193
182
  this.objectElems?.push(container);
@@ -0,0 +1 @@
1
+ export {};
@@ -33,8 +33,8 @@ export default class KeyboardShortcutManager {
33
33
  * const shortcutId = 'io.github.personalizedrefrigerator.js-draw.select-all';
34
34
  *
35
35
  * // Associate two shortcuts with the same ID
36
- * const shortcut1 = KeyboardShortcutManager.keyboardShortcutFromString('ctrlOrMeta+a');
37
- * const shortcut2 = KeyboardShortcutManager.keyboardShortcutFromString('ctrlOrMeta+shift+a');
36
+ * const shortcut1 = KeyBinding.fromString('ctrlOrMeta+a');
37
+ * const shortcut2 = KeyBinding.fromString('ctrlOrMeta+shift+a');
38
38
  * KeyboardShortcutManager.registerDefaultKeyboardShortcut(
39
39
  * shortcutId,
40
40
  * [ shortcut1, shortcut2 ],
@@ -54,8 +54,8 @@ class KeyboardShortcutManager {
54
54
  * const shortcutId = 'io.github.personalizedrefrigerator.js-draw.select-all';
55
55
  *
56
56
  * // Associate two shortcuts with the same ID
57
- * const shortcut1 = KeyboardShortcutManager.keyboardShortcutFromString('ctrlOrMeta+a');
58
- * const shortcut2 = KeyboardShortcutManager.keyboardShortcutFromString('ctrlOrMeta+shift+a');
57
+ * const shortcut1 = KeyBinding.fromString('ctrlOrMeta+a');
58
+ * const shortcut2 = KeyBinding.fromString('ctrlOrMeta+shift+a');
59
59
  * KeyboardShortcutManager.registerDefaultKeyboardShortcut(
60
60
  * shortcutId,
61
61
  * [ shortcut1, shortcut2 ],
@@ -44,6 +44,7 @@ export interface ToolbarLocalization {
44
44
  imageWidthOption: string;
45
45
  imageHeightOption: string;
46
46
  useGridOption: string;
47
+ enableAutoresizeOption: string;
47
48
  toggleOverflow: string;
48
49
  about: string;
49
50
  inputStabilization: string;
@@ -34,6 +34,7 @@ export const defaultToolbarLocalization = {
34
34
  imageWidthOption: 'Width',
35
35
  imageHeightOption: 'Height',
36
36
  useGridOption: 'Grid',
37
+ enableAutoresizeOption: 'Auto-resize',
37
38
  toggleOverflow: 'More',
38
39
  about: 'About',
39
40
  inputStabilization: 'Input stabilization',
@@ -50,6 +50,11 @@ class BaseWidget {
50
50
  this.label = document.createElement('label');
51
51
  this.button.setAttribute('role', 'button');
52
52
  this.button.tabIndex = 0;
53
+ // Disable the context menu. This allows long-press gestures to trigger the button's
54
+ // tooltip instead.
55
+ this.button.oncontextmenu = event => {
56
+ event.preventDefault();
57
+ };
53
58
  const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
54
59
  // If the onKeyPress function has been extended and the editor is configured to send keypress events to
55
60
  // toolbar widgets,
@@ -94,39 +94,49 @@ class DocumentPropertiesWidget extends BaseWidget {
94
94
  const container = document.createElement('div');
95
95
  container.classList.add(`${toolbarCSSPrefix}spacedList`, `${toolbarCSSPrefix}nonbutton-controls-main-list`, `${toolbarCSSPrefix}document-properties-widget`);
96
96
  // Background color input
97
- const backgroundColorRow = document.createElement('div');
98
- const backgroundColorLabel = document.createElement('label');
99
- backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
100
- const { input: colorInput, container: backgroundColorInputContainer, setValue: setBgColorInputValue } = makeColorInput(this.editor, color => {
101
- if (!color.eq(this.getBackgroundColor())) {
102
- this.setBackgroundColor(color);
103
- }
104
- });
105
- colorInput.id = `${toolbarCSSPrefix}docPropertiesColorInput-${DocumentPropertiesWidget.idCounter++}`;
106
- backgroundColorLabel.htmlFor = colorInput.id;
107
- backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
97
+ const makeBackgroundColorInput = () => {
98
+ const backgroundColorRow = document.createElement('div');
99
+ const backgroundColorLabel = document.createElement('label');
100
+ backgroundColorLabel.innerText = this.localizationTable.backgroundColor;
101
+ const { input: colorInput, container: backgroundColorInputContainer, setValue: setBgColorInputValue } = makeColorInput(this.editor, color => {
102
+ if (!color.eq(this.getBackgroundColor())) {
103
+ this.setBackgroundColor(color);
104
+ }
105
+ });
106
+ colorInput.id = `${toolbarCSSPrefix}docPropertiesColorInput-${DocumentPropertiesWidget.idCounter++}`;
107
+ backgroundColorLabel.htmlFor = colorInput.id;
108
+ backgroundColorRow.replaceChildren(backgroundColorLabel, backgroundColorInputContainer);
109
+ return { setBgColorInputValue, backgroundColorRow };
110
+ };
111
+ const { backgroundColorRow, setBgColorInputValue } = makeBackgroundColorInput();
112
+ const makeCheckboxRow = (labelText, onChange) => {
113
+ const rowContainer = document.createElement('div');
114
+ const labelElement = document.createElement('label');
115
+ const checkboxElement = document.createElement('input');
116
+ checkboxElement.id = `${toolbarCSSPrefix}docPropertiesCheckbox-${DocumentPropertiesWidget.idCounter++}`;
117
+ labelElement.htmlFor = checkboxElement.id;
118
+ checkboxElement.type = 'checkbox';
119
+ labelElement.innerText = labelText;
120
+ checkboxElement.oninput = () => {
121
+ onChange(checkboxElement.checked);
122
+ };
123
+ rowContainer.replaceChildren(labelElement, checkboxElement);
124
+ return { container: rowContainer, checkbox: checkboxElement };
125
+ };
108
126
  // Background style selector
109
- const useGridRow = document.createElement('div');
110
- const useGridLabel = document.createElement('label');
111
- const useGridCheckbox = document.createElement('input');
112
- useGridCheckbox.id = `${toolbarCSSPrefix}docPropertiesUseGridCheckbox-${DocumentPropertiesWidget.idCounter++}`;
113
- useGridLabel.htmlFor = useGridCheckbox.id;
114
- useGridCheckbox.type = 'checkbox';
115
- useGridLabel.innerText = this.localizationTable.useGridOption;
116
- useGridCheckbox.oninput = () => {
127
+ const { container: useGridRow, checkbox: useGridCheckbox } = makeCheckboxRow(this.localizationTable.useGridOption, (checked) => {
117
128
  const prevBackgroundType = this.getBackgroundType();
118
129
  const wasGrid = prevBackgroundType === BackgroundType.Grid;
119
- if (wasGrid === useGridCheckbox.checked) {
130
+ if (wasGrid === checked) {
120
131
  // Already the requested background type.
121
132
  return;
122
133
  }
123
134
  let newBackgroundType = BackgroundType.SolidColor;
124
- if (useGridCheckbox.checked) {
135
+ if (checked) {
125
136
  newBackgroundType = BackgroundType.Grid;
126
137
  }
127
138
  this.editor.dispatch(this.setBackgroundType(newBackgroundType));
128
- };
129
- useGridRow.replaceChildren(useGridLabel, useGridCheckbox);
139
+ });
130
140
  // Adds a width/height input
131
141
  const addDimensionRow = (labelContent, onChange) => {
132
142
  const row = document.createElement('div');
@@ -139,15 +149,25 @@ class DocumentPropertiesWidget extends BaseWidget {
139
149
  label.htmlFor = input.id;
140
150
  input.style.flexGrow = '2';
141
151
  input.style.width = '25px';
142
- row.style.display = 'flex';
143
152
  input.oninput = () => {
144
153
  onChange(parseFloat(input.value));
145
154
  };
155
+ row.classList.add('js-draw-size-input-row');
146
156
  row.replaceChildren(label, input);
147
157
  return {
148
158
  setValue: (value) => {
149
159
  input.value = value.toString();
150
160
  },
161
+ setIsAutomaticSize: (automatic) => {
162
+ input.disabled = automatic;
163
+ const automaticSizeClass = 'size-input-row--automatic-size';
164
+ if (automatic) {
165
+ row.classList.add(automaticSizeClass);
166
+ }
167
+ else {
168
+ row.classList.remove(automaticSizeClass);
169
+ }
170
+ },
151
171
  element: row,
152
172
  };
153
173
  };
@@ -157,6 +177,11 @@ class DocumentPropertiesWidget extends BaseWidget {
157
177
  const imageHeightRow = addDimensionRow(this.localizationTable.imageHeightOption, (value) => {
158
178
  this.updateImportExportRectSize({ height: value });
159
179
  });
180
+ // The autoresize checkbox
181
+ const { container: auroresizeRow, checkbox: autoresizeCheckbox } = makeCheckboxRow(this.localizationTable.enableAutoresizeOption, (checked) => {
182
+ const image = this.editor.image;
183
+ this.editor.dispatch(image.setAutoresizeEnabled(checked));
184
+ });
160
185
  // The "About..." button
161
186
  const aboutButton = document.createElement('button');
162
187
  aboutButton.classList.add('about-button');
@@ -166,13 +191,17 @@ class DocumentPropertiesWidget extends BaseWidget {
166
191
  };
167
192
  this.updateDropdownContent = () => {
168
193
  setBgColorInputValue(this.getBackgroundColor());
194
+ const autoresize = this.editor.image.getAutoresizeEnabled();
169
195
  const importExportRect = this.editor.getImportExportRect();
170
196
  imageWidthRow.setValue(importExportRect.width);
171
197
  imageHeightRow.setValue(importExportRect.height);
198
+ autoresizeCheckbox.checked = autoresize;
199
+ imageWidthRow.setIsAutomaticSize(autoresize);
200
+ imageHeightRow.setIsAutomaticSize(autoresize);
172
201
  useGridCheckbox.checked = this.getBackgroundType() === BackgroundType.Grid;
173
202
  };
174
203
  this.updateDropdownContent();
175
- container.replaceChildren(backgroundColorRow, useGridRow, imageWidthRow.element, imageHeightRow.element, aboutButton);
204
+ container.replaceChildren(backgroundColorRow, useGridRow, imageWidthRow.element, imageHeightRow.element, auroresizeRow, aboutButton);
176
205
  dropdown.replaceChildren(container);
177
206
  return true;
178
207
  }
@@ -68,6 +68,14 @@ labelText, defaultId, choices) => {
68
68
  }
69
69
  updateButtonCSS();
70
70
  };
71
+ button.onfocus = () => {
72
+ if (buttonContainer.querySelector(':focus-visible')) {
73
+ buttonContainer.classList.add('focus-visible');
74
+ }
75
+ };
76
+ button.onblur = () => {
77
+ buttonContainer.classList.remove('focus-visible');
78
+ };
71
79
  buttonContainer.replaceChildren(button, labelContainer);
72
80
  menuContainer.appendChild(buttonContainer);
73
81
  // Set whether the current button is checked
@@ -206,24 +206,26 @@ export default class PanZoom extends BaseTool {
206
206
  this.lastScreenCenter = screenCenter;
207
207
  this.lastDist = dist;
208
208
  this.transform = Viewport.transformBy(this.transform.transform.rightMul(transformUpdate));
209
+ return transformUpdate;
209
210
  }
210
211
  handleOneFingerMove(pointer) {
211
212
  const delta = this.getCenterDelta(pointer.screenPos);
212
- this.transform = Viewport.transformBy(this.transform.transform.rightMul(Mat33.translation(delta)));
213
+ const transformUpdate = Mat33.translation(delta);
214
+ this.transform = Viewport.transformBy(this.transform.transform.rightMul(transformUpdate));
213
215
  this.updateVelocity(pointer.screenPos);
214
216
  this.lastScreenCenter = pointer.screenPos;
217
+ return transformUpdate;
215
218
  }
216
219
  onPointerMove({ allPointers }) {
217
220
  this.transform ??= Viewport.transformBy(Mat33.identity);
218
- const lastTransform = this.transform;
221
+ let transformUpdate = Mat33.identity;
219
222
  if (allPointers.length === 2) {
220
- this.handleTwoFingerMove(allPointers);
223
+ transformUpdate = this.handleTwoFingerMove(allPointers);
221
224
  }
222
225
  else if (allPointers.length === 1) {
223
- this.handleOneFingerMove(allPointers[0]);
226
+ transformUpdate = this.handleOneFingerMove(allPointers[0]);
224
227
  }
225
- lastTransform.unapply(this.editor);
226
- this.transform.apply(this.editor);
228
+ Viewport.transformBy(transformUpdate).apply(this.editor);
227
229
  this.lastTimestamp = performance.now();
228
230
  }
229
231
  onPointerUp(event) {
@@ -303,8 +305,11 @@ export default class PanZoom extends BaseTool {
303
305
  const toCanvas = this.editor.viewport.screenToCanvasTransform;
304
306
  // Transform without including translation
305
307
  const translation = toCanvas.transformVec3(Vec3.of(-delta.x, -delta.y, 0));
306
- const pinchZoomScaleFactor = 1.03;
307
- const transformUpdate = Mat33.scaling2D(Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos).rightMul(Mat33.translation(translation));
308
+ let pinchAmount = delta.z;
309
+ // Clamp the magnitude of pinchAmount
310
+ pinchAmount = Math.atan(pinchAmount / 2) * 2;
311
+ const pinchZoomScaleFactor = 1.04;
312
+ const transformUpdate = Mat33.scaling2D(Math.max(0.4, Math.min(Math.pow(pinchZoomScaleFactor, -pinchAmount), 4)), canvasPos).rightMul(Mat33.translation(translation));
308
313
  this.updateTransform(transformUpdate, true);
309
314
  return true;
310
315
  }
@@ -0,0 +1,18 @@
1
+ import Editor from '../Editor';
2
+ import BaseTool from './BaseTool';
3
+ /**
4
+ * This tool, when enabled, renders scrollbars reflecting the current position
5
+ * of the view relative to the import/export area of the image.
6
+ *
7
+ * **Note**: These scrollbars are currently not draggable. This may change in
8
+ * a future release.
9
+ */
10
+ export default class ScrollbarTool extends BaseTool {
11
+ private editor;
12
+ private scrollbarOverlay;
13
+ private verticalScrollbar;
14
+ private horizontalScrollbar;
15
+ constructor(editor: Editor);
16
+ private fadeOutTimeout;
17
+ private updateScrollbars;
18
+ }