js-draw 1.2.1 → 1.3.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 (122) hide show
  1. package/README.md +30 -30
  2. package/dist/Editor.css +70 -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 +30 -10
  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 +60 -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/components/lib.d.ts +2 -2
  33. package/dist/cjs/components/lib.js +15 -2
  34. package/dist/cjs/lib.d.ts +2 -45
  35. package/dist/cjs/lib.js +2 -45
  36. package/dist/cjs/rendering/RenderablePathSpec.d.ts +1 -0
  37. package/dist/cjs/rendering/RenderablePathSpec.js +1 -0
  38. package/dist/cjs/rendering/RenderingStyle.d.ts +1 -0
  39. package/dist/cjs/rendering/lib.d.ts +1 -0
  40. package/dist/cjs/rendering/lib.js +5 -1
  41. package/dist/cjs/rendering/renderers/AbstractRenderer.js +1 -1
  42. package/dist/cjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  43. package/dist/cjs/shortcuts/KeyboardShortcutManager.js +2 -2
  44. package/dist/cjs/toolbar/localization.d.ts +1 -0
  45. package/dist/cjs/toolbar/localization.js +1 -0
  46. package/dist/cjs/toolbar/widgets/BaseWidget.js +5 -0
  47. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +54 -25
  48. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +8 -0
  49. package/dist/cjs/tools/PanZoom.js +13 -8
  50. package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
  51. package/dist/cjs/tools/ScrollbarTool.js +85 -0
  52. package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  53. package/dist/cjs/tools/ToolController.js +2 -0
  54. package/dist/cjs/types.d.ts +3 -1
  55. package/dist/cjs/util/adjustEditorThemeForContrast.js +1 -0
  56. package/dist/cjs/util/assertions.d.ts +4 -0
  57. package/dist/cjs/util/assertions.js +12 -1
  58. package/dist/cjs/version.js +1 -1
  59. package/dist/mjs/Editor.d.ts +73 -40
  60. package/dist/mjs/Editor.mjs +90 -24
  61. package/dist/mjs/EditorImage.d.ts +58 -6
  62. package/dist/mjs/EditorImage.mjs +313 -61
  63. package/dist/mjs/SVGLoader.d.ts +10 -4
  64. package/dist/mjs/SVGLoader.mjs +29 -9
  65. package/dist/mjs/UndoRedoHistory.d.ts +2 -2
  66. package/dist/mjs/UndoRedoHistory.mjs +4 -2
  67. package/dist/mjs/Viewport.d.ts +2 -1
  68. package/dist/mjs/Viewport.mjs +12 -3
  69. package/dist/mjs/commands/Command.d.ts +1 -0
  70. package/dist/mjs/commands/Command.mjs +1 -0
  71. package/dist/mjs/commands/Erase.mjs +1 -1
  72. package/dist/mjs/commands/SerializableCommand.d.ts +1 -1
  73. package/dist/mjs/commands/SerializableCommand.mjs +16 -2
  74. package/dist/mjs/commands/localization.d.ts +2 -0
  75. package/dist/mjs/commands/localization.mjs +2 -0
  76. package/dist/mjs/components/AbstractComponent.d.ts +38 -0
  77. package/dist/mjs/components/AbstractComponent.mjs +30 -0
  78. package/dist/mjs/components/BackgroundComponent.d.ts +10 -1
  79. package/dist/mjs/components/BackgroundComponent.mjs +37 -6
  80. package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +2 -1
  81. package/dist/mjs/components/SVGGlobalAttributesObject.mjs +7 -1
  82. package/dist/mjs/components/Stroke.d.ts +1 -0
  83. package/dist/mjs/components/Stroke.mjs +44 -0
  84. package/dist/mjs/components/UnknownSVGObject.d.ts +2 -1
  85. package/dist/mjs/components/UnknownSVGObject.mjs +7 -1
  86. package/dist/mjs/components/lib.d.ts +2 -2
  87. package/dist/mjs/components/lib.mjs +2 -2
  88. package/dist/mjs/lib.d.ts +2 -45
  89. package/dist/mjs/lib.mjs +2 -45
  90. package/dist/mjs/rendering/RenderablePathSpec.d.ts +1 -0
  91. package/dist/mjs/rendering/RenderablePathSpec.mjs +1 -0
  92. package/dist/mjs/rendering/RenderingStyle.d.ts +1 -0
  93. package/dist/mjs/rendering/lib.d.ts +1 -0
  94. package/dist/mjs/rendering/lib.mjs +1 -0
  95. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +1 -1
  96. package/dist/mjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  97. package/dist/mjs/shortcuts/KeyboardShortcutManager.mjs +2 -2
  98. package/dist/mjs/toolbar/localization.d.ts +1 -0
  99. package/dist/mjs/toolbar/localization.mjs +1 -0
  100. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +5 -0
  101. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -25
  102. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +8 -0
  103. package/dist/mjs/tools/PanZoom.mjs +13 -8
  104. package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
  105. package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
  106. package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  107. package/dist/mjs/tools/ToolController.mjs +2 -0
  108. package/dist/mjs/types.d.ts +3 -1
  109. package/dist/mjs/util/adjustEditorThemeForContrast.mjs +1 -0
  110. package/dist/mjs/util/assertions.d.ts +4 -0
  111. package/dist/mjs/util/assertions.mjs +10 -0
  112. package/dist/mjs/version.mjs +1 -1
  113. package/package.json +3 -4
  114. package/src/Editor.scss +8 -0
  115. package/src/dialogs/dialogs.scss +2 -1
  116. package/src/toolbar/AbstractToolbar.scss +3 -0
  117. package/src/toolbar/EdgeToolbar.scss +4 -1
  118. package/src/toolbar/widgets/DocumentPropertiesWidget.scss +12 -0
  119. package/src/toolbar/widgets/components/makeGridSelector.scss +6 -1
  120. package/src/tools/ScrollbarTool.scss +57 -0
  121. package/src/tools/{SoundUITool.css → SoundUITool.scss} +4 -0
  122. package/src/tools/tools.scss +2 -1
