@lvl99dotcom/paintcanvas 0.4.0 → 0.5.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 (174) hide show
  1. package/README.md +30 -15
  2. package/dist/lib/brushes/presets/round/index.d.ts +1 -1
  3. package/dist/lib/brushes/presets/round/index.js +12 -7
  4. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  5. package/dist/lib/brushes/presets/square/index.js +7 -3
  6. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  7. package/dist/lib/brushes/shapeImage/index.js +15 -9
  8. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  9. package/dist/lib/brushes/shapes/index.js +86 -0
  10. package/dist/lib/classes/BrushSettings/index.d.ts +29 -2
  11. package/dist/lib/classes/BrushSettings/index.js +51 -14
  12. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +31 -2
  13. package/dist/lib/classes/KeyboardShortcuts/index.js +66 -4
  14. package/dist/lib/classes/PaintCanvas/index.d.ts +46 -0
  15. package/dist/lib/classes/PaintCanvas/index.js +67 -0
  16. package/dist/lib/classes/SettingsAutosave/index.d.ts +3 -2
  17. package/dist/lib/classes/SettingsAutosave/index.js +5 -3
  18. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  19. package/dist/lib/classes/UiSettings/index.js +82 -0
  20. package/dist/lib/settings/index.d.ts +1 -1
  21. package/dist/lib/settings/index.js +4 -2
  22. package/dist/lib/settings/schema.d.ts +18 -0
  23. package/dist/lib/settings/schema.js +14 -0
  24. package/dist/lib/validation/validators.generated.js +169 -1
  25. package/dist/locales/en-GB.js +9 -2
  26. package/dist/locales/en-US.js +9 -2
  27. package/dist/locales/fr.js +9 -2
  28. package/dist/locales/ja.js +9 -2
  29. package/dist/locales/ko.js +9 -2
  30. package/dist/locales/zh-Hans.js +9 -2
  31. package/dist/locales/zh-Hant.js +9 -2
  32. package/dist/package.js +1 -1
  33. package/dist/testing/recordingCanvas.js +2 -1
  34. package/dist/ui/base/index.d.ts +2 -2
  35. package/dist/ui/dismiss/index.d.ts +21 -0
  36. package/dist/ui/dismiss/index.js +27 -0
  37. package/dist/ui/drag/index.d.ts +51 -0
  38. package/dist/ui/drag/index.js +81 -0
  39. package/dist/ui/elements/PaintCanvas/index.d.ts +17 -12
  40. package/dist/ui/elements/PaintCanvas/index.js +34 -17
  41. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  42. package/dist/ui/elements/PcAppSettings/index.d.ts +1 -1
  43. package/dist/ui/elements/PcAppSettings/index.js +1 -1
  44. package/dist/ui/elements/PcBrushConfig/index.d.ts +9 -2
  45. package/dist/ui/elements/PcBrushConfig/index.js +47 -7
  46. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  47. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  48. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  49. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  50. package/dist/ui/elements/PcBrushTest/index.js +73 -5
  51. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  52. package/dist/ui/elements/PcCanvas/index.d.ts +21 -1
  53. package/dist/ui/elements/PcCanvas/index.js +53 -10
  54. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  55. package/dist/ui/elements/PcClipboardTools/index.d.ts +48 -0
  56. package/dist/ui/elements/PcClipboardTools/index.js +118 -0
  57. package/dist/ui/elements/PcContextMenu/index.d.ts +2 -2
  58. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  59. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  60. package/dist/ui/elements/PcMenu/index.d.ts +48 -3
  61. package/dist/ui/elements/PcMenu/index.js +111 -19
  62. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  63. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  64. package/dist/ui/elements/PcPane/index.d.ts +3 -1
  65. package/dist/ui/elements/PcPane/index.js +44 -7
  66. package/dist/ui/elements/PcPanel/index.d.ts +2 -2
  67. package/dist/ui/elements/PcPanel/index.js +8 -8
  68. package/dist/ui/elements/PcPanelBrushes/index.d.ts +2 -15
  69. package/dist/ui/elements/PcPanelBrushes/index.js +6 -43
  70. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  71. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  72. package/dist/ui/elements/PcPopover/index.js +72 -4
  73. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  74. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  75. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  76. package/dist/ui/elements/PcToolSettings/index.js +382 -318
  77. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  78. package/dist/ui/elements/PcToolbar/index.d.ts +15 -0
  79. package/dist/ui/elements/PcToolbar/index.js +55 -14
  80. package/dist/ui/elements/PcToolbar/style.js +1 -1
  81. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  82. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  83. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  84. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  85. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  86. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  87. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  88. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  89. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  90. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  91. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  92. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  93. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  94. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  95. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  96. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  97. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  98. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  99. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  100. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  101. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  102. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  103. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  104. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  105. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  106. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  107. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  108. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  109. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  110. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  111. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  112. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  113. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  114. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  115. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  116. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  117. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  118. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  119. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  120. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  121. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  122. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  123. package/dist/ui/icons/index.d.ts +1 -1
  124. package/dist/ui/icons/index.js +84 -72
  125. package/dist/ui/index.d.ts +7 -2
  126. package/dist/ui/index.js +10 -4
  127. package/dist/ui/layout/index.d.ts +9 -1
  128. package/dist/ui/layout/index.js +11 -2
  129. package/dist/ui/tokens/index.d.ts +5 -4
  130. package/dist/ui/tokens/index.js +12 -7
  131. package/locales/en-GB.json +9 -2
  132. package/locales/en-US.json +9 -2
  133. package/locales/fr.json +9 -2
  134. package/locales/ja.json +9 -2
  135. package/locales/ko.json +9 -2
  136. package/locales/zh-Hans.json +9 -2
  137. package/locales/zh-Hant.json +9 -2
  138. package/package.json +11 -4
  139. package/dist/ui/icons/images/brush.js +0 -3
  140. package/dist/ui/icons/images/circle-mask.js +0 -3
  141. package/dist/ui/icons/images/clipboard.js +0 -3
  142. package/dist/ui/icons/images/cursor.js +0 -3
  143. package/dist/ui/icons/images/eraser.js +0 -3
  144. package/dist/ui/icons/images/eyedropper.js +0 -3
  145. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  146. package/dist/ui/icons/images/flip-vertical.js +0 -3
  147. package/dist/ui/icons/images/focus-pen.js +0 -3
  148. package/dist/ui/icons/images/freehand-mask.js +0 -3
  149. package/dist/ui/icons/images/hidden.js +0 -3
  150. package/dist/ui/icons/images/layers.js +0 -3
  151. package/dist/ui/icons/images/linked.js +0 -3
  152. package/dist/ui/icons/images/locked.js +0 -3
  153. package/dist/ui/icons/images/moon.js +0 -3
  154. package/dist/ui/icons/images/pan.js +0 -3
  155. package/dist/ui/icons/images/polygon-mask.js +0 -3
  156. package/dist/ui/icons/images/pressure.js +0 -3
  157. package/dist/ui/icons/images/randomise.js +0 -3
  158. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  159. package/dist/ui/icons/images/redo.js +0 -3
  160. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  161. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  162. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  163. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  164. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  165. package/dist/ui/icons/images/star.js +0 -3
  166. package/dist/ui/icons/images/sun.js +0 -3
  167. package/dist/ui/icons/images/tilt.js +0 -3
  168. package/dist/ui/icons/images/undo.js +0 -3
  169. package/dist/ui/icons/images/unlinked.js +0 -3
  170. package/dist/ui/icons/images/unlocked.js +0 -3
  171. package/dist/ui/icons/images/visible.js +0 -3
  172. package/dist/ui/icons/images/zoom-in.js +0 -3
  173. package/dist/ui/icons/images/zoom-out.js +0 -3
  174. package/dist/ui/icons/images/zoom.js +0 -3
@@ -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 };
@@ -18,6 +18,7 @@ import type { LayerStoreLike } from "../../storage";
18
18
  import type { AssetAuthor, BrushConfig } from "../../brushes/types";
19
19
  import CustomPresets, { type PresetStoreLike } from "../CustomPresets";
20
20
  import InputSettings from "../InputSettings";
21
+ import UiSettings from "../UiSettings";
21
22
  import Palette from "../Palette";
22
23
  import ShortcutSettings from "../ShortcutSettings";
23
24
  import ExternalSheets from "../ExternalSheets";
@@ -125,7 +126,14 @@ export interface PaintCanvasOptions {
125
126
  names?: Partial<PaintCanvasNames>;
126
127
  /** Where to keep the user's layers so the drawing survives reloading the page (see `useLayerStore`) */
127
128
  layerStore?: LayerStoreLike;
129
+ /**
130
+ * The system clipboard, which copying and cutting also put a PNG of the copied image data on so other apps (and brush
131
+ * shapes) can use it (the browser's by default; `null` for none)
132
+ */
133
+ systemClipboard?: SystemClipboardLike | null;
128
134
  }
135
+ /** The part of the system clipboard PaintCanvas writes to (the browser's `navigator.clipboard`) */
136
+ export type SystemClipboardLike = Pick<Clipboard, "write">;
129
137
  /**
130
138
  * The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool, foreground and background colours,
131
139
  * selection, transforms, clipboard, undo/redo history, viewport and input settings. Where its data is kept and who it
@@ -159,6 +167,8 @@ export default class PaintCanvas extends EventTarget {
159
167
  readonly background: BackgroundWork;
160
168
  /** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
161
169
  readonly input: InputSettings;
170
+ /** The user's interface choices (theme and language), kept with the other app settings */
171
+ readonly ui: UiSettings;
162
172
  /** The user's saved colours, shown as swatches */
163
173
  readonly palette: Palette;
164
174
  /** The user's keyboard shortcuts: each configurable action's key combination */
@@ -203,6 +213,10 @@ export default class PaintCanvas extends EventTarget {
203
213
  private currentSelectedColour;
204
214
  /** Image data copied with cut or copy, and the area it was copied from */
205
215
  private clipboard?;
216
+ /** How many times image data has been copied (see `copyCount`) */
217
+ private copies;
218
+ /** The system clipboard copies are also put on, if any */
219
+ private readonly systemClipboard?;
206
220
  /** Drawings from elsewhere */
207
221
  private externalSheets;
208
222
  /** Who receives the drawing messages this canvas sends */
@@ -425,6 +439,26 @@ export default class PaintCanvas extends EventTarget {
425
439
  * nothing during a stroke or while a transform is pending. Returns the area copied, if any.
426
440
  */
427
441
  copy(): Rect | undefined;
442
+ /**
443
+ * How many times image data has been copied or cut, to tell whether it was copied here since something else may have
444
+ * been copied in another app (see `KeyboardShortcuts`).
445
+ */
446
+ get copyCount(): number;
447
+ /** Whether there is copied image data to paste (see `copy` and `cut`) */
448
+ get hasClipboard(): boolean;
449
+ /**
450
+ * The copied image data as a PNG, if there is any (e.g. to use as a brush shape).
451
+ *
452
+ * @example
453
+ * const png = await canvas.copiedPng(); // Blob of type "image/png", or undefined
454
+ */
455
+ copiedPng(): Promise<Blob | undefined>;
456
+ /**
457
+ * Put copied image data on the system clipboard as a PNG, if there is one and the browser can. The item is made
458
+ * straight away (some browsers only allow writing while handling the key press or click) with the PNG to follow.
459
+ * Failing (e.g. the page isn't focused) changes nothing: the image data can still be pasted here.
460
+ */
461
+ private writeSystemClipboard;
428
462
  /**
429
463
  * Copy the masked image data of the selected layer to the clipboard, then remove it from the layer as one undoable
430
464
  * action (see `deleteMasked`).
@@ -471,6 +505,18 @@ export default class PaintCanvas extends EventTarget {
471
505
  * discards it. A pending transform is applied first.
472
506
  */
473
507
  paste(): void;
508
+ /**
509
+ * Show an image from elsewhere (e.g. pasted from another app) over the selected layer, in the middle of the part of
510
+ * the canvas in view, and select the Move tool to place, scale and rotate it, like `paste`. An image larger than the
511
+ * canvas is shrunk to fit it. Does nothing during a stroke.
512
+ *
513
+ * @example
514
+ * canvas.pasteImage(await createImageBitmap(file));
515
+ */
516
+ pasteImage(source: CanvasImageSource & {
517
+ width: number;
518
+ height: number;
519
+ }): void;
474
520
  /**
475
521
  * Apply a finished lasso action to the mask: a shape is added, subtracted or replaces it; a plain click deselects.
476
522
  */
@@ -19,6 +19,7 @@ import "../../presets/index.js";
19
19
  import CustomPresets from "../CustomPresets/index.js";
20
20
  import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
21
21
  import InputSettings from "../InputSettings/index.js";
22
+ import UiSettings from "../UiSettings/index.js";
22
23
  import Palette from "../Palette/index.js";
23
24
  import ShortcutSettings from "../ShortcutSettings/index.js";
24
25
  import { PALETTE_FILE_FORMAT } from "../../palette/schema.js";
@@ -125,6 +126,8 @@ var PaintCanvas = class extends EventTarget {
125
126
  background = new BackgroundWork(this.strokeLog);
126
127
  /** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
127
128
  input = new InputSettings();
129
+ /** The user's interface choices (theme and language), kept with the other app settings */
130
+ ui = new UiSettings();
128
131
  /** The user's saved colours, shown as swatches */
129
132
  palette = new Palette();
130
133
  /** The user's keyboard shortcuts: each configurable action's key combination */
@@ -193,6 +196,10 @@ var PaintCanvas = class extends EventTarget {
193
196
  currentSelectedColour = "foreground";
194
197
  /** Image data copied with cut or copy, and the area it was copied from */
195
198
  clipboard;
199
+ /** How many times image data has been copied (see `copyCount`) */
200
+ copies = 0;
201
+ /** The system clipboard copies are also put on, if any */
202
+ systemClipboard;
196
203
  /** Drawings from elsewhere */
197
204
  externalSheets;
198
205
  /** Who receives the drawing messages this canvas sends */
@@ -233,6 +240,7 @@ var PaintCanvas = class extends EventTarget {
233
240
  this.customPresets.addEventListener("change", () => this.listCustomPresets());
234
241
  this.customPresets.load().catch(() => void 0);
235
242
  this.createCanvas = options.createCanvas ?? createOffscreenCanvas;
243
+ this.systemClipboard = options.systemClipboard === null ? void 0 : options.systemClipboard ?? globalThis.navigator?.clipboard;
236
244
  this.canvasWidth = DEFAULT_CANVAS_SIZE;
237
245
  this.canvasHeight = DEFAULT_CANVAS_SIZE;
238
246
  this.layerStack = new LayerStack(this.canvasWidth, this.canvasHeight, this.createCanvas, this.maxLayers);
@@ -717,9 +725,42 @@ var PaintCanvas = class extends EventTarget {
717
725
  image,
718
726
  area
719
727
  };
728
+ this.copies++;
729
+ this.writeSystemClipboard(image);
730
+ this.notify();
720
731
  return area;
721
732
  }
722
733
  /**
734
+ * How many times image data has been copied or cut, to tell whether it was copied here since something else may have
735
+ * been copied in another app (see `KeyboardShortcuts`).
736
+ */
737
+ get copyCount() {
738
+ return this.copies;
739
+ }
740
+ /** Whether there is copied image data to paste (see `copy` and `cut`) */
741
+ get hasClipboard() {
742
+ return this.clipboard !== void 0;
743
+ }
744
+ /**
745
+ * The copied image data as a PNG, if there is any (e.g. to use as a brush shape).
746
+ *
747
+ * @example
748
+ * const png = await canvas.copiedPng(); // Blob of type "image/png", or undefined
749
+ */
750
+ async copiedPng() {
751
+ return this.clipboard?.image.convertToBlob({ type: "image/png" });
752
+ }
753
+ /**
754
+ * Put copied image data on the system clipboard as a PNG, if there is one and the browser can. The item is made
755
+ * straight away (some browsers only allow writing while handling the key press or click) with the PNG to follow.
756
+ * Failing (e.g. the page isn't focused) changes nothing: the image data can still be pasted here.
757
+ */
758
+ writeSystemClipboard(image) {
759
+ if (!this.systemClipboard || typeof ClipboardItem === "undefined") return;
760
+ const item = new ClipboardItem({ "image/png": image.convertToBlob({ type: "image/png" }) });
761
+ this.systemClipboard.write([item]).catch(() => void 0);
762
+ }
763
+ /**
723
764
  * Copy the masked image data of the selected layer to the clipboard, then remove it from the layer as one undoable
724
765
  * action (see `deleteMasked`).
725
766
  */
@@ -830,6 +871,32 @@ var PaintCanvas = class extends EventTarget {
830
871
  this.transformer.float(this.layerStack.active, clipboard.image, clipboard.area);
831
872
  }
832
873
  /**
874
+ * Show an image from elsewhere (e.g. pasted from another app) over the selected layer, in the middle of the part of
875
+ * the canvas in view, and select the Move tool to place, scale and rotate it, like `paste`. An image larger than the
876
+ * canvas is shrunk to fit it. Does nothing during a stroke.
877
+ *
878
+ * @example
879
+ * canvas.pasteImage(await createImageBitmap(file));
880
+ */
881
+ pasteImage(source) {
882
+ if (this.strokeEngine.drawing) return;
883
+ const scale = Math.min(1, this.canvasWidth / source.width, this.canvasHeight / source.height);
884
+ const width = Math.max(1, Math.round(source.width * scale));
885
+ const height = Math.max(1, Math.round(source.height * scale));
886
+ const image = this.createCanvas(width, height);
887
+ image.getContext("2d").drawImage(source, 0, 0, width, height);
888
+ const view = this.view.visibleRect();
889
+ const area = {
890
+ x: Math.round(view.x + (view.width - width) / 2),
891
+ y: Math.round(view.y + (view.height - height) / 2),
892
+ width,
893
+ height
894
+ };
895
+ if (this.transformer.pending) this.commitTransform();
896
+ this.selectTool(Tool.Move);
897
+ this.transformer.float(this.layerStack.active, image, area);
898
+ }
899
+ /**
833
900
  * Apply a finished lasso action to the mask: a shape is added, subtracted or replaces it; a plain click deselects.
834
901
  */
835
902
  applyLasso(result) {
@@ -10,8 +10,9 @@ export interface SettingsAutosaveOptions {
10
10
  delayMs?: number;
11
11
  }
12
12
  /**
13
- * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette and colours) in a store: the saved
14
- * settings are restored when it starts (invalid ones are ignored), then changes are saved soon after they happen.
13
+ * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, theme and language) in a
14
+ * store: the saved settings are restored when it starts (invalid ones are ignored), then changes are saved soon after
15
+ * they happen.
15
16
  *
16
17
  * Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
17
18
  *
@@ -5,8 +5,9 @@ var SETTINGS_SAVE_DELAY_MS = 500;
5
5
  /** Event dispatched when loading or saving settings fails (detail: the error) */
6
6
  var SETTINGS_AUTOSAVE_ERROR_EVENT = "error";
7
7
  /**
8
- * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette and colours) in a store: the saved
9
- * settings are restored when it starts (invalid ones are ignored), then changes are saved soon after they happen.
8
+ * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, theme and language) in a
9
+ * store: the saved settings are restored when it starts (invalid ones are ignored), then changes are saved soon after
10
+ * they happen.
10
11
  *
11
12
  * Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
12
13
  *
@@ -41,7 +42,8 @@ var SettingsAutosave = class extends EventTarget {
41
42
  app,
42
43
  app.input,
43
44
  app.shortcuts,
44
- app.palette
45
+ app.palette,
46
+ app.ui
45
47
  ];
46
48
  }
47
49
  /**
@@ -0,0 +1,68 @@
1
+ /** The themes: light, dark, or following the device's (system) */
2
+ export declare const THEMES: readonly ["light", "dark", "system"];
3
+ export type Theme = (typeof THEMES)[number];
4
+ /** Longest locale tag kept in UI settings (characters) */
5
+ export declare const MAX_LOCALE_LENGTH = 35;
6
+ /** A locale tag's characters: letters, digits and hyphens (e.g. `zh-Hant`) */
7
+ export declare const LOCALE_PATTERN = "^[A-Za-z0-9-]+$";
8
+ /**
9
+ * Whether a value is one of the themes.
10
+ *
11
+ * @example
12
+ * isTheme("dark"); // true
13
+ * isTheme(undefined); // false
14
+ */
15
+ export declare const isTheme: (value: unknown) => value is Theme;
16
+ /**
17
+ * Whether a value can be a locale tag (letters, digits and hyphens, at most `MAX_LOCALE_LENGTH` characters).
18
+ *
19
+ * @example
20
+ * isLocaleTag("zh-Hant"); // true
21
+ * isLocaleTag("<script>"); // false
22
+ */
23
+ export declare const isLocaleTag: (value: unknown) => value is string;
24
+ /** The user's interface choices */
25
+ export interface UiSettingsData {
26
+ /** The theme (`system` by default) */
27
+ theme: Theme;
28
+ /** The language's locale tag, e.g. `fr`, or undefined to follow the device's languages */
29
+ locale?: string;
30
+ }
31
+ /** UI settings the UI can show and change: PaintCanvas's `UiSettings`, or anything with the same data and `update` */
32
+ export interface UiSettingsLike extends EventTarget {
33
+ readonly data: Readonly<UiSettingsData>;
34
+ update(changes: Partial<UiSettingsData>): void;
35
+ }
36
+ /**
37
+ * Keep only valid UI settings from an untrusted value (e.g. stored or imported), using `defaults` for anything missing
38
+ * or invalid. Other properties are left out.
39
+ *
40
+ * @example
41
+ * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system" }); // { theme: "dark" }
42
+ */
43
+ export declare const sanitiseUiSettings: (input: unknown, defaults: UiSettingsData) => UiSettingsData;
44
+ /**
45
+ * The user's interface choices: the theme and the language. Kept in memory; the implementer saves and restores them
46
+ * with the other app settings (see `SettingsAutosave`), and the UI shows and applies them (e.g. `<pc-theme-picker>` and
47
+ * `<pc-input-settings>`).
48
+ *
49
+ * Dispatches a `change` event when a setting changes.
50
+ *
51
+ * @example
52
+ * const ui = new UiSettings();
53
+ * ui.update({ theme: "dark", locale: "fr" });
54
+ * ui.data.theme; // "dark"
55
+ */
56
+ export default class UiSettings extends EventTarget {
57
+ private values;
58
+ constructor(initial?: unknown);
59
+ /** Current settings (read-only copy) */
60
+ get data(): Readonly<UiSettingsData>;
61
+ /**
62
+ * Change some settings and dispatch `change` if anything changed. Invalid values and other properties are ignored.
63
+ *
64
+ * @example
65
+ * ui.update({ locale: "ja" });
66
+ */
67
+ update(changes: Partial<UiSettingsData>): void;
68
+ }
@@ -0,0 +1,82 @@
1
+ // src/lib/classes/UiSettings/index.ts
2
+ /** The themes: light, dark, or following the device's (system) */
3
+ var THEMES = [
4
+ "light",
5
+ "dark",
6
+ "system"
7
+ ];
8
+ /** Longest locale tag kept in UI settings (characters) */
9
+ var MAX_LOCALE_LENGTH = 35;
10
+ /** A locale tag's characters: letters, digits and hyphens (e.g. `zh-Hant`) */
11
+ var LOCALE_PATTERN = "^[A-Za-z0-9-]+$";
12
+ /**
13
+ * Whether a value is one of the themes.
14
+ *
15
+ * @example
16
+ * isTheme("dark"); // true
17
+ * isTheme(undefined); // false
18
+ */
19
+ var isTheme = (value) => THEMES.includes(value);
20
+ /**
21
+ * Whether a value can be a locale tag (letters, digits and hyphens, at most `MAX_LOCALE_LENGTH` characters).
22
+ *
23
+ * @example
24
+ * isLocaleTag("zh-Hant"); // true
25
+ * isLocaleTag("<script>"); // false
26
+ */
27
+ var isLocaleTag = (value) => typeof value === "string" && value.length <= 35 && (/* @__PURE__ */ new RegExp("^[A-Za-z0-9-]+$")).test(value);
28
+ /**
29
+ * Keep only valid UI settings from an untrusted value (e.g. stored or imported), using `defaults` for anything missing
30
+ * or invalid. Other properties are left out.
31
+ *
32
+ * @example
33
+ * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system" }); // { theme: "dark" }
34
+ */
35
+ var sanitiseUiSettings = (input, defaults) => {
36
+ const value = typeof input === "object" && input !== null ? input : {};
37
+ const theme = isTheme(value.theme) ? value.theme : defaults.theme;
38
+ const locale = isLocaleTag(value.locale) ? value.locale : defaults.locale;
39
+ return locale === void 0 ? { theme } : {
40
+ theme,
41
+ locale
42
+ };
43
+ };
44
+ /**
45
+ * The user's interface choices: the theme and the language. Kept in memory; the implementer saves and restores them
46
+ * with the other app settings (see `SettingsAutosave`), and the UI shows and applies them (e.g. `<pc-theme-picker>` and
47
+ * `<pc-input-settings>`).
48
+ *
49
+ * Dispatches a `change` event when a setting changes.
50
+ *
51
+ * @example
52
+ * const ui = new UiSettings();
53
+ * ui.update({ theme: "dark", locale: "fr" });
54
+ * ui.data.theme; // "dark"
55
+ */
56
+ var UiSettings = class extends EventTarget {
57
+ values;
58
+ constructor(initial) {
59
+ super();
60
+ this.values = sanitiseUiSettings(initial, { theme: "system" });
61
+ }
62
+ /** Current settings (read-only copy) */
63
+ get data() {
64
+ return this.values;
65
+ }
66
+ /**
67
+ * Change some settings and dispatch `change` if anything changed. Invalid values and other properties are ignored.
68
+ *
69
+ * @example
70
+ * ui.update({ locale: "ja" });
71
+ */
72
+ update(changes) {
73
+ const next = sanitiseUiSettings({
74
+ ...this.values,
75
+ ...changes
76
+ }, this.values);
77
+ if (next.theme === this.values.theme && next.locale === this.values.locale) return;
78
+ this.values = next;
79
+ this.dispatchEvent(new Event("change"));
80
+ }
81
+ };
82
+ export { LOCALE_PATTERN, MAX_LOCALE_LENGTH, THEMES, UiSettings as default, isLocaleTag, isTheme, sanitiseUiSettings };
@@ -25,7 +25,7 @@ export type SettingsValidationResult = {
25
25
  */
26
26
  export declare const isValidSettings: (input: unknown) => SettingsValidationResult;
27
27
  /**
28
- * The app settings of a canvas: its input settings, keyboard shortcuts, colour palette and colours.
28
+ * The app settings of a canvas: its input settings, keyboard shortcuts, colour palette, colours, theme and language.
29
29
  *
30
30
  * @example
31
31
  * collectSettings(canvas).palette; // ["#2c6bff", ...]
@@ -23,7 +23,7 @@ var isValidSettings = (input) => validateSettings(input) ? {
23
23
  errors: formatErrors(validateSettings.errors)
24
24
  };
25
25
  /**
26
- * The app settings of a canvas: its input settings, keyboard shortcuts, colour palette and colours.
26
+ * The app settings of a canvas: its input settings, keyboard shortcuts, colour palette, colours, theme and language.
27
27
  *
28
28
  * @example
29
29
  * collectSettings(canvas).palette; // ["#2c6bff", ...]
@@ -32,7 +32,8 @@ var collectSettings = (app) => ({
32
32
  input: structuredClone(app.input.data),
33
33
  shortcuts: structuredClone(app.shortcuts.bindings),
34
34
  palette: [...app.palette.colours],
35
- colours: { ...app.colours }
35
+ colours: { ...app.colours },
36
+ ui: { ...app.ui.data }
36
37
  });
37
38
  /**
38
39
  * Use app settings on a canvas, e.g. ones kept in the browser or imported. Parts left out stay as they are; invalid
@@ -49,6 +50,7 @@ var applySettings = (app, settings) => {
49
50
  app.setColour(settings.colours.foreground, "foreground");
50
51
  app.setColour(settings.colours.background, "background");
51
52
  }
53
+ if (settings.ui) app.ui.update(settings.ui);
52
54
  };
53
55
  /**
54
56
  * The file name settings are exported to on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
@@ -1,4 +1,5 @@
1
1
  import type { InputSettingsData } from "../classes/InputSettings";
2
+ import { type UiSettingsData } from "../classes/UiSettings/index.ts";
2
3
  import type { KeyCombo } from "../utils/shortcuts";
3
4
  /** The `format` of a settings file, unless the implementer names their own (see `PaintCanvasNames`) */
4
5
  export declare const SETTINGS_FILE_FORMAT = "paintcanvas-settings";
@@ -23,6 +24,8 @@ export interface AppSettings {
23
24
  foreground: string;
24
25
  background: string;
25
26
  };
27
+ /** The theme and language */
28
+ ui?: Partial<UiSettingsData>;
26
29
  }
27
30
  /** A settings file: what users export and import */
28
31
  export interface SettingsFile {
@@ -126,6 +129,21 @@ export declare const settingsSchema: {
126
129
  };
127
130
  };
128
131
  };
132
+ readonly ui: {
133
+ readonly type: "object";
134
+ readonly additionalProperties: false;
135
+ readonly properties: {
136
+ readonly theme: {
137
+ readonly enum: readonly ["light", "dark", "system"];
138
+ };
139
+ readonly locale: {
140
+ readonly type: "string";
141
+ readonly minLength: 1;
142
+ readonly maxLength: 35;
143
+ readonly pattern: "^[A-Za-z0-9-]+$";
144
+ };
145
+ };
146
+ };
129
147
  };
130
148
  };
131
149
  /** Schema of a settings file */
@@ -1,3 +1,4 @@
1
+ import { LOCALE_PATTERN, THEMES } from "../classes/UiSettings/index.js";
1
2
  import { HEX_COLOUR_PATTERN, MAX_PALETTE_COLOURS, paletteFileSchema } from "../palette/schema.js";
2
3
  // src/lib/settings/schema.ts
3
4
  /** The `format` of a settings file, unless the implementer names their own (see `PaintCanvasNames`) */
@@ -76,6 +77,19 @@ var settingsSchema = {
76
77
  foreground: hexColour,
77
78
  background: hexColour
78
79
  }
80
+ },
81
+ ui: {
82
+ type: "object",
83
+ additionalProperties: false,
84
+ properties: {
85
+ theme: { enum: THEMES },
86
+ locale: {
87
+ type: "string",
88
+ minLength: 1,
89
+ maxLength: 35,
90
+ pattern: LOCALE_PATTERN
91
+ }
92
+ }
79
93
  }
80
94
  }
81
95
  };