js-draw 1.2.2 → 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 (107) 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 +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/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/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  37. package/dist/cjs/shortcuts/KeyboardShortcutManager.js +2 -2
  38. package/dist/cjs/toolbar/localization.d.ts +1 -0
  39. package/dist/cjs/toolbar/localization.js +1 -0
  40. package/dist/cjs/toolbar/widgets/BaseWidget.js +5 -0
  41. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +54 -25
  42. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +8 -0
  43. package/dist/cjs/tools/PanZoom.js +13 -8
  44. package/dist/cjs/tools/ScrollbarTool.d.ts +18 -0
  45. package/dist/cjs/tools/ScrollbarTool.js +85 -0
  46. package/dist/cjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  47. package/dist/cjs/tools/ToolController.js +2 -0
  48. package/dist/cjs/types.d.ts +3 -1
  49. package/dist/cjs/util/assertions.d.ts +4 -0
  50. package/dist/cjs/util/assertions.js +12 -1
  51. package/dist/cjs/version.js +1 -1
  52. package/dist/mjs/Editor.d.ts +73 -40
  53. package/dist/mjs/Editor.mjs +90 -24
  54. package/dist/mjs/EditorImage.d.ts +58 -6
  55. package/dist/mjs/EditorImage.mjs +313 -61
  56. package/dist/mjs/SVGLoader.d.ts +10 -4
  57. package/dist/mjs/SVGLoader.mjs +29 -9
  58. package/dist/mjs/UndoRedoHistory.d.ts +2 -2
  59. package/dist/mjs/UndoRedoHistory.mjs +4 -2
  60. package/dist/mjs/Viewport.d.ts +2 -1
  61. package/dist/mjs/Viewport.mjs +12 -3
  62. package/dist/mjs/commands/Command.d.ts +1 -0
  63. package/dist/mjs/commands/Command.mjs +1 -0
  64. package/dist/mjs/commands/Erase.mjs +1 -1
  65. package/dist/mjs/commands/SerializableCommand.d.ts +1 -1
  66. package/dist/mjs/commands/SerializableCommand.mjs +16 -2
  67. package/dist/mjs/commands/localization.d.ts +2 -0
  68. package/dist/mjs/commands/localization.mjs +2 -0
  69. package/dist/mjs/components/AbstractComponent.d.ts +38 -0
  70. package/dist/mjs/components/AbstractComponent.mjs +30 -0
  71. package/dist/mjs/components/BackgroundComponent.d.ts +10 -1
  72. package/dist/mjs/components/BackgroundComponent.mjs +37 -6
  73. package/dist/mjs/components/SVGGlobalAttributesObject.d.ts +2 -1
  74. package/dist/mjs/components/SVGGlobalAttributesObject.mjs +7 -1
  75. package/dist/mjs/components/Stroke.d.ts +1 -0
  76. package/dist/mjs/components/Stroke.mjs +44 -0
  77. package/dist/mjs/components/UnknownSVGObject.d.ts +2 -1
  78. package/dist/mjs/components/UnknownSVGObject.mjs +7 -1
  79. package/dist/mjs/lib.d.ts +2 -45
  80. package/dist/mjs/lib.mjs +2 -45
  81. package/dist/mjs/rendering/RenderingStyle.d.ts +1 -0
  82. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +1 -1
  83. package/dist/mjs/shortcuts/KeyboardShortcutManager.d.ts +2 -2
  84. package/dist/mjs/shortcuts/KeyboardShortcutManager.mjs +2 -2
  85. package/dist/mjs/toolbar/localization.d.ts +1 -0
  86. package/dist/mjs/toolbar/localization.mjs +1 -0
  87. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +5 -0
  88. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +54 -25
  89. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +8 -0
  90. package/dist/mjs/tools/PanZoom.mjs +13 -8
  91. package/dist/mjs/tools/ScrollbarTool.d.ts +18 -0
  92. package/dist/mjs/tools/ScrollbarTool.mjs +79 -0
  93. package/dist/mjs/tools/SelectionTool/SelectionTool.selecting.test.d.ts +1 -0
  94. package/dist/mjs/tools/ToolController.mjs +2 -0
  95. package/dist/mjs/types.d.ts +3 -1
  96. package/dist/mjs/util/assertions.d.ts +4 -0
  97. package/dist/mjs/util/assertions.mjs +10 -0
  98. package/dist/mjs/version.mjs +1 -1
  99. package/package.json +3 -4
  100. package/src/Editor.scss +8 -0
  101. package/src/dialogs/dialogs.scss +2 -1
  102. package/src/toolbar/EdgeToolbar.scss +4 -1
  103. package/src/toolbar/widgets/DocumentPropertiesWidget.scss +12 -0
  104. package/src/toolbar/widgets/components/makeGridSelector.scss +1 -1
  105. package/src/tools/ScrollbarTool.scss +57 -0
  106. package/src/tools/{SoundUITool.css → SoundUITool.scss} +4 -0
  107. package/src/tools/tools.scss +2 -1
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;
@@ -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,
@@ -82,7 +82,9 @@ export interface ToolbarDropdownShownEvent {
82
82
  export type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | CommandDoneEvent | CommandUndoneEvent | SelectionUpdated | ColorPickerToggled | ColorPickerColorSelected | ToolbarDropdownShownEvent;
83
83
  export type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null | void;
84
84
  export type ComponentAddedListener = (component: AbstractComponent) => Promise<void> | void;
85
- export type OnDetermineExportRectListener = (exportRect: Rect2) => void;
85
+ export type OnDetermineExportRectListener = (exportRect: Rect2, options?: {
86
+ autoresize: boolean;
87
+ }) => void;
86
88
  export interface ImageLoader {
87
89
  start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
88
90
  }
@@ -21,3 +21,7 @@ export declare const assertIsNumber: (value: any, allowNaN?: boolean) => value i
21
21
  * Throws if any of `values` is not of type number.
22
22
  */
23
23
  export declare const assertIsNumberArray: (values: any[], allowNaN?: boolean) => values is number[];
24
+ /**
25
+ * Throws an exception if `typeof value` is not a boolean.
26
+ */
27
+ export declare const assertIsBoolean: (value: any) => value is boolean;
@@ -43,3 +43,13 @@ export const assertIsNumberArray = (values, allowNaN = false) => {
43
43
  }
44
44
  return true;
45
45
  };