@@ -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
  }
@@ -10,7 +10,7 @@ import TextComponent from './TextComponent';
10
10
  import ImageComponent from './ImageComponent';
11
11
  import RestyleableComponent from './RestylableComponent';
12
12
  import { createRestyleComponentCommand, isRestylableComponent, ComponentStyle as RestyleableComponentStyle } from './RestylableComponent';
13
- import BackgroundComponent from './BackgroundComponent';
13
+ import BackgroundComponent, { BackgroundType } from './BackgroundComponent';
14
14
  export { Stroke, RestyleableComponent, createRestyleComponentCommand, isRestylableComponent, RestyleableComponentStyle, TextComponent,
15
15
  /** @deprecated use {@link TextComponent} */
16
- TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
16
+ TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, BackgroundType as BackgroundComponentBackgroundType, ImageComponent, };
@@ -9,7 +9,7 @@ import Stroke from './Stroke.mjs';
9
9
  import TextComponent from './TextComponent.mjs';
10
10
  import ImageComponent from './ImageComponent.mjs';
11
11
  import { createRestyleComponentCommand, isRestylableComponent } from './RestylableComponent.mjs';
12
- import BackgroundComponent from './BackgroundComponent.mjs';
12
+ import BackgroundComponent, { BackgroundType } from './BackgroundComponent.mjs';
13
13
  export { Stroke, createRestyleComponentCommand, isRestylableComponent, TextComponent,
14
14
  /** @deprecated use {@link TextComponent} */
15
- TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, ImageComponent, };
15
+ TextComponent as Text, Stroke as StrokeComponent, BackgroundComponent, BackgroundType as BackgroundComponentBackgroundType, ImageComponent, };
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}
@@ -6,6 +6,7 @@ interface RenderablePathSpec {
6
6
  style: RenderingStyle;
7
7
  path?: Path;
8
8
  }
9
+ /** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
9
10
  export declare const pathFromRenderable: (renderable: RenderablePathSpec) => Path;
10
11
  export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpec;
11
12
  /**
@@ -1,4 +1,5 @@
1
1
  import { Mat33, Path, PathCommandType } from '@js-draw/math';
2
+ /** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
2
3
  export const pathFromRenderable = (renderable) => {
3
4
  if (renderable.path) {
4
5
  return renderable.path;
@@ -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
  }
@@ -5,3 +5,4 @@ export { default as CanvasRenderer } from './renderers/CanvasRenderer';
5
5
  export { default as Display, RenderingMode } from './Display';
6
6
  export { default as TextRenderingStyle } from './TextRenderingStyle';
7
7
  export { default as RenderingStyle } from './RenderingStyle';
8
+ export { pathToRenderable, pathFromRenderable, visualEquivalent as pathVisualEquivalent, default as RenderablePathSpec, } from './RenderablePathSpec';
@@ -3,3 +3,4 @@ export { default as DummyRenderer } from './renderers/DummyRenderer.mjs';
3
3
  export { default as SVGRenderer } from './renderers/SVGRenderer.mjs';
4
4
  export { default as CanvasRenderer } from './renderers/CanvasRenderer.mjs';
5
5
  export { default as Display, RenderingMode } from './Display.mjs';
6
+ export { pathToRenderable, pathFromRenderable, visualEquivalent as pathVisualEquivalent, } from './RenderablePathSpec.mjs';
@@ -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;
@@ -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
+ }
@@ -0,0 +1,79 @@
1
+ import { Rect2 } from '@js-draw/math';
2
+ import { EditorEventType } from '../types.mjs';
3
+ import BaseTool from './BaseTool.mjs';
4
+ /**
5
+ * This tool, when enabled, renders scrollbars reflecting the current position
6
+ * of the view relative to the import/export area of the image.
7
+ *
8
+ * **Note**: These scrollbars are currently not draggable. This may change in
9
+ * a future release.
10
+ */
11
+ export default class ScrollbarTool extends BaseTool {
12
+ constructor(editor) {
13
+ super(editor.notifier, 'scrollbar');
14
+ this.editor = editor;
15
+ this.fadeOutTimeout = null;
16
+ this.scrollbarOverlay = document.createElement('div');
17
+ this.scrollbarOverlay.classList.add('ScrollbarTool-overlay');
18
+ this.verticalScrollbar = document.createElement('div');
19
+ this.verticalScrollbar.classList.add('vertical-scrollbar');
20
+ this.horizontalScrollbar = document.createElement('div');
21
+ this.horizontalScrollbar.classList.add('horizontal-scrollbar');
22
+ this.scrollbarOverlay.replaceChildren(this.verticalScrollbar, this.horizontalScrollbar);
23
+ let overlay = null;
24
+ let viewportListener = null;
25
+ this.enabledValue().onUpdateAndNow(enabled => {
26
+ overlay?.remove();
27
+ viewportListener?.remove();
28
+ viewportListener = null;
29
+ overlay = null;
30
+ if (enabled) {
31
+ viewportListener = editor.notifier.on(EditorEventType.ViewportChanged, _event => {
32
+ this.updateScrollbars();
33
+ });
34
+ this.updateScrollbars();
35
+ overlay = editor.createHTMLOverlay(this.scrollbarOverlay);
36
+ }
37
+ });
38
+ }
39
+ updateScrollbars() {
40
+ const viewport = this.editor.viewport;
41
+ const screenSize = viewport.getScreenRectSize();
42
+ const screenRect = new Rect2(0, 0, screenSize.x, screenSize.y);
43
+ const imageRect = this.editor.getImportExportRect()
44
+ // The scrollbars are positioned in screen coordinates, so the exportRect also needs
45
+ // to be in screen coordinates
46
+ .transformedBoundingBox(viewport.canvasToScreenTransform)
47
+ // If the screenRect is outside of the exportRect, expand the image rectangle
48
+ .union(screenRect);
49
+ const scrollbarWidth = screenRect.width / imageRect.width * screenSize.x;
50
+ const scrollbarHeight = screenRect.height / imageRect.height * screenSize.y;
51
+ const scrollbarX = (screenRect.x - imageRect.x) / imageRect.width * (screenSize.x);
52
+ const scrollbarY = (screenRect.y - imageRect.y) / imageRect.height * (screenSize.y);
53
+ this.horizontalScrollbar.style.width = `${scrollbarWidth}px`;
54
+ this.verticalScrollbar.style.height = `${scrollbarHeight}px`;
55
+ this.horizontalScrollbar.style.marginLeft = `${scrollbarX}px`;
56
+ this.verticalScrollbar.style.marginTop = `${scrollbarY}px`;
57
+ // Style the scrollbars differently when there's no scroll (all content visible)
58
+ const handleNoScrollStyling = (scrollbar, size, fillSize) => {
59
+ const fillsWindowClass = 'represents-no-scroll';
60
+ if (Math.abs(size - fillSize) < 1e-8) {
61
+ scrollbar.classList.add(fillsWindowClass);
62
+ }
63
+ else {
64
+ scrollbar.classList.remove(fillsWindowClass);
65
+ }
66
+ };
67
+ handleNoScrollStyling(this.horizontalScrollbar, scrollbarWidth, screenSize.x);
68
+ handleNoScrollStyling(this.verticalScrollbar, scrollbarHeight, screenSize.y);
69
+ // Fade out after a delay.
70
+ if (this.fadeOutTimeout !== null) {
71
+ clearTimeout(this.fadeOutTimeout);
72
+ }
73
+ const fadeOutDelay = 3000;
74
+ this.fadeOutTimeout = setTimeout(() => {
75
+ this.scrollbarOverlay.classList.remove('just-updated');
76
+ }, fadeOutDelay);
77
+ this.scrollbarOverlay.classList.add('just-updated');
78
+ }
79
+ }
@@ -18,6 +18,7 @@ import SoundUITool from './SoundUITool.mjs';
18
18
  import { InputEvtType } from '../inputEvents.mjs';
19
19
  import InputPipeline from './InputFilter/InputPipeline.mjs';
20
20
  import InputStabilizer from './InputFilter/InputStabilizer.mjs';
21
+ import ScrollbarTool from './ScrollbarTool.mjs';
21
22
  export default class ToolController {
22
23
  /** @internal */
23
24
  constructor(editor, localization) {
@@ -51,6 +52,7 @@ export default class ToolController {
51
52
  const soundExplorer = new SoundUITool(editor, localization.soundExplorer);
52
53
  soundExplorer.setEnabled(false);
53
54
  this.tools = [
55
+ new ScrollbarTool(editor),
54
56
  new PipetteTool(editor, localization.pipetteTool),
55
57
  soundExplorer,
56
58
  panZoomTool,