js-draw 1.9.1 → 1.11.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 (145) hide show
  1. package/dist/Editor.css +48 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +41 -0
  5. package/dist/cjs/Editor.js +9 -0
  6. package/dist/cjs/Pointer.js +1 -1
  7. package/dist/cjs/commands/Erase.d.ts +22 -2
  8. package/dist/cjs/commands/Erase.js +22 -2
  9. package/dist/cjs/commands/invertCommand.js +5 -0
  10. package/dist/cjs/commands/uniteCommands.d.ts +36 -0
  11. package/dist/cjs/commands/uniteCommands.js +36 -0
  12. package/dist/cjs/components/AbstractComponent.d.ts +8 -0
  13. package/dist/cjs/components/AbstractComponent.js +28 -8
  14. package/dist/cjs/components/ImageComponent.d.ts +12 -0
  15. package/dist/cjs/components/ImageComponent.js +16 -9
  16. package/dist/cjs/components/Stroke.d.ts +16 -2
  17. package/dist/cjs/components/Stroke.js +17 -1
  18. package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
  19. package/dist/cjs/components/builders/CircleBuilder.js +3 -3
  20. package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
  21. package/dist/cjs/components/builders/LineBuilder.js +3 -3
  22. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
  23. package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
  24. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  25. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
  26. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  27. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
  28. package/dist/cjs/components/builders/types.d.ts +12 -0
  29. package/dist/cjs/image/EditorImage.d.ts +32 -1
  30. package/dist/cjs/image/EditorImage.js +32 -1
  31. package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
  32. package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
  33. package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
  34. package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
  35. package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
  36. package/dist/cjs/toolbar/IconProvider.js +17 -0
  37. package/dist/cjs/toolbar/localization.d.ts +3 -0
  38. package/dist/cjs/toolbar/localization.js +4 -1
  39. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
  40. package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  41. package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
  42. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  43. package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
  44. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  45. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
  46. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  47. package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
  48. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  49. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  50. package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
  51. package/dist/cjs/tools/Pen.d.ts +9 -0
  52. package/dist/cjs/tools/Pen.js +82 -3
  53. package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
  54. package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
  55. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  56. package/dist/cjs/tools/SelectionTool/SelectionTool.js +19 -1
  57. package/dist/cjs/tools/TextTool.js +5 -1
  58. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  59. package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
  60. package/dist/cjs/tools/keybindings.d.ts +1 -0
  61. package/dist/cjs/tools/keybindings.js +3 -1
  62. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
  63. package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
  64. package/dist/cjs/util/ReactiveValue.d.ts +2 -0
  65. package/dist/cjs/util/ReactiveValue.js +2 -0
  66. package/dist/cjs/util/lib.d.ts +1 -0
  67. package/dist/cjs/util/lib.js +4 -1
  68. package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
  69. package/dist/cjs/util/waitForImageLoaded.js +12 -0
  70. package/dist/cjs/version.js +1 -1
  71. package/dist/mjs/Editor.d.ts +41 -0
  72. package/dist/mjs/Editor.mjs +9 -0
  73. package/dist/mjs/Pointer.mjs +1 -1
  74. package/dist/mjs/commands/Erase.d.ts +22 -2
  75. package/dist/mjs/commands/Erase.mjs +22 -2
  76. package/dist/mjs/commands/invertCommand.mjs +5 -0
  77. package/dist/mjs/commands/uniteCommands.d.ts +36 -0
  78. package/dist/mjs/commands/uniteCommands.mjs +36 -0
  79. package/dist/mjs/components/AbstractComponent.d.ts +8 -0
  80. package/dist/mjs/components/AbstractComponent.mjs +28 -8
  81. package/dist/mjs/components/ImageComponent.d.ts +12 -0
  82. package/dist/mjs/components/ImageComponent.mjs +16 -9
  83. package/dist/mjs/components/Stroke.d.ts +16 -2
  84. package/dist/mjs/components/Stroke.mjs +17 -1
  85. package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
  86. package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
  87. package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
  88. package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
  89. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
  90. package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
  91. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  92. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
  93. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  94. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
  95. package/dist/mjs/components/builders/types.d.ts +12 -0
  96. package/dist/mjs/image/EditorImage.d.ts +32 -1
  97. package/dist/mjs/image/EditorImage.mjs +32 -1
  98. package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
  99. package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
  100. package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
  101. package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
  102. package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
  103. package/dist/mjs/toolbar/IconProvider.mjs +17 -0
  104. package/dist/mjs/toolbar/localization.d.ts +3 -0
  105. package/dist/mjs/toolbar/localization.mjs +4 -1
  106. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
  107. package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  108. package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
  109. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  110. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
  111. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  112. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
  113. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  114. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
  115. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  116. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  117. package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
  118. package/dist/mjs/tools/Pen.d.ts +9 -0
  119. package/dist/mjs/tools/Pen.mjs +82 -3
  120. package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
  121. package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
  122. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  123. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +20 -2
  124. package/dist/mjs/tools/TextTool.mjs +5 -1
  125. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  126. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
  127. package/dist/mjs/tools/keybindings.d.ts +1 -0
  128. package/dist/mjs/tools/keybindings.mjs +2 -0
  129. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
  130. package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
  131. package/dist/mjs/util/ReactiveValue.d.ts +2 -0
  132. package/dist/mjs/util/ReactiveValue.mjs +2 -0
  133. package/dist/mjs/util/lib.d.ts +1 -0
  134. package/dist/mjs/util/lib.mjs +1 -0
  135. package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
  136. package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
  137. package/dist/mjs/version.mjs +1 -1
  138. package/package.json +3 -3
  139. package/src/Editor.scss +7 -0
  140. package/src/toolbar/AbstractToolbar.scss +20 -0
  141. package/src/toolbar/toolbar.scss +1 -1
  142. package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
  143. package/src/toolbar/widgets/PenToolWidget.scss +33 -0
  144. package/src/tools/SelectionTool/SelectionTool.scss +6 -0
  145. package/src/toolbar/widgets/PenToolWidget.css +0 -2
@@ -0,0 +1,22 @@
1
+ import Pointer from '../../Pointer';
2
+ interface Config {
3
+ maxSpeed: number;
4
+ minTimeSeconds: number;
5
+ maxRadius: number;
6
+ }
7
+ type OnStationaryCallback = (lastPointer: Pointer) => void;
8
+ export default class StationaryPenDetector {
9
+ private config;
10
+ private onStationary;
11
+ private stationaryStartPointer;
12
+ private lastPointer;
13
+ private averageVelocity;
14
+ private timeout;
15
+ constructor(startPointer: Pointer, config: Config, onStationary: OnStationaryCallback);
16
+ onPointerMove(currentPointer: Pointer): boolean | undefined;
17
+ onPointerUp(pointer: Pointer): void;
18
+ destroy(): void;
19
+ private cancelStationaryTimeout;
20
+ private setStationaryTimeout;
21
+ }
22
+ export {};
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const math_1 = require("@js-draw/math");
4
+ class StationaryPenDetector {
5
+ // Only handles one pen. As such, `startPointer` should be the same device/finger
6
+ // as `updatedPointer` in `onPointerMove`.
7
+ //
8
+ // A new `StationaryPenDetector` should be created for each gesture.
9
+ constructor(startPointer, config, onStationary) {
10
+ this.config = config;
11
+ this.onStationary = onStationary;
12
+ this.timeout = null;
13
+ this.stationaryStartPointer = startPointer;
14
+ this.lastPointer = startPointer;
15
+ this.averageVelocity = math_1.Vec2.zero;
16
+ }
17
+ // Returns true if stationary
18
+ onPointerMove(currentPointer) {
19
+ if (!this.stationaryStartPointer) {
20
+ // Destoroyed
21
+ return;
22
+ }
23
+ if (currentPointer.id !== this.stationaryStartPointer.id) {
24
+ return false;
25
+ }
26
+ // dx: "Δx" Displacement from last.
27
+ const dxFromLast = currentPointer.screenPos.minus(this.lastPointer.screenPos);
28
+ const dxFromStationaryStart = currentPointer.screenPos.minus(this.stationaryStartPointer.screenPos);
29
+ // dt: Delta time:
30
+ // /1000: Convert to s.
31
+ let dtFromLast = (currentPointer.timeStamp - this.lastPointer.timeStamp) / 1000; // s
32
+ // Don't divide by zero
33
+ if (dtFromLast === 0) {
34
+ dtFromLast = 1;
35
+ }
36
+ const currentVelocity = dxFromLast.times(1 / dtFromLast); // px/s
37
+ // Slight smoothing of the velocity to prevent input jitter from affecting the
38
+ // velocity too significantly.
39
+ this.averageVelocity = this.averageVelocity.lerp(currentVelocity, 0.5); // px/s
40
+ const dtFromStart = currentPointer.timeStamp - this.stationaryStartPointer.timeStamp; // ms
41
+ // If not stationary
42
+ if (dxFromStationaryStart.length() > this.config.maxRadius
43
+ || this.averageVelocity.length() > this.config.maxSpeed
44
+ || dtFromStart < this.config.minTimeSeconds) {
45
+ this.stationaryStartPointer = currentPointer;
46
+ this.lastPointer = currentPointer;
47
+ this.setStationaryTimeout(this.config.minTimeSeconds * 1000);
48
+ return false;
49
+ }
50
+ const stationaryTimeoutMs = this.config.minTimeSeconds * 1000 - dtFromStart;
51
+ this.lastPointer = currentPointer;
52
+ return stationaryTimeoutMs <= 0;
53
+ }
54
+ onPointerUp(pointer) {
55
+ if (pointer.id !== this.stationaryStartPointer?.id) {
56
+ this.cancelStationaryTimeout();
57
+ }
58
+ }
59
+ destroy() {
60
+ this.cancelStationaryTimeout();
61
+ this.stationaryStartPointer = null;
62
+ }
63
+ cancelStationaryTimeout() {
64
+ if (this.timeout !== null) {
65
+ clearTimeout(this.timeout);
66
+ this.timeout = null;
67
+ }
68
+ }
69
+ setStationaryTimeout(timeoutMs) {
70
+ if (this.timeout !== null) {
71
+ return;
72
+ }
73
+ if (timeoutMs <= 0) {
74
+ this.onStationary(this.lastPointer);
75
+ }
76
+ else {
77
+ this.timeout = setTimeout(() => {
78
+ this.timeout = null;
79
+ if (!this.stationaryStartPointer) {
80
+ // Destroyed
81
+ return;
82
+ }
83
+ const timeSinceStationaryStart = performance.now() - this.stationaryStartPointer.timeStamp;
84
+ const timeRemaining = this.config.minTimeSeconds * 1000 - timeSinceStationaryStart;
85
+ if (timeRemaining <= 0) {
86
+ this.onStationary(this.lastPointer);
87
+ }
88
+ else {
89
+ this.setStationaryTimeout(timeRemaining);
90
+ }
91
+ }, timeoutMs);
92
+ }
93
+ }
94
+ }
95
+ exports.default = StationaryPenDetector;
@@ -13,6 +13,8 @@ type UpdateCallback<T> = (value: T) => void;
13
13
  *
14
14
  * Static methods in the `ReactiveValue` and `MutableReactiveValue` classes are
15
15
  * constructors (e.g. `fromImmutable`).
16
+ *
17
+ * Avoid extending this class from an external library, as that may not be stable.
16
18
  */
17
19
  export declare abstract class ReactiveValue<T> {
18
20
  /**
@@ -34,6 +34,8 @@ const noOpSetUpdateListener = () => {
34
34
  *
35
35
  * Static methods in the `ReactiveValue` and `MutableReactiveValue` classes are
36
36
  * constructors (e.g. `fromImmutable`).
37
+ *
38
+ * Avoid extending this class from an external library, as that may not be stable.
37
39
  */
38
40
  class ReactiveValue {
39
41
  /** Creates a `ReactiveValue` with an initial value, `initialValue`. */
@@ -1 +1,2 @@
1
1
  export { default as adjustEditorThemeForContrast } from './adjustEditorThemeForContrast';
2
+ export { ReactiveValue, MutableReactiveValue } from './ReactiveValue';
@@ -3,6 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.adjustEditorThemeForContrast = void 0;
6
+ exports.MutableReactiveValue = exports.ReactiveValue = exports.adjustEditorThemeForContrast = void 0;
7
7
  var adjustEditorThemeForContrast_1 = require("./adjustEditorThemeForContrast");
8
8
  Object.defineProperty(exports, "adjustEditorThemeForContrast", { enumerable: true, get: function () { return __importDefault(adjustEditorThemeForContrast_1).default; } });
9
+ var ReactiveValue_1 = require("./ReactiveValue");
10
+ Object.defineProperty(exports, "ReactiveValue", { enumerable: true, get: function () { return ReactiveValue_1.ReactiveValue; } });
11
+ Object.defineProperty(exports, "MutableReactiveValue", { enumerable: true, get: function () { return ReactiveValue_1.MutableReactiveValue; } });
@@ -0,0 +1,2 @@
1
+ declare const waitForImageLoad: (image: HTMLImageElement) => Promise<void>;
2
+ export default waitForImageLoad;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const waitForImageLoad = async (image) => {
4
+ if (!image.complete) {
5
+ await new Promise((resolve, reject) => {
6
+ image.onload = event => resolve(event);
7
+ image.onerror = event => reject(event);
8
+ image.onabort = event => reject(event);
9
+ });
10
+ }
11
+ };
12
+ exports.default = waitForImageLoad;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.9.1',
4
+ number: '1.11.0',
5
5
  };
@@ -156,6 +156,38 @@ export declare class Editor {
156
156
  readonly toolController: ToolController;
157
157
  /**
158
158
  * Global event dispatcher/subscriber.
159
+ *
160
+ * @example
161
+ *
162
+ * ```ts,runnable
163
+ * import { Editor, EditorEventType, SerializableCommand } from 'js-draw';
164
+ *
165
+ * // Create a minimal editor
166
+ * const editor = new Editor(document.body);
167
+ * editor.addToolbar();
168
+ *
169
+ * // Create a place to show text output
170
+ * const log = document.createElement('textarea');
171
+ * document.body.appendChild(log);
172
+ * log.style.width = '100%';
173
+ * log.style.height = '200px';
174
+ *
175
+ * // Listen for CommandDone events (there's also a CommandUndone)
176
+ * editor.notifier.on(EditorEventType.CommandDone, event => {
177
+ * // Type narrowing for TypeScript -- event will always be of kind CommandDone,
178
+ * // but TypeScript doesn't know this.
179
+ * if (event.kind !== EditorEventType.CommandDone) return;
180
+ *
181
+ * log.value = `Command done ${event.command.description(editor, editor.localization)}\n`;
182
+ *
183
+ * if (event.command instanceof SerializableCommand) {
184
+ * log.value += `serializes to: ${JSON.stringify(event.command.serialize())}`;
185
+ * }
186
+ * });
187
+ *
188
+ * // Dispatch an initial command to trigger the event listener for the first time
189
+ * editor.dispatch(editor.image.setAutoresizeEnabled(true));
190
+ * ```
159
191
  */
160
192
  readonly notifier: EditorNotifier;
161
193
  private loadingWarning;
@@ -395,6 +427,12 @@ export declare class Editor {
395
427
  * @see {@link sendPenEvent} {@link sendTouchEvent}
396
428
  */
397
429
  sendPenEvent(eventType: InputEvtType.PointerDownEvt | InputEvtType.PointerMoveEvt | InputEvtType.PointerUpEvt, point: Point2, allPointers?: Pointer[]): void;
430
+ /**
431
+ * Adds all components in `components` such that they are in the center of the screen.
432
+ * This is a convenience method that creates **and applies** a single command.
433
+ *
434
+ * If `selectComponents` is true (the default), the components are selected.
435
+ */
398
436
  addAndCenterComponents(components: AbstractComponent[], selectComponents?: boolean): Promise<void>;
399
437
  /**
400
438
  * Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
@@ -403,6 +441,9 @@ export declare class Editor {
403
441
  *
404
442
  * The export resolution is the same as the size of the drawing canvas, unless `outputSize`
405
443
  * is given.
444
+ *
445
+ * **Example**:
446
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
406
447
  */
407
448
  toDataURL(format?: 'image/png' | 'image/jpeg' | 'image/webp', outputSize?: Vec2): string;
408
449
  /**
@@ -886,6 +886,12 @@ export class Editor {
886
886
  allPointers) {
887
887
  sendPenEvent(this, eventType, point, allPointers);
888
888
  }
889
+ /**
890
+ * Adds all components in `components` such that they are in the center of the screen.
891
+ * This is a convenience method that creates **and applies** a single command.
892
+ *
893
+ * If `selectComponents` is true (the default), the components are selected.
894
+ */
889
895
  async addAndCenterComponents(components, selectComponents = true) {
890
896
  let bbox = null;
891
897
  for (const component of components) {
@@ -932,6 +938,9 @@ export class Editor {
932
938
  *
933
939
  * The export resolution is the same as the size of the drawing canvas, unless `outputSize`
934
940
  * is given.
941
+ *
942
+ * **Example**:
943
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
935
944
  */
936
945
  toDataURL(format = 'image/png', outputSize) {
937
946
  const canvas = document.createElement('canvas');
@@ -112,7 +112,7 @@ export default class Pointer {
112
112
  // Intended for unit tests.
113
113
  static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
114
114
  const screenPos = viewport.canvasToScreen(canvasPos);
115
- const timeStamp = (new Date()).getTime();
115
+ const timeStamp = performance.now();
116
116
  return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
117
117
  }
118
118
  }
@@ -5,8 +5,28 @@ import SerializableCommand from './SerializableCommand';
5
5
  /**
6
6
  * Removes the given {@link AbstractComponent}s from the image.
7
7
  *
8
- * @example
9
- * ```ts
8
+ * **Example**:
9
+ * ```ts,runnable
10
+ * import { Editor, Erase, uniteCommands, Color4, Path, Stroke, Rect2, pathToRenderable } from 'js-draw';
11
+ *
12
+ * const editor = new Editor(document.body);
13
+ * editor.addToolbar();
14
+ *
15
+ * // Add a large number of strokes
16
+ * const commands = [];
17
+ * for (let x = -20; x < 20; x++) {
18
+ * for (let y = 0; y < 60; y++) {
19
+ * const stroke = new Stroke([
20
+ * pathToRenderable(
21
+ * Path.fromString(`m${x * 5},${y * 5}l1,1`),
22
+ * { fill: Color4.transparent, stroke: {width: 2, color: Color4.ofRGB(x / 10, y / 10, 0.5)}} )
23
+ * ]);
24
+ * commands.push(editor.image.addElement(stroke));
25
+ * }
26
+ * }
27
+ * await editor.dispatch(uniteCommands(commands, 100));
28
+ *
29
+ * ---visible---
10
30
  * // Given some editor...
11
31
  *
12
32
  * // Find all elements intersecting the rectangle with top left (-10,-30) and
@@ -5,8 +5,28 @@ import SerializableCommand from './SerializableCommand.mjs';
5
5
  /**
6
6
  * Removes the given {@link AbstractComponent}s from the image.
7
7
  *
8
- * @example
9
- * ```ts
8
+ * **Example**:
9
+ * ```ts,runnable
10
+ * import { Editor, Erase, uniteCommands, Color4, Path, Stroke, Rect2, pathToRenderable } from 'js-draw';
11
+ *
12
+ * const editor = new Editor(document.body);
13
+ * editor.addToolbar();
14
+ *
15
+ * // Add a large number of strokes
16
+ * const commands = [];
17
+ * for (let x = -20; x < 20; x++) {
18
+ * for (let y = 0; y < 60; y++) {
19
+ * const stroke = new Stroke([
20
+ * pathToRenderable(
21
+ * Path.fromString(`m${x * 5},${y * 5}l1,1`),
22
+ * { fill: Color4.transparent, stroke: {width: 2, color: Color4.ofRGB(x / 10, y / 10, 0.5)}} )
23
+ * ]);
24
+ * commands.push(editor.image.addElement(stroke));
25
+ * }
26
+ * }
27
+ * await editor.dispatch(uniteCommands(commands, 100));
28
+ *
29
+ * ---visible---
10
30
  * // Given some editor...
11
31
  *
12
32
  * // Find all elements intersecting the rectangle with top left (-10,-30) and
@@ -6,6 +6,11 @@ const invertCommand = (command) => {
6
6
  if (command instanceof SerializableCommand) {
7
7
  // SerializableCommand that does the inverse of [command]
8
8
  return new class extends SerializableCommand {
9
+ constructor() {
10
+ super(...arguments);
11
+ // For debugging
12
+ this._command = command;
13
+ }
9
14
  serializeToJSON() {
10
15
  return command.serialize();
11
16
  }
@@ -1,4 +1,40 @@
1
1
  import Command from './Command';
2
2
  import SerializableCommand from './SerializableCommand';
3
+ /**
4
+ * Creates a single command from `commands`. This is useful when undoing should undo *all* commands
5
+ * in `commands` at once, rather than one at a time.
6
+ *
7
+ * @example
8
+ *
9
+ * ```ts,runnable
10
+ * import { Editor, pathToRenderable, Stroke, uniteCommands } from 'js-draw';
11
+ * import { Path, Color4 } from '@js-draw/math';
12
+ *
13
+ * const editor = new Editor(document.body);
14
+ * editor.addToolbar();
15
+ *
16
+ * // Create strokes!
17
+ * const strokes = [];
18
+ * for (let i = 0; i < 10; i++) {
19
+ * const renderablePath = pathToRenderable(
20
+ * Path.fromString(`M0,${i * 10} L100,100 L300,30 z`),
21
+ * { fill: Color4.transparent, stroke: { color: Color4.red, width: 1, } }
22
+ * );
23
+ * strokes.push(new Stroke([ renderablePath ]));
24
+ * }
25
+ *
26
+ * // Convert to commands
27
+ * const addStrokesCommands = strokes.map(stroke => editor.image.addElement(stroke));
28
+ *
29
+ * // Apply all as a single undoable command (try applying each in a loop instead!)
30
+ * await editor.dispatch(uniteCommands(addStrokesCommands));
31
+ *
32
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
33
+ * // applying them shouldn't be done all at once (which would block the UI).
34
+ *
35
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
36
+ * // applying them shouldn't be done all at once (which would block the UI).
37
+ * ```
38
+ */
3
39
  declare const uniteCommands: <T extends Command>(commands: T[], applyChunkSize?: number) => T extends SerializableCommand ? SerializableCommand : Command;
4
40
  export default uniteCommands;
@@ -84,6 +84,42 @@ class SerializableUnion extends SerializableCommand {
84
84
  return this.nonserializableCommand.description(editor, localizationTable);
85
85
  }
86
86
  }
87
+ /**
88
+ * Creates a single command from `commands`. This is useful when undoing should undo *all* commands
89
+ * in `commands` at once, rather than one at a time.
90
+ *
91
+ * @example
92
+ *
93
+ * ```ts,runnable
94
+ * import { Editor, pathToRenderable, Stroke, uniteCommands } from 'js-draw';
95
+ * import { Path, Color4 } from '@js-draw/math';
96
+ *
97
+ * const editor = new Editor(document.body);
98
+ * editor.addToolbar();
99
+ *
100
+ * // Create strokes!
101
+ * const strokes = [];
102
+ * for (let i = 0; i < 10; i++) {
103
+ * const renderablePath = pathToRenderable(
104
+ * Path.fromString(`M0,${i * 10} L100,100 L300,30 z`),
105
+ * { fill: Color4.transparent, stroke: { color: Color4.red, width: 1, } }
106
+ * );
107
+ * strokes.push(new Stroke([ renderablePath ]));
108
+ * }
109
+ *
110
+ * // Convert to commands
111
+ * const addStrokesCommands = strokes.map(stroke => editor.image.addElement(stroke));
112
+ *
113
+ * // Apply all as a single undoable command (try applying each in a loop instead!)
114
+ * await editor.dispatch(uniteCommands(addStrokesCommands));
115
+ *
116
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
117
+ * // applying them shouldn't be done all at once (which would block the UI).
118
+ *
119
+ * // The second parameter to uniteCommands is for very large numbers of commands, when
120
+ * // applying them shouldn't be done all at once (which would block the UI).
121
+ * ```
122
+ */
87
123
  const uniteCommands = (commands, applyChunkSize) => {
88
124
  let allSerializable = true;
89
125
  for (const command of commands) {
@@ -116,6 +116,14 @@ export default abstract class AbstractComponent {
116
116
  protected abstract applyTransformation(affineTransfm: Mat33): void;
117
117
  transformBy(affineTransfm: Mat33): SerializableCommand;
118
118
  setZIndex(newZIndex: number): SerializableCommand;
119
+ /**
120
+ * Combines {@link transformBy} and {@link setZIndex} into a single command.
121
+ *
122
+ * @param newZIndex - The z-index this component should have after applying this command.
123
+ * @param originalZIndex - @internal The z-index the component should revert to after unapplying
124
+ * this command.
125
+ */
126
+ setZIndexAndTransformBy(affineTransfm: Mat33, newZIndex: number, originalZIndex?: number): SerializableCommand;
119
127
  isSelectable(): boolean;
120
128
  isBackground(): boolean;
121
129
  getProportionalRenderingTime(): number;
@@ -138,13 +138,25 @@ class AbstractComponent {
138
138
  }
139
139
  // Returns a command that, when applied, transforms this by [affineTransfm] and
140
140
  // updates the editor.
141
- // This also increases the element's z-index so that it is on top.
141
+ //
142
+ // The transformed component is also moved to the top (use {@link setZIndexAndTransformBy} to
143
+ // avoid this behavior).
142
144
  transformBy(affineTransfm) {
143
145
  return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
144
146
  }
145
147
  // Returns a command that updates this component's z-index.
146
148
  setZIndex(newZIndex) {
147
- return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex, this.getZIndex());
149
+ return new AbstractComponent.TransformElementCommand(Mat33.identity, this.getId(), this, newZIndex);
150
+ }
151
+ /**
152
+ * Combines {@link transformBy} and {@link setZIndex} into a single command.
153
+ *
154
+ * @param newZIndex - The z-index this component should have after applying this command.
155
+ * @param originalZIndex - @internal The z-index the component should revert to after unapplying
156
+ * this command.
157
+ */
158
+ setZIndexAndTransformBy(affineTransfm, newZIndex, originalZIndex) {
159
+ return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this, newZIndex, originalZIndex);
148
160
  }
149
161
  // @returns true iff this component can be selected (e.g. by the selection tool.)
150
162
  isSelectable() {
@@ -215,8 +227,12 @@ class AbstractComponent {
215
227
  throw new Error(`Element with data ${json} cannot be deserialized.`);
216
228
  }
217
229
  const instance = this.deserializationCallbacks[json.name](json.data);
218
- instance.zIndex = json.zIndex;
219
230
  instance.id = json.id;
231
+ if (isFinite(json.zIndex)) {
232
+ instance.zIndex = json.zIndex;
233
+ // Ensure that new components will be added on top.
234
+ AbstractComponent.zIndexCounter = Math.max(AbstractComponent.zIndexCounter, instance.zIndex + 1);
235
+ }
220
236
  // TODO: What should we do with json.loadSaveData?
221
237
  // If we attach it to [instance], we create a potential security risk — loadSaveData
222
238
  // is often used to store unrecognised attributes so they can be preserved on output.
@@ -225,6 +241,7 @@ class AbstractComponent {
225
241
  }
226
242
  }
227
243
  // Topmost z-index
244
+ // TODO: Should be a property of the EditorImage.
228
245
  AbstractComponent.zIndexCounter = 0;
229
246
  AbstractComponent.deserializationCallbacks = {};
230
247
  AbstractComponent.transformElementCommandId = 'transform-element';
@@ -252,7 +269,7 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
252
269
  super.resolveComponent(image);
253
270
  this.origZIndex ??= this.component.getZIndex();
254
271
  }
255
- updateTransform(editor, newTransfm) {
272
+ updateTransform(editor, newTransfm, targetZIndex) {
256
273
  if (!this.component) {
257
274
  throw new Error('this.component is undefined or null!');
258
275
  }
@@ -264,7 +281,12 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
264
281
  hadParent = true;
265
282
  }
266
283
  this.component.applyTransformation(newTransfm);
284
+ this.component.zIndex = targetZIndex;
267
285
  this.component.lastChangedTime = (new Date()).getTime();
286
+ // Ensure that new components are automatically drawn above the current component.
287
+ if (targetZIndex >= AbstractComponent.zIndexCounter) {
288
+ AbstractComponent.zIndexCounter = targetZIndex + 1;
289
+ }
268
290
  // Add the element back to the document.
269
291
  if (hadParent) {
270
292
  EditorImage.addElement(this.component).apply(editor);
@@ -272,14 +294,12 @@ AbstractComponent.TransformElementCommand = (_a = class extends UnresolvedSerial
272
294
  }
273
295
  apply(editor) {
274
296
  this.resolveComponent(editor.image);
275
- this.component.zIndex = this.targetZIndex;
276
- this.updateTransform(editor, this.affineTransfm);
297
+ this.updateTransform(editor, this.affineTransfm, this.targetZIndex);
277
298
  editor.queueRerender();
278
299
  }
279
300
  unapply(editor) {
280
301
  this.resolveComponent(editor.image);
281
- this.component.zIndex = this.origZIndex;
282
- this.updateTransform(editor, this.affineTransfm.inverse());
302
+ this.updateTransform(editor, this.affineTransfm.inverse(), this.origZIndex);
283
303
  editor.queueRerender();
284
304
  }
285
305
  description(_editor, localizationTable) {
@@ -2,12 +2,24 @@ import { Mat33Array, Rect2, Mat33, LineSegment2 } from '@js-draw/math';
2
2
  import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
3
3
  import AbstractComponent from './AbstractComponent';
4
4
  import { ImageComponentLocalization } from './localization';
5
+ /**
6
+ * Represents a raster image.
7
+ *
8
+ * **Example: Adding images**:
9
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
10
+ */
5
11
  export default class ImageComponent extends AbstractComponent {
6
12
  protected contentBBox: Rect2;
7
13
  private image;
8
14
  constructor(image: RenderableImage);
9
15
  private getImageRect;
10
16
  private recomputeBBox;
17
+ /**
18
+ * Load from an image. Waits for the image to load if incomplete.
19
+ *
20
+ * The image, `elem`, must not [taint](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases)
21
+ * an HTMLCanvasElement when rendered.
22
+ */
11
23
  static fromImage(elem: HTMLImageElement, transform: Mat33): Promise<ImageComponent>;
12
24
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
13
25
  getProportionalRenderingTime(): number;
@@ -1,7 +1,13 @@
1
1
  import { Rect2, Mat33 } from '@js-draw/math';
2
2
  import { assertIsNumber, assertIsNumberArray } from '../util/assertions.mjs';
3
3
  import AbstractComponent from './AbstractComponent.mjs';
4
- // Represents a raster image.
4
+ import waitForImageLoaded from '../util/waitForImageLoaded.mjs';
5
+ /**
6
+ * Represents a raster image.
7
+ *
8
+ * **Example: Adding images**:
9
+ * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
10
+ */
5
11
  export default class ImageComponent extends AbstractComponent {
6
12
  constructor(image) {
7
13
  super('image-component');
@@ -24,15 +30,14 @@ export default class ImageComponent extends AbstractComponent {
24
30
  this.contentBBox = this.getImageRect();
25
31
  this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
26
32
  }
27
- // Load from an image. Waits for the image to load if incomplete.
33
+ /**
34
+ * Load from an image. Waits for the image to load if incomplete.
35
+ *
36
+ * The image, `elem`, must not [taint](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases)
37
+ * an HTMLCanvasElement when rendered.
38
+ */
28
39
  static async fromImage(elem, transform) {
29
- if (!elem.complete) {
30
- await new Promise((resolve, reject) => {
31
- elem.onload = resolve;
32
- elem.onerror = reject;
33
- elem.onabort = reject;
34
- });
35
- }
40
+ await waitForImageLoaded(elem);
36
41
  let width, height;
37
42
  if (typeof elem.width === 'number' && typeof elem.height === 'number'
38
43
  && elem.width !== 0 && elem.height !== 0) {
@@ -74,6 +79,7 @@ export default class ImageComponent extends AbstractComponent {
74
79
  canvas.drawImage(this.image);
75
80
  canvas.endObject(this.getLoadSaveData());
76
81
  }
82
+ // A *very* rough estimate of how long it takes to render this component
77
83
  getProportionalRenderingTime() {
78
84
  // Estimate: Equivalent to a stroke with 10 segments.
79
85
  return 10;
@@ -98,6 +104,7 @@ export default class ImageComponent extends AbstractComponent {
98
104
  getAltText() {
99
105
  return this.image.label;
100
106
  }
107
+ // The base64 image URL of this image.
101
108
  getURL() {
102
109
  return this.image.base64Url;
103
110
  }
@@ -5,7 +5,7 @@ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
7
  import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
8
- import RenderablePathSpec from '../rendering/RenderablePathSpec';
8
+ import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
9
9
  /**
10
10
  * Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
11
11
  *
@@ -21,6 +21,9 @@ import RenderablePathSpec from '../rendering/RenderablePathSpec';
21
21
  * ```ts
22
22
  * editor.dispatch(stroke.transformBy(Mat33.translation(Vec2.of(10, 0))));
23
23
  * ```
24
+ *
25
+ * **Adding**:
26
+ * [[include:doc-pages/inline-examples/adding-a-stroke.md]]
24
27
  */
25
28
  export default class Stroke extends AbstractComponent implements RestyleableComponent {
26
29
  private parts;
@@ -39,7 +42,7 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
39
42
  *
40
43
  * const stroke = new Stroke([
41
44
  * // Fill with red
42
- * pathToRenderable({ fill: Color4.red })
45
+ * pathToRenderable(path, { fill: Color4.red })
43
46
  * ]);
44
47
  * ```
45
48
  */
@@ -57,6 +60,17 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
57
60
  private bboxForPart;
58
61
  getExactBBox(): Rect2;
59
62
  protected applyTransformation(affineTransfm: Mat33): void;
63
+ /**
64
+ * @returns A list of the parts that make up this path. Many paths only have one part.
65
+ *
66
+ * Each part (a {@link RenderablePathSpec}) contains information about the style and geometry
67
+ * of that part of the stroke. Use the `.path` property to do collision detection and other
68
+ * operations involving the stroke's geometry.
69
+ *
70
+ * Note that many of {@link Path}'s methods (e.g. {@link Path.intersection}) take a
71
+ * `strokeWidth` parameter that can be gotten from {@link RenderablePathSpec.style} `.stroke.width`.
72
+ */
73
+ getParts(): Readonly<RenderablePathSpecWithPath>[];
60
74
  /**
61
75
  * @returns the {@link Path.union} of all paths that make up this stroke.
62
76
  */