@lvl99dotcom/paintcanvas 0.4.0 → 0.6.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 (224) hide show
  1. package/README.md +30 -15
  2. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  3. package/dist/lib/brushes/configRanges/index.js +31 -1
  4. package/dist/lib/brushes/params/index.d.ts +2 -1
  5. package/dist/lib/brushes/params/index.js +7 -4
  6. package/dist/lib/brushes/presets/round/index.d.ts +1 -1
  7. package/dist/lib/brushes/presets/round/index.js +12 -7
  8. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  9. package/dist/lib/brushes/presets/square/index.js +7 -3
  10. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  11. package/dist/lib/brushes/shapeImage/index.js +15 -9
  12. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  13. package/dist/lib/brushes/shapes/index.js +86 -0
  14. package/dist/lib/brushes/types/index.d.ts +8 -0
  15. package/dist/lib/classes/BrushSettings/index.d.ts +29 -2
  16. package/dist/lib/classes/BrushSettings/index.js +51 -14
  17. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +31 -2
  18. package/dist/lib/classes/KeyboardShortcuts/index.js +66 -4
  19. package/dist/lib/classes/Layer/index.d.ts +10 -0
  20. package/dist/lib/classes/Layer/index.js +15 -1
  21. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  22. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  23. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  24. package/dist/lib/classes/LayerStack/index.js +26 -2
  25. package/dist/lib/classes/PaintCanvas/index.d.ts +72 -3
  26. package/dist/lib/classes/PaintCanvas/index.js +106 -12
  27. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  28. package/dist/lib/classes/PngEncoder/index.js +104 -0
  29. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  30. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  31. package/dist/lib/classes/SettingsAutosave/index.d.ts +3 -2
  32. package/dist/lib/classes/SettingsAutosave/index.js +6 -3
  33. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  34. package/dist/lib/classes/UiSettings/index.js +82 -0
  35. package/dist/lib/messages/schema.d.ts +720 -0
  36. package/dist/lib/presets/schema.d.ts +360 -0
  37. package/dist/lib/presets/schema.js +8 -1
  38. package/dist/lib/profiles/index.d.ts +61 -0
  39. package/dist/lib/profiles/index.js +131 -0
  40. package/dist/lib/profiles/schema.d.ts +73 -0
  41. package/dist/lib/profiles/schema.js +53 -0
  42. package/dist/lib/rendering/sample/index.d.ts +11 -0
  43. package/dist/lib/rendering/sample/index.js +36 -5
  44. package/dist/lib/settings/index.d.ts +2 -1
  45. package/dist/lib/settings/index.js +7 -2
  46. package/dist/lib/settings/schema.d.ts +67 -0
  47. package/dist/lib/settings/schema.js +20 -0
  48. package/dist/lib/validation/validators.generated.js +13191 -3655
  49. package/dist/locales/en-GB.js +39 -2
  50. package/dist/locales/en-US.js +39 -2
  51. package/dist/locales/fr.js +39 -2
  52. package/dist/locales/ja.js +39 -2
  53. package/dist/locales/ko.js +39 -2
  54. package/dist/locales/zh-Hans.js +39 -2
  55. package/dist/locales/zh-Hant.js +39 -2
  56. package/dist/package.js +1 -1
  57. package/dist/testing/recordingCanvas.js +2 -1
  58. package/dist/ui/ask/index.d.ts +48 -0
  59. package/dist/ui/ask/index.js +67 -0
  60. package/dist/ui/base/index.d.ts +10 -3
  61. package/dist/ui/base/index.js +19 -4
  62. package/dist/ui/dismiss/index.d.ts +21 -0
  63. package/dist/ui/dismiss/index.js +27 -0
  64. package/dist/ui/drag/index.d.ts +51 -0
  65. package/dist/ui/drag/index.js +81 -0
  66. package/dist/ui/elements/PaintCanvas/index.d.ts +17 -12
  67. package/dist/ui/elements/PaintCanvas/index.js +36 -17
  68. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  69. package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
  70. package/dist/ui/elements/PcAppSettings/index.js +21 -65
  71. package/dist/ui/elements/PcBrushConfig/index.d.ts +34 -16
  72. package/dist/ui/elements/PcBrushConfig/index.js +263 -153
  73. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  74. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  75. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  76. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  77. package/dist/ui/elements/PcBrushTest/index.js +79 -6
  78. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  79. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  80. package/dist/ui/elements/PcButton/index.js +18 -10
  81. package/dist/ui/elements/PcCanvas/index.d.ts +27 -2
  82. package/dist/ui/elements/PcCanvas/index.js +63 -13
  83. package/dist/ui/elements/PcCanvas/style.js +1 -1
  84. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  85. package/dist/ui/elements/PcClipboardTools/index.d.ts +49 -0
  86. package/dist/ui/elements/PcClipboardTools/index.js +125 -0
  87. package/dist/ui/elements/PcContextMenu/index.d.ts +2 -2
  88. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  89. package/dist/ui/elements/PcDialog/index.js +102 -51
  90. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  91. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  92. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  93. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  94. package/dist/ui/elements/PcMenu/index.d.ts +48 -3
  95. package/dist/ui/elements/PcMenu/index.js +111 -19
  96. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  97. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  98. package/dist/ui/elements/PcPane/index.d.ts +3 -1
  99. package/dist/ui/elements/PcPane/index.js +44 -7
  100. package/dist/ui/elements/PcPanel/index.d.ts +2 -2
  101. package/dist/ui/elements/PcPanel/index.js +8 -8
  102. package/dist/ui/elements/PcPanelBrushes/index.d.ts +5 -17
  103. package/dist/ui/elements/PcPanelBrushes/index.js +13 -48
  104. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  105. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  106. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  107. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  108. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  109. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  110. package/dist/ui/elements/PcPopover/index.js +72 -4
  111. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  112. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  113. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  114. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  115. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  116. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  117. package/dist/ui/elements/PcSliderRange/index.js +307 -0
  118. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  119. package/dist/ui/elements/PcStats/index.js +29 -13
  120. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  121. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  122. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  123. package/dist/ui/elements/PcToolSettings/index.js +382 -318
  124. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  125. package/dist/ui/elements/PcToolbar/index.d.ts +15 -0
  126. package/dist/ui/elements/PcToolbar/index.js +55 -14
  127. package/dist/ui/elements/PcToolbar/style.js +1 -1
  128. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  129. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  130. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  131. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  132. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  133. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  134. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  135. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  136. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  137. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  138. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  139. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  140. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  141. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  142. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  143. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  144. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  145. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  146. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  147. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  148. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  149. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  150. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  151. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  152. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  153. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  154. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  155. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  156. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  157. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  158. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  159. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  160. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  161. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  162. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  163. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  164. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  165. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  166. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  167. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  168. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  169. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  170. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  171. package/dist/ui/icons/index.d.ts +1 -1
  172. package/dist/ui/icons/index.js +86 -72
  173. package/dist/ui/index.d.ts +13 -3
  174. package/dist/ui/index.js +15 -4
  175. package/dist/ui/layout/index.d.ts +9 -1
  176. package/dist/ui/layout/index.js +11 -2
  177. package/dist/ui/styles/index.d.ts +21 -1
  178. package/dist/ui/styles/index.js +73 -14
  179. package/dist/ui/tokens/index.d.ts +5 -4
  180. package/dist/ui/tokens/index.js +12 -7
  181. package/locales/en-GB.json +39 -2
  182. package/locales/en-US.json +39 -2
  183. package/locales/fr.json +39 -2
  184. package/locales/ja.json +39 -2
  185. package/locales/ko.json +39 -2
  186. package/locales/zh-Hans.json +39 -2
  187. package/locales/zh-Hant.json +39 -2
  188. package/package.json +27 -18
  189. package/dist/ui/icons/images/brush.js +0 -3
  190. package/dist/ui/icons/images/circle-mask.js +0 -3
  191. package/dist/ui/icons/images/clipboard.js +0 -3
  192. package/dist/ui/icons/images/cursor.js +0 -3
  193. package/dist/ui/icons/images/eraser.js +0 -3
  194. package/dist/ui/icons/images/eyedropper.js +0 -3
  195. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  196. package/dist/ui/icons/images/flip-vertical.js +0 -3
  197. package/dist/ui/icons/images/focus-pen.js +0 -3
  198. package/dist/ui/icons/images/freehand-mask.js +0 -3
  199. package/dist/ui/icons/images/hidden.js +0 -3
  200. package/dist/ui/icons/images/layers.js +0 -3
  201. package/dist/ui/icons/images/linked.js +0 -3
  202. package/dist/ui/icons/images/locked.js +0 -3
  203. package/dist/ui/icons/images/moon.js +0 -3
  204. package/dist/ui/icons/images/pan.js +0 -3
  205. package/dist/ui/icons/images/polygon-mask.js +0 -3
  206. package/dist/ui/icons/images/pressure.js +0 -3
  207. package/dist/ui/icons/images/randomise.js +0 -3
  208. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  209. package/dist/ui/icons/images/redo.js +0 -3
  210. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  211. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  212. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  213. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  214. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  215. package/dist/ui/icons/images/star.js +0 -3
  216. package/dist/ui/icons/images/sun.js +0 -3
  217. package/dist/ui/icons/images/tilt.js +0 -3
  218. package/dist/ui/icons/images/undo.js +0 -3
  219. package/dist/ui/icons/images/unlinked.js +0 -3
  220. package/dist/ui/icons/images/unlocked.js +0 -3
  221. package/dist/ui/icons/images/visible.js +0 -3
  222. package/dist/ui/icons/images/zoom-in.js +0 -3
  223. package/dist/ui/icons/images/zoom-out.js +0 -3
  224. package/dist/ui/icons/images/zoom.js +0 -3
@@ -1,5 +1,17 @@
1
1
  import { type default as PaintCanvas } from "../PaintCanvas";
2
2
  import { type ShortcutAction } from "../../utils/shortcuts";
3
+ /**
4
+ * How long (milliseconds) the paste shortcut waits for the browser's `paste` event (which carries other apps' images)
5
+ * before pasting the image data copied here
6
+ */
7
+ export declare const PASTE_EVENT_WAIT_MS = 50;
8
+ /**
9
+ * The first image in a paste's clipboard data (e.g. copied in another app), if any.
10
+ *
11
+ * @example
12
+ * pastedImage(event.clipboardData); // File of type "image/png", or undefined
13
+ */
14
+ export declare const pastedImage: (data: DataTransfer | null) => File | undefined;
3
15
  /** Event dispatched on the window for shortcut actions handled elsewhere (e.g. saving) */
4
16
  export declare const SHORTCUT_EVENT = "pc-shortcut";
5
17
  /**
@@ -9,7 +21,10 @@ export declare const SHORTCUT_EVENT = "pc-shortcut";
9
21
  *
10
22
  * Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
11
23
  * cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
12
- * and finishing or cancelling a lasso shape are handled here; other actions (and Escape/Enter for later features)
24
+ * and finishing or cancelling a lasso shape are handled here. Pasting (the shortcut, or the browser's own paste, e.g. from
25
+ * its Edit menu) pastes the image data copied here, unless the window has lost focus since it was copied and the
26
+ * clipboard has an image (e.g. copied in another app): that image is pasted instead (see `PaintCanvas.pasteImage`).
27
+ * Other actions other actions (and Escape/Enter for later features)
13
28
  * dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
14
29
  * temporarily; releasing it returns to the selected tool.
15
30
  *
@@ -24,8 +39,15 @@ export default class KeyboardShortcuts {
24
39
  private heldKey?;
25
40
  private readonly onKeyDown;
26
41
  private readonly onKeyUp;
42
+ /** Turns pasted image files into images (replaceable for tests) */
43
+ private readonly decodeImage;
44
+ /** `app.copyCount` when the window last lost focus: copies since then were made here */
45
+ private copiesBeforeBlur;
46
+ /** Pastes the image data copied here if the browser doesn't fire `paste` soon after the shortcut */
47
+ private pasteTimer?;
27
48
  private readonly onBlur;
28
- constructor(app: PaintCanvas);
49
+ private readonly onPaste;
50
+ constructor(app: PaintCanvas, decodeImage?: (blob: Blob) => Promise<ImageBitmap>);
29
51
  /**
30
52
  * Start listening for key presses on a target (usually `window`).
31
53
  */
@@ -38,6 +60,13 @@ export default class KeyboardShortcuts {
38
60
  * Run the shortcut for a key press, if any. Returns the action run.
39
61
  */
40
62
  handle(event: KeyboardEvent): ShortcutAction | undefined;
63
+ /**
64
+ * Paste from the browser's `paste` event: the image data copied here if it was copied since the window last lost
65
+ * focus (or the clipboard has no image), otherwise the clipboard's image. Ignored in text fields and open dialogs.
66
+ */
67
+ handlePaste(event: ClipboardEvent): Promise<void>;
68
+ /** Stop waiting for the browser's `paste` event */
69
+ private cancelPasteTimer;
41
70
  /**
42
71
  * Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
43
72
  */
@@ -7,6 +7,18 @@ import { comboFromEvent, isTypingTarget, sameCombo } from "../../utils/shortcuts
7
7
  * Escape is left to close it.
8
8
  */
9
9
  var inOpenDialog = (path) => path.some((target) => target instanceof HTMLDialogElement && target.open);
10
+ /**
11
+ * How long (milliseconds) the paste shortcut waits for the browser's `paste` event (which carries other apps' images)
12
+ * before pasting the image data copied here
13
+ */
14
+ var PASTE_EVENT_WAIT_MS = 50;
15
+ /**
16
+ * The first image in a paste's clipboard data (e.g. copied in another app), if any.
17
+ *
18
+ * @example
19
+ * pastedImage(event.clipboardData); // File of type "image/png", or undefined
20
+ */
21
+ var pastedImage = (data) => [...data?.files ?? []].find((file) => file.type.startsWith("image/"));
10
22
  /** Event dispatched on the window for shortcut actions handled elsewhere (e.g. saving) */
11
23
  var SHORTCUT_EVENT = "pc-shortcut";
12
24
  /**
@@ -16,7 +28,10 @@ var SHORTCUT_EVENT = "pc-shortcut";
16
28
  *
17
29
  * Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
18
30
  * cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
19
- * and finishing or cancelling a lasso shape are handled here; other actions (and Escape/Enter for later features)
31
+ * and finishing or cancelling a lasso shape are handled here. Pasting (the shortcut, or the browser's own paste, e.g. from
32
+ * its Edit menu) pastes the image data copied here, unless the window has lost focus since it was copied and the
33
+ * clipboard has an image (e.g. copied in another app): that image is pasted instead (see `PaintCanvas.pasteImage`).
34
+ * Other actions other actions (and Escape/Enter for later features)
20
35
  * dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
21
36
  * temporarily; releasing it returns to the selected tool.
22
37
  *
@@ -31,9 +46,20 @@ var KeyboardShortcuts = class {
31
46
  heldKey;
32
47
  onKeyDown = (event) => this.handle(event);
33
48
  onKeyUp = (event) => this.handleKeyUp(event);
34
- onBlur = () => this.release();
35
- constructor(app) {
49
+ /** Turns pasted image files into images (replaceable for tests) */
50
+ decodeImage;
51
+ /** `app.copyCount` when the window last lost focus: copies since then were made here */
52
+ copiesBeforeBlur = 0;
53
+ /** Pastes the image data copied here if the browser doesn't fire `paste` soon after the shortcut */
54
+ pasteTimer;
55
+ onBlur = () => {
56
+ this.release();
57
+ this.copiesBeforeBlur = this.app.copyCount;
58
+ };
59
+ onPaste = (event) => void this.handlePaste(event);
60
+ constructor(app, decodeImage = (blob) => createImageBitmap(blob)) {
36
61
  this.app = app;
62
+ this.decodeImage = decodeImage;
37
63
  }
38
64
  /**
39
65
  * Start listening for key presses on a target (usually `window`).
@@ -44,6 +70,7 @@ var KeyboardShortcuts = class {
44
70
  target.addEventListener("keydown", this.onKeyDown);
45
71
  target.addEventListener("keyup", this.onKeyUp);
46
72
  target.addEventListener("blur", this.onBlur);
73
+ target.addEventListener("paste", this.onPaste);
47
74
  }
48
75
  /**
49
76
  * Stop listening for key presses.
@@ -52,7 +79,9 @@ var KeyboardShortcuts = class {
52
79
  this.target?.removeEventListener("keydown", this.onKeyDown);
53
80
  this.target?.removeEventListener("keyup", this.onKeyUp);
54
81
  this.target?.removeEventListener("blur", this.onBlur);
82
+ this.target?.removeEventListener("paste", this.onPaste);
55
83
  this.target = null;
84
+ this.cancelPasteTimer();
56
85
  }
57
86
  /**
58
87
  * Run the shortcut for a key press, if any. Returns the action run.
@@ -71,11 +100,44 @@ var KeyboardShortcuts = class {
71
100
  }
72
101
  const action = shortcuts.find((shortcut) => sameCombo(shortcut, combo))?.action;
73
102
  if (!action) return void 0;
103
+ if (action.type === "paste") {
104
+ this.cancelPasteTimer();
105
+ this.pasteTimer = setTimeout(() => {
106
+ this.pasteTimer = void 0;
107
+ this.app.paste();
108
+ }, 50);
109
+ return action;
110
+ }
74
111
  event.preventDefault();
75
112
  this.run(action);
76
113
  return action;
77
114
  }
78
115
  /**
116
+ * Paste from the browser's `paste` event: the image data copied here if it was copied since the window last lost
117
+ * focus (or the clipboard has no image), otherwise the clipboard's image. Ignored in text fields and open dialogs.
118
+ */
119
+ async handlePaste(event) {
120
+ const path = event.composedPath();
121
+ if (isTypingTarget(path[0] ?? event.target) || inOpenDialog(path)) return;
122
+ this.cancelPasteTimer();
123
+ event.preventDefault();
124
+ const app = this.app;
125
+ const image = pastedImage(event.clipboardData);
126
+ const copiedHere = app.copyCount > this.copiesBeforeBlur;
127
+ if (!image || copiedHere && app.hasClipboard) {
128
+ app.paste();
129
+ return;
130
+ }
131
+ try {
132
+ app.pasteImage(await this.decodeImage(image));
133
+ } catch {}
134
+ }
135
+ /** Stop waiting for the browser's `paste` event */
136
+ cancelPasteTimer() {
137
+ clearTimeout(this.pasteTimer);
138
+ this.pasteTimer = void 0;
139
+ }
140
+ /**
79
141
  * Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
80
142
  */
81
143
  handleKeyUp(event) {
@@ -197,4 +259,4 @@ var KeyboardShortcuts = class {
197
259
  (this.target ?? this.app).dispatchEvent(new CustomEvent(SHORTCUT_EVENT, { detail: action }));
198
260
  }
199
261
  };
200
- export { SHORTCUT_EVENT, KeyboardShortcuts as default };
262
+ export { PASTE_EVENT_WAIT_MS, SHORTCUT_EVENT, KeyboardShortcuts as default, pastedImage };
@@ -29,6 +29,16 @@ export type CanvasFactory = (width: number, height: number) => OffscreenCanvas;
29
29
  * Create an `OffscreenCanvas` of the given size.
30
30
  */
31
31
  export declare const createOffscreenCanvas: CanvasFactory;
32
+ /**
33
+ * Create an `OffscreenCanvas` whose pixels are kept in regular memory rather than on the GPU (its 2D context is made
34
+ * with `willReadFrequently`). Reading its pixels, encoding it and copying it are then plain memory copies, which on
35
+ * phones avoids long stalls waiting for the GPU (up to 15 seconds were seen on an iPhone), at the cost of drawing on
36
+ * the CPU. PaintCanvas makes its canvases this way by default.
37
+ *
38
+ * @example
39
+ * const canvas = createMemoryCanvas(2000, 2000);
40
+ */
41
+ export declare const createMemoryCanvas: CanvasFactory;
32
42
  /**
33
43
  * Clamp an opacity value to 0–1.
34
44
  *
@@ -16,6 +16,20 @@ var MAX_LAYER_NAME_LENGTH = 64;
16
16
  */
17
17
  var createOffscreenCanvas = (width, height) => new OffscreenCanvas(width, height);
18
18
  /**
19
+ * Create an `OffscreenCanvas` whose pixels are kept in regular memory rather than on the GPU (its 2D context is made
20
+ * with `willReadFrequently`). Reading its pixels, encoding it and copying it are then plain memory copies, which on
21
+ * phones avoids long stalls waiting for the GPU (up to 15 seconds were seen on an iPhone), at the cost of drawing on
22
+ * the CPU. PaintCanvas makes its canvases this way by default.
23
+ *
24
+ * @example
25
+ * const canvas = createMemoryCanvas(2000, 2000);
26
+ */
27
+ var createMemoryCanvas = (width, height) => {
28
+ const canvas = new OffscreenCanvas(width, height);
29
+ canvas.getContext("2d", { willReadFrequently: true });
30
+ return canvas;
31
+ };
32
+ /**
19
33
  * Clamp an opacity value to 0–1.
20
34
  *
21
35
  * @example
@@ -108,4 +122,4 @@ var Layer = class {
108
122
  };
109
123
  }
110
124
  };
111
- export { BlendMode, MAX_LAYER_NAME_LENGTH, clampOpacity, createOffscreenCanvas, Layer as default };
125
+ export { BlendMode, MAX_LAYER_NAME_LENGTH, clampOpacity, createMemoryCanvas, createOffscreenCanvas, Layer as default };
@@ -34,7 +34,7 @@ export interface AutosaveOptions {
34
34
  locks?: LockManager | null;
35
35
  /** Tells when the page is hidden or shown again (the page's document by default) */
36
36
  document?: Pick<Document, "visibilityState" | "addEventListener" | "removeEventListener"> | null;
37
- /** Turns a layer's canvas into a PNG (replaceable for tests) */
37
+ /** Turns a layer's canvas into a PNG (in a background worker by default; replaceable for tests) */
38
38
  encode?: (canvas: OffscreenCanvas) => Promise<Blob>;
39
39
  /** Turns a saved PNG into an image to draw (replaceable for tests) */
40
40
  decode?: (image: Blob) => Promise<CanvasImageSource>;
@@ -44,8 +44,8 @@ export interface AutosaveOptions {
44
44
  /**
45
45
  * Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
46
46
  * starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
47
- * Only layers whose pixels changed are encoded and saved again; a quick fingerprint of each layer's pixels tells
48
- * which.
47
+ * Only layers whose pixels changed are encoded and saved again: the layers' change counts tell which (see
48
+ * `LayerStack.pixelVersion`), without reading their pixels, which is slow on phones.
49
49
  *
50
50
  * Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
51
51
  * (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
@@ -74,14 +74,10 @@ export default class LayerAutosave extends EventTarget {
74
74
  private readonly delayMs;
75
75
  /** The layers being watched for changes */
76
76
  private watched?;
77
- /** Fingerprints of each layer's pixels as last saved, by layer ID */
78
- private prints;
77
+ /** Each layer as last saved, by layer ID: the layer itself and its change count (see `LayerStack.pixelVersion`) */
78
+ private saved;
79
79
  /** Size in bytes of each layer's image as last saved or restored, by layer ID */
80
80
  private sizes;
81
- /** Layers whose pixels changed since the last save */
82
- private changed;
83
- /** Whether any layer may have changed since the last save (not only those in `changed`) */
84
- private checkAll;
85
81
  private timer?;
86
82
  /** The save being written, if any */
87
83
  private saving?;
@@ -133,11 +129,13 @@ export default class LayerAutosave extends EventTarget {
133
129
  */
134
130
  private restore;
135
131
  /**
136
- * Remember each layer's pixels as saved (just restored).
132
+ * Remember each layer as saved (just restored).
137
133
  */
138
134
  private remember;
135
+ /** A layer and its change count, to tell later whether it changed */
136
+ private record;
139
137
  /**
140
- * Save the drawing, encoding only the layers whose pixels changed since they were last saved.
138
+ * Save the drawing, encoding only the layers whose pixels changed since they were last saved (or that are new).
141
139
  */
142
140
  private write;
143
141
  /**
@@ -145,10 +143,6 @@ export default class LayerAutosave extends EventTarget {
145
143
  * changed.
146
144
  */
147
145
  private rememberSizes;
148
- /**
149
- * A fingerprint of a layer's pixels.
150
- */
151
- private fingerprint;
152
146
  /**
153
147
  * Save once the layers have stopped changing for a moment and drawing pauses.
154
148
  */
@@ -1,4 +1,4 @@
1
- import { pixelFingerprint } from "../../image/index.js";
1
+ import PngEncoder from "../PngEncoder/index.js";
2
2
  import "../../storage/index.js";
3
3
  // src/lib/classes/LayerAutosave/index.ts
4
4
  /** How long to wait after the layers change before saving, so quick changes are saved together (milliseconds) */
@@ -18,12 +18,14 @@ var AutosaveState = {
18
18
  /** Not saving any more (see `stop`) */
19
19
  Stopped: "stopped"
20
20
  };
21
+ /** Encodes layers as PNGs in a background worker, shared by every autosave on the page (made on first use) */
22
+ var sharedEncoder;
21
23
  /**
22
- * Encode a canvas as a PNG.
24
+ * Encode a canvas as a PNG, in a background worker where one can be used, so saving doesn't freeze the page.
23
25
  */
24
26
  var encodePng = (canvas) => {
25
- canvas.getContext("2d");
26
- return canvas.convertToBlob({ type: "image/png" });
27
+ sharedEncoder ??= new PngEncoder();
28
+ return sharedEncoder.encode(canvas);
27
29
  };
28
30
  /**
29
31
  * Whether a saved drawing has the expected shape and an image for each layer.
@@ -32,8 +34,8 @@ var isComplete = (drawing, images) => drawing.version === 1 && drawing.layers.le
32
34
  /**
33
35
  * Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
34
36
  * starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
35
- * Only layers whose pixels changed are encoded and saved again; a quick fingerprint of each layer's pixels tells
36
- * which.
37
+ * Only layers whose pixels changed are encoded and saved again: the layers' change counts tell which (see
38
+ * `LayerStack.pixelVersion`), without reading their pixels, which is slow on phones.
37
39
  *
38
40
  * Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
39
41
  * (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
@@ -62,14 +64,10 @@ var LayerAutosave = class extends EventTarget {
62
64
  delayMs;
63
65
  /** The layers being watched for changes */
64
66
  watched;
65
- /** Fingerprints of each layer's pixels as last saved, by layer ID */
66
- prints = /* @__PURE__ */ new Map();
67
+ /** Each layer as last saved, by layer ID: the layer itself and its change count (see `LayerStack.pixelVersion`) */
68
+ saved = /* @__PURE__ */ new Map();
67
69
  /** Size in bytes of each layer's image as last saved or restored, by layer ID */
68
70
  sizes = /* @__PURE__ */ new Map();
69
- /** Layers whose pixels changed since the last save */
70
- changed = /* @__PURE__ */ new Set();
71
- /** Whether any layer may have changed since the last save (not only those in `changed`) */
72
- checkAll = false;
73
71
  timer;
74
72
  /** The save being written, if any */
75
73
  saving;
@@ -77,12 +75,7 @@ var LayerAutosave = class extends EventTarget {
77
75
  again = false;
78
76
  /** Lets go of the drawing's lock, while held */
79
77
  release;
80
- onLayersChange = () => {
81
- const id = this.watched?.changedLayerId;
82
- if (id) this.changed.add(id);
83
- else this.checkAll = true;
84
- this.schedule();
85
- };
78
+ onLayersChange = () => this.schedule();
86
79
  onCanvasChange = () => {
87
80
  if (this.canvas.layers === this.watched) return;
88
81
  this.watch();
@@ -202,7 +195,7 @@ var LayerAutosave = class extends EventTarget {
202
195
  return;
203
196
  }
204
197
  if (this.currentState !== AutosaveState.Starting) return;
205
- this.prints = /* @__PURE__ */ new Map();
198
+ this.saved = /* @__PURE__ */ new Map();
206
199
  this.watch();
207
200
  if (restored) this.remember();
208
201
  this.setState(AutosaveState.Ready);
@@ -222,29 +215,32 @@ var LayerAutosave = class extends EventTarget {
222
215
  return true;
223
216
  }
224
217
  /**
225
- * Remember each layer's pixels as saved (just restored).
218
+ * Remember each layer as saved (just restored).
226
219
  */
227
220
  remember() {
228
- for (const layer of this.canvas.layers.layers) this.prints.set(layer.id, this.fingerprint(layer.id));
229
- this.changed.clear();
230
- this.checkAll = false;
221
+ const stack = this.canvas.layers;
222
+ for (const layer of stack.layers) this.saved.set(layer.id, this.record(layer));
223
+ }
224
+ /** A layer and its change count, to tell later whether it changed */
225
+ record(layer) {
226
+ return {
227
+ layer,
228
+ version: this.canvas.layers.pixelVersion(layer.id)
229
+ };
231
230
  }
232
231
  /**
233
- * Save the drawing, encoding only the layers whose pixels changed since they were last saved.
232
+ * Save the drawing, encoding only the layers whose pixels changed since they were last saved (or that are new).
234
233
  */
235
234
  async write() {
236
235
  const stack = this.canvas.layers;
237
- const { checkAll, changed } = this;
238
- this.checkAll = false;
239
- this.changed = /* @__PURE__ */ new Set();
240
- const prints = /* @__PURE__ */ new Map();
236
+ const encoded = /* @__PURE__ */ new Map();
241
237
  try {
242
238
  const encoding = [];
243
239
  for (const layer of stack.layers) {
244
- if (!!this.prints.has(layer.id) && !checkAll && !changed.has(layer.id)) continue;
245
- const print = this.fingerprint(layer.id);
246
- if (print === this.prints.get(layer.id)) continue;
247
- prints.set(layer.id, print);
240
+ const record = this.record(layer);
241
+ const saved = this.saved.get(layer.id);
242
+ if (saved?.layer === layer && saved.version === record.version) continue;
243
+ encoded.set(layer.id, record);
248
244
  encoding.push(this.encode(layer.canvas).then((image) => [layer.id, image]));
249
245
  }
250
246
  const drawing = {
@@ -258,13 +254,11 @@ var LayerAutosave = class extends EventTarget {
258
254
  if (this.currentState !== AutosaveState.Ready) return;
259
255
  await this.store.save(drawing, images);
260
256
  const kept = new Set(drawing.layers.map((layer) => layer.id));
261
- for (const id of this.prints.keys()) if (!kept.has(id)) this.prints.delete(id);
262
- for (const [id, print] of prints) this.prints.set(id, print);
257
+ for (const id of this.saved.keys()) if (!kept.has(id)) this.saved.delete(id);
258
+ for (const [id, record] of encoded) this.saved.set(id, record);
263
259
  this.error = void 0;
264
260
  this.rememberSizes(kept, images);
265
261
  } catch (error) {
266
- for (const id of [...changed, ...prints.keys()]) this.changed.add(id);
267
- this.checkAll ||= checkAll;
268
262
  this.fail(error);
269
263
  }
270
264
  }
@@ -279,13 +273,6 @@ var LayerAutosave = class extends EventTarget {
279
273
  if (this.storedBytes !== before) this.dispatchEvent(new Event("change"));
280
274
  }
281
275
  /**
282
- * A fingerprint of a layer's pixels.
283
- */
284
- fingerprint(id) {
285
- const layer = this.canvas.layers.get(id);
286
- return pixelFingerprint(layer.context.getImageData(0, 0, layer.width, layer.height).data);
287
- }
288
- /**
289
276
  * Save once the layers have stopped changing for a moment and drawing pauses.
290
277
  */
291
278
  schedule() {
@@ -29,6 +29,8 @@ export default class LayerStack extends EventTarget {
29
29
  /** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
30
30
  private onlyChanged?;
31
31
  private readonly createCanvas;
32
+ /** How many times each layer's image data has been changed through the stack (see `pixelVersion`) */
33
+ private readonly versions;
32
34
  /** Most layers the stack can have */
33
35
  readonly maxLayers: number;
34
36
  constructor(width: number, height: number, createCanvas?: CanvasFactory, maxLayers?: number);
@@ -71,6 +73,17 @@ export default class LayerStack extends EventTarget {
71
73
  * `markChanged`, e.g. a brush stroke); undefined when anything may have changed.
72
74
  */
73
75
  get changedLayerId(): string | undefined;
76
+ /**
77
+ * How many times a layer's image data has been changed through the stack (`markChanged`, `changeImage`, `clear`,
78
+ * merging into it, and undoing or redoing those), to tell whether it changed since some point without reading its
79
+ * pixels (which is slow on phones). A new layer starts at 0.
80
+ *
81
+ * @example
82
+ * const before = stack.pixelVersion(id);
83
+ * stack.markChanged(id);
84
+ * stack.pixelVersion(id) > before; // true
85
+ */
86
+ pixelVersion(id: string): number;
74
87
  /**
75
88
  * Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
76
89
  */
@@ -150,6 +163,8 @@ export default class LayerStack extends EventTarget {
150
163
  * An undoable change of image data: undo and redo both swap the stored pixels with the layer's, then select the layer.
151
164
  */
152
165
  private imageEntry;
166
+ /** Count a change to a layer's image data */
167
+ private touch;
153
168
  /**
154
169
  * Create a layer with the stack's size and canvas factory.
155
170
  */
@@ -31,6 +31,8 @@ var LayerStack = class extends EventTarget {
31
31
  /** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
32
32
  onlyChanged;
33
33
  createCanvas;
34
+ /** How many times each layer's image data has been changed through the stack (see `pixelVersion`) */
35
+ versions = /* @__PURE__ */ new WeakMap();
34
36
  /** Most layers the stack can have */
35
37
  maxLayers;
36
38
  constructor(width, height, createCanvas = createOffscreenCanvas, maxLayers = 5) {
@@ -130,10 +132,23 @@ var LayerStack = class extends EventTarget {
130
132
  return this.onlyChanged;
131
133
  }
132
134
  /**
135
+ * How many times a layer's image data has been changed through the stack (`markChanged`, `changeImage`, `clear`,
136
+ * merging into it, and undoing or redoing those), to tell whether it changed since some point without reading its
137
+ * pixels (which is slow on phones). A new layer starts at 0.
138
+ *
139
+ * @example
140
+ * const before = stack.pixelVersion(id);
141
+ * stack.markChanged(id);
142
+ * stack.pixelVersion(id) > before; // true
143
+ */
144
+ pixelVersion(id) {
145
+ return this.versions.get(this.get(id)) ?? 0;
146
+ }
147
+ /**
133
148
  * Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
134
149
  */
135
150
  markChanged(id) {
136
- this.get(id);
151
+ this.touch(this.get(id));
137
152
  this.onlyChanged = id;
138
153
  try {
139
154
  this.notify();
@@ -150,6 +165,7 @@ var LayerStack = class extends EventTarget {
150
165
  const area = contentBounds(data, this.width, this.height);
151
166
  if (area) this.recordImage(id, area);
152
167
  layer.clear();
168
+ this.touch(layer);
153
169
  this.notify();
154
170
  }
155
171
  /**
@@ -174,7 +190,9 @@ var LayerStack = class extends EventTarget {
174
190
  */
175
191
  changeImage(id, area, change) {
176
192
  this.recordImage(id, area);
177
- change(this.get(id).context);
193
+ const layer = this.get(id);
194
+ change(layer.context);
195
+ this.touch(layer);
178
196
  this.notify();
179
197
  }
180
198
  /**
@@ -209,6 +227,7 @@ var LayerStack = class extends EventTarget {
209
227
  ctx.globalCompositeOperation = layer.blendMode;
210
228
  ctx.drawImage(layer.canvas, x, y, width, height, x, y, width, height);
211
229
  ctx.restore();
230
+ this.touch(below);
212
231
  }
213
232
  this.items = this.items.filter((l) => l !== layer);
214
233
  this.activeId = below.id;
@@ -332,6 +351,7 @@ var LayerStack = class extends EventTarget {
332
351
  ctx.clearRect(area.x, area.y, area.width, area.height);
333
352
  ctx.drawImage(stored, area.x, area.y);
334
353
  stored = current;
354
+ this.touch(layer);
335
355
  this.select(id);
336
356
  };
337
357
  return {
@@ -339,6 +359,10 @@ var LayerStack = class extends EventTarget {
339
359
  redo: swap
340
360
  };
341
361
  }
362
+ /** Count a change to a layer's image data */
363
+ touch(layer) {
364
+ this.versions.set(layer, this.pixelVersion(layer.id) + 1);
365
+ }
342
366
  /**
343
367
  * Create a layer with the stack's size and canvas factory.
344
368
  */