46
+ /**
47
+ * Throws an exception if `typeof value` is not a boolean.
48
+ */
49
+ export const assertIsBoolean = (value) => {
50
+ if (typeof value !== 'boolean') {
51
+ throw new Error('Given value is not a boolean');
52
+ // return false;
53
+ }
54
+ return true;
55
+ };
@@ -1,3 +1,3 @@
1
1
  export default {
2
- number: '1.2.2',
2
+ number: '1.3.0',
3
3
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "types": "./dist/mjs/lib.d.ts",
6
6
  "main": "./dist/cjs/lib.js",
@@ -64,12 +64,11 @@
64
64
  "postpack": "ts-node tools/copyREADME.ts revert"
65
65
  },
66
66
  "dependencies": {
67
- "@js-draw/math": "^1.2.2",
67
+ "@js-draw/math": "^1.3.0",
68
68
  "@melloware/coloris": "0.21.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@js-draw/build-tool": "^1.0.2",
72
- "@types/bezier-js": "4.1.0",
73
72
  "@types/jest": "29.5.3",
74
73
  "@types/jsdom": "21.1.1"
75
74
  },
@@ -87,5 +86,5 @@
87
86
  "freehand",
88
87
  "svg"
89
88
  ],
90
- "gitHead": "ced98ae289f299c44b54515571d0087aa6d32cdc"
89
+ "gitHead": "46b3d8f819f8e083f6e3e1d01e027e4311355456"
91
90
  }
package/src/Editor.scss CHANGED
@@ -136,6 +136,14 @@
136
136
  z-index: 5;
137
137
  }
138
138
 
139
+ // TODO: Apply this change during a future major release.
140
+ // So as not to change the position of other overlays, all overlays should have
141
+ // 0 height.
142
+ // Uses the alternate overlay class name to decrease specificity.
143
+ // .js-draw-editor-overlay {
144
+ // //height: 0;
145
+ // }
146
+
139
147
  @media print {
140
148
  .imageEditorContainer .loadingMessage {
141
149
  display: none;
@@ -1,7 +1,8 @@
1
1
 
2
2
  @use './makeAboutDialog.scss';
3
3
 
4
- .dialog-container {
4
+ // Repeat to increase specificity -- dialog containers are often overlays
5
+ .dialog-container.dialog-container {
5
6
  background-color: var( --background-color-transparent);
6
7
 
7
8
  backdrop-filter: blur(5px);
@@ -480,6 +480,8 @@
480
480
  .toolbar-spacedList {
481
481
  box-sizing: border-box;
482
482
 
483
+ --align-items-to-x: 105px;
484
+
483
485
  & > div {
484
486
  display: flex;
485
487
  align-items: center;
@@ -491,7 +493,7 @@
491
493
  // Align inputs (assumes labels come first)
492
494
  & > label {
493
495
  padding-right: 35px;
494
- min-width: 105px;
496
+ min-width: var(--align-items-to-x);
495
497
  flex-shrink: 1;
496
498
  box-sizing: border-box;
497
499
  }
@@ -499,6 +501,7 @@
499
501
  & > input[type="checkbox"] {
500
502
  width: 20px;
501
503
  height: 20px;
504
+ margin-left: 0;
502
505
  }
503
506
 
504
507
  // If checkboxes have flex-grow, the checkbox region can become centered.
@@ -4,4 +4,16 @@
4
4
  width: 100%;
5
5
  text-align: end;
6
6
  }
7
+
8
+ & > * {
9
+ --align-items-to-x: 120px;
10
+ }
11
+
12
+ .js-draw-size-input-row.js-draw-size-input-row {
13
+ display: flex;
14
+
15
+ &.size-input-row--automatic-size {
16
+ display: none;
17
+ }
18
+ }
7
19
  }
@@ -20,7 +20,7 @@
20
20
  flex-shrink: 1;
21
21
  margin: 2px;
22
22
 
23
- &:focus-within {
23
+ &.focus-visible {
24
24
  outline: 2px solid var(--foreground-color-1);
25
25
  }
26
26
 
@@ -0,0 +1,57 @@
1
+
2
+ .ScrollbarTool-overlay {
3
+ width: 0;
4
+ height: 0;
5
+ overflow: visible;
6
+
7
+ $visible-opacity: 0.2;
8
+ opacity: $visible-opacity;
9
+ pointer-events: none;
10
+
11
+ --fade-out-animation: 1s ease 0s fade-out;
12
+
13
+ @media (prefers-reduced-motion: reduce) {
14
+ --fade-out-animation: none !important;
15
+ }
16
+
17
+ @keyframes fade-out {
18
+ from { opacity: $visible-opacity; }
19
+ to { opacity: 0; }
20
+ }
21
+
22
+ &:not(.just-updated) {
23
+ animation: var(--fade-out-animation);
24
+ opacity: 0;
25
+ }
26
+
27
+ --scrollbar-size: 3px;
28
+
29
+ .vertical-scrollbar, .horizontal-scrollbar {
30
+ width: var(--scrollbar-size);
31
+ height: var(--scrollbar-size);
32
+
33
+ min-width: var(--scrollbar-size);
34
+ min-height: var(--scrollbar-size);
35
+
36
+ background-color: var(--foreground-color-1);
37
+ border-radius: var(--scrollbar-size);
38
+ position: absolute;
39
+
40
+ &.represents-no-scroll {
41
+ animation: var(--fade-out-animation);
42
+ opacity: 0;
43
+ }
44
+ }
45
+
46
+ &:not(.scrollbar-left) {
47
+ .vertical-scrollbar {
48
+ margin-left: calc(var(--editor-current-display-width-px) - var(--scrollbar-size));
49
+ }
50
+ }
51
+
52
+ &:not(.scrollbar-top) {
53
+ .horizontal-scrollbar {
54
+ margin-top: calc(var(--editor-current-display-height-px) - var(--scrollbar-size));
55
+ }
56
+ }
57
+ }