@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
@@ -18,7 +18,9 @@ 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";
23
+ import ProfileGraphs from "../ProfileGraphs";
22
24
  import ShortcutSettings from "../ShortcutSettings";
23
25
  import ExternalSheets from "../ExternalSheets";
24
26
  import { type CanvasMessage, type PatchMessage, type StrokeMessage } from "../../messages";
@@ -113,7 +115,11 @@ export interface PaintCanvasOptions {
113
115
  maxCanvasSize?: number;
114
116
  /** Most layers the user can have (default `DEFAULT_MAX_LAYERS`) */
115
117
  maxLayers?: number;
116
- /** Creates layer canvases (replaceable for tests) */
118
+ /**
119
+ * Creates the canvases the engine draws in: layers, stroke buffers, masks and copies. `createMemoryCanvas` by default,
120
+ * keeping pixels in regular memory so saving and copying don't stall phones; pass `createOffscreenCanvas` to keep them
121
+ * on the GPU instead (replaceable for tests).
122
+ */
117
123
  createCanvas?: CanvasFactory;
118
124
  /** Where custom brush presets are kept (defaults to keeping them in memory only) */
119
125
  presetStore?: PresetStoreLike;
@@ -125,7 +131,14 @@ export interface PaintCanvasOptions {
125
131
  names?: Partial<PaintCanvasNames>;
126
132
  /** Where to keep the user's layers so the drawing survives reloading the page (see `useLayerStore`) */
127
133
  layerStore?: LayerStoreLike;
134
+ /**
135
+ * The system clipboard, which copying and cutting also put a PNG of the copied image data on so other apps (and brush
136
+ * shapes) can use it (the browser's by default; `null` for none)
137
+ */
138
+ systemClipboard?: SystemClipboardLike | null;
128
139
  }
140
+ /** The part of the system clipboard PaintCanvas writes to (the browser's `navigator.clipboard`) */
141
+ export type SystemClipboardLike = Pick<Clipboard, "write">;
129
142
  /**
130
143
  * The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool, foreground and background colours,
131
144
  * selection, transforms, clipboard, undo/redo history, viewport and input settings. Where its data is kept and who it
@@ -159,8 +172,12 @@ export default class PaintCanvas extends EventTarget {
159
172
  readonly background: BackgroundWork;
160
173
  /** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
161
174
  readonly input: InputSettings;
175
+ /** The user's interface choices (theme and language), kept with the other app settings */
176
+ readonly ui: UiSettings;
162
177
  /** The user's saved colours, shown as swatches */
163
178
  readonly palette: Palette;
179
+ /** The user's own profile graphs (and the presets), for shaping how brush settings vary */
180
+ readonly profiles: ProfileGraphs;
164
181
  /** The user's keyboard shortcuts: each configurable action's key combination */
165
182
  readonly shortcuts: ShortcutSettings;
166
183
  /** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
@@ -203,6 +220,10 @@ export default class PaintCanvas extends EventTarget {
203
220
  private currentSelectedColour;
204
221
  /** Image data copied with cut or copy, and the area it was copied from */
205
222
  private clipboard?;
223
+ /** How many times image data has been copied (see `copyCount`) */
224
+ private copies;
225
+ /** The system clipboard copies are also put on, if any */
226
+ private readonly systemClipboard?;
206
227
  /** Drawings from elsewhere */
207
228
  private externalSheets;
208
229
  /** Who receives the drawing messages this canvas sends */
@@ -425,6 +446,40 @@ export default class PaintCanvas extends EventTarget {
425
446
  * nothing during a stroke or while a transform is pending. Returns the area copied, if any.
426
447
  */
427
448
  copy(): Rect | undefined;
449
+ /**
450
+ * Copy the masked image data of all the shown layers, mixed as they're shown (each layer's opacity and blend mode,
451
+ * without the white background), to the clipboard. Hidden layers are left out. Like `copy`, it needs a mask with image
452
+ * data inside it, and does nothing during a stroke or while a transform is pending. Returns the area copied, if any.
453
+ *
454
+ * @example
455
+ * app.selection.selectAll();
456
+ * app.copyMerged(); // { x: 10, y: 20, width: 30, height: 5 }: the part of the canvas with paint on any shown layer
457
+ */
458
+ copyMerged(): Rect | undefined;
459
+ /**
460
+ * Copy the masked part of a canvas-sized image to the clipboard (see `copy`), returning the area copied, if any.
461
+ */
462
+ private copyMasked;
463
+ /**
464
+ * How many times image data has been copied or cut, to tell whether it was copied here since something else may have
465
+ * been copied in another app (see `KeyboardShortcuts`).
466
+ */
467
+ get copyCount(): number;
468
+ /** Whether there is copied image data to paste (see `copy` and `cut`) */
469
+ get hasClipboard(): boolean;
470
+ /**
471
+ * The copied image data as a PNG, if there is any (e.g. to use as a brush shape).
472
+ *
473
+ * @example
474
+ * const png = await canvas.copiedPng(); // Blob of type "image/png", or undefined
475
+ */
476
+ copiedPng(): Promise<Blob | undefined>;
477
+ /**
478
+ * Put copied image data on the system clipboard as a PNG, if there is one and the browser can. The item is made
479
+ * straight away (some browsers only allow writing while handling the key press or click) with the PNG to follow.
480
+ * Failing (e.g. the page isn't focused) changes nothing: the image data can still be pasted here.
481
+ */
482
+ private writeSystemClipboard;
428
483
  /**
429
484
  * Copy the masked image data of the selected layer to the clipboard, then remove it from the layer as one undoable
430
485
  * action (see `deleteMasked`).
@@ -450,9 +505,11 @@ export default class PaintCanvas extends EventTarget {
450
505
  * data when its alpha lock is on. None if that is empty.
451
506
  */
452
507
  private fillArea;
508
+ /** Whether image data can be copied now: not during a stroke or while a transform is pending */
509
+ private get canCopy();
453
510
  /**
454
- * The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
455
- * during a stroke or while a transform is pending.
511
+ * The bounds of a canvas-sized image's data (the selected layer's by default) inside the mask, if there is a mask
512
+ * with image data inside it. None during a stroke or while a transform is pending.
456
513
  */
457
514
  private maskedArea;
458
515
  /**
@@ -471,6 +528,18 @@ export default class PaintCanvas extends EventTarget {
471
528
  * discards it. A pending transform is applied first.
472
529
  */
473
530
  paste(): void;
531
+ /**
532
+ * Show an image from elsewhere (e.g. pasted from another app) over the selected layer, in the middle of the part of
533
+ * the canvas in view, and select the Move tool to place, scale and rotate it, like `paste`. An image larger than the
534
+ * canvas is shrunk to fit it. Does nothing during a stroke.
535
+ *
536
+ * @example
537
+ * canvas.pasteImage(await createImageBitmap(file));
538
+ */
539
+ pasteImage(source: CanvasImageSource & {
540
+ width: number;
541
+ height: number;
542
+ }): void;
474
543
  /**
475
544
  * Apply a finished lasso action to the mask: a shape is added, subtracted or replaces it; a plain click deselects.
476
545
  */
@@ -11,17 +11,19 @@ import Selection from "../../selection/Selection/index.js";
11
11
  import Lasso from "../../selection/Lasso/index.js";
12
12
  import Transformer, { applyMask, contentBounds, copyArea, drawTransformed } from "../../transform/Transformer/index.js";
13
13
  import { boxBounds } from "../../transform/geometry/index.js";
14
- import Layer, { createOffscreenCanvas } from "../Layer/index.js";
14
+ import Layer, { createMemoryCanvas } from "../Layer/index.js";
15
15
  import LayerAutosave from "../LayerAutosave/index.js";
16
16
  import { brushPresets, eraserPresets } from "../../brushes/index.js";
17
17
  import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
18
18
  import "../../presets/index.js";
19
- import CustomPresets from "../CustomPresets/index.js";
20
19
  import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
20
+ import CustomPresets from "../CustomPresets/index.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
- import ShortcutSettings from "../ShortcutSettings/index.js";
24
24
  import { PALETTE_FILE_FORMAT } from "../../palette/schema.js";
25
+ import ProfileGraphs from "../ProfileGraphs/index.js";
26
+ import ShortcutSettings from "../ShortcutSettings/index.js";
25
27
  import "../../palette/index.js";
26
28
  import { SETTINGS_FILE_FORMAT } from "../../settings/schema.js";
27
29
  import ExternalSheets from "../ExternalSheets/index.js";
@@ -125,8 +127,12 @@ var PaintCanvas = class extends EventTarget {
125
127
  background = new BackgroundWork(this.strokeLog);
126
128
  /** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
127
129
  input = new InputSettings();
130
+ /** The user's interface choices (theme and language), kept with the other app settings */
131
+ ui = new UiSettings();
128
132
  /** The user's saved colours, shown as swatches */
129
133
  palette = new Palette();
134
+ /** The user's own profile graphs (and the presets), for shaping how brush settings vary */
135
+ profiles = new ProfileGraphs();
130
136
  /** The user's keyboard shortcuts: each configurable action's key combination */
131
137
  shortcuts = new ShortcutSettings();
132
138
  /** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
@@ -193,6 +199,10 @@ var PaintCanvas = class extends EventTarget {
193
199
  currentSelectedColour = "foreground";
194
200
  /** Image data copied with cut or copy, and the area it was copied from */
195
201
  clipboard;
202
+ /** How many times image data has been copied (see `copyCount`) */
203
+ copies = 0;
204
+ /** The system clipboard copies are also put on, if any */
205
+ systemClipboard;
196
206
  /** Drawings from elsewhere */
197
207
  externalSheets;
198
208
  /** Who receives the drawing messages this canvas sends */
@@ -232,7 +242,8 @@ var PaintCanvas = class extends EventTarget {
232
242
  this.customPresets = new CustomPresets(this.ownerId, options.presetStore);
233
243
  this.customPresets.addEventListener("change", () => this.listCustomPresets());
234
244
  this.customPresets.load().catch(() => void 0);
235
- this.createCanvas = options.createCanvas ?? createOffscreenCanvas;
245
+ this.createCanvas = options.createCanvas ?? createMemoryCanvas;
246
+ this.systemClipboard = options.systemClipboard === null ? void 0 : options.systemClipboard ?? globalThis.navigator?.clipboard;
236
247
  this.canvasWidth = DEFAULT_CANVAS_SIZE;
237
248
  this.canvasHeight = DEFAULT_CANVAS_SIZE;
238
249
  this.layerStack = new LayerStack(this.canvasWidth, this.canvasHeight, this.createCanvas, this.maxLayers);
@@ -707,19 +718,72 @@ var PaintCanvas = class extends EventTarget {
707
718
  * nothing during a stroke or while a transform is pending. Returns the area copied, if any.
708
719
  */
709
720
  copy() {
710
- const area = this.maskedArea();
721
+ return this.copyMasked(this.layerStack.active.canvas);
722
+ }
723
+ /**
724
+ * Copy the masked image data of all the shown layers, mixed as they're shown (each layer's opacity and blend mode,
725
+ * without the white background), to the clipboard. Hidden layers are left out. Like `copy`, it needs a mask with image
726
+ * data inside it, and does nothing during a stroke or while a transform is pending. Returns the area copied, if any.
727
+ *
728
+ * @example
729
+ * app.selection.selectAll();
730
+ * app.copyMerged(); // { x: 10, y: 20, width: 30, height: 5 }: the part of the canvas with paint on any shown layer
731
+ */
732
+ copyMerged() {
733
+ if (!this.canCopy) return void 0;
734
+ const merged = this.createCanvas(this.width, this.height);
735
+ drawLayers(merged.getContext("2d"), this.layerStack.layers);
736
+ return this.copyMasked(merged);
737
+ }
738
+ /**
739
+ * Copy the masked part of a canvas-sized image to the clipboard (see `copy`), returning the area copied, if any.
740
+ */
741
+ copyMasked(source) {
742
+ const area = this.maskedArea(source);
711
743
  const mask = this.currentSelection.mask;
712
744
  if (!area || !mask) return void 0;
713
- const layer = this.layerStack.active;
714
- const image = copyArea(this.createCanvas, layer.canvas, area);
745
+ const image = copyArea(this.createCanvas, source, area);
715
746
  applyMask(image, copyArea(this.createCanvas, mask, area));
716
747
  this.clipboard = {
717
748
  image,
718
749
  area
719
750
  };
751
+ this.copies++;
752
+ this.writeSystemClipboard(image);
753
+ this.notify();
720
754
  return area;
721
755
  }
722
756
  /**
757
+ * How many times image data has been copied or cut, to tell whether it was copied here since something else may have
758
+ * been copied in another app (see `KeyboardShortcuts`).
759
+ */
760
+ get copyCount() {
761
+ return this.copies;
762
+ }
763
+ /** Whether there is copied image data to paste (see `copy` and `cut`) */
764
+ get hasClipboard() {
765
+ return this.clipboard !== void 0;
766
+ }
767
+ /**
768
+ * The copied image data as a PNG, if there is any (e.g. to use as a brush shape).
769
+ *
770
+ * @example
771
+ * const png = await canvas.copiedPng(); // Blob of type "image/png", or undefined
772
+ */
773
+ async copiedPng() {
774
+ return this.clipboard?.image.convertToBlob({ type: "image/png" });
775
+ }
776
+ /**
777
+ * Put copied image data on the system clipboard as a PNG, if there is one and the browser can. The item is made
778
+ * straight away (some browsers only allow writing while handling the key press or click) with the PNG to follow.
779
+ * Failing (e.g. the page isn't focused) changes nothing: the image data can still be pasted here.
780
+ */
781
+ writeSystemClipboard(image) {
782
+ if (!this.systemClipboard || typeof ClipboardItem === "undefined") return;
783
+ const item = new ClipboardItem({ "image/png": image.convertToBlob({ type: "image/png" }) });
784
+ this.systemClipboard.write([item]).catch(() => void 0);
785
+ }
786
+ /**
723
787
  * Copy the masked image data of the selected layer to the clipboard, then remove it from the layer as one undoable
724
788
  * action (see `deleteMasked`).
725
789
  */
@@ -766,15 +830,19 @@ var PaintCanvas = class extends EventTarget {
766
830
  height
767
831
  };
768
832
  }
833
+ /** Whether image data can be copied now: not during a stroke or while a transform is pending */
834
+ get canCopy() {
835
+ return !this.strokeEngine.drawing && !this.transformer.pending;
836
+ }
769
837
  /**
770
- * The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
771
- * during a stroke or while a transform is pending.
838
+ * The bounds of a canvas-sized image's data (the selected layer's by default) inside the mask, if there is a mask
839
+ * with image data inside it. None during a stroke or while a transform is pending.
772
840
  */
773
- maskedArea() {
841
+ maskedArea(source = this.layerStack.active.canvas) {
774
842
  const mask = this.currentSelection.mask;
775
- if (!mask || this.strokeEngine.drawing || this.transformer.pending) return void 0;
843
+ if (!mask || !this.canCopy) return void 0;
776
844
  const { width, height } = this;
777
- return contentBounds(this.layerStack.active.context.getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
845
+ return contentBounds(source.getContext("2d").getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
778
846
  }
779
847
  /**
780
848
  * Erase the mask's part of an area from the selected layer, or paint it with the background colour when the layer's
@@ -830,6 +898,32 @@ var PaintCanvas = class extends EventTarget {
830
898
  this.transformer.float(this.layerStack.active, clipboard.image, clipboard.area);
831
899
  }
832
900
  /**
901
+ * Show an image from elsewhere (e.g. pasted from another app) over the selected layer, in the middle of the part of
902
+ * the canvas in view, and select the Move tool to place, scale and rotate it, like `paste`. An image larger than the
903
+ * canvas is shrunk to fit it. Does nothing during a stroke.
904
+ *
905
+ * @example
906
+ * canvas.pasteImage(await createImageBitmap(file));
907
+ */
908
+ pasteImage(source) {
909
+ if (this.strokeEngine.drawing) return;
910
+ const scale = Math.min(1, this.canvasWidth / source.width, this.canvasHeight / source.height);
911
+ const width = Math.max(1, Math.round(source.width * scale));
912
+ const height = Math.max(1, Math.round(source.height * scale));
913
+ const image = this.createCanvas(width, height);
914
+ image.getContext("2d").drawImage(source, 0, 0, width, height);
915
+ const view = this.view.visibleRect();
916
+ const area = {
917
+ x: Math.round(view.x + (view.width - width) / 2),
918
+ y: Math.round(view.y + (view.height - height) / 2),
919
+ width,
920
+ height
921
+ };
922
+ if (this.transformer.pending) this.commitTransform();
923
+ this.selectTool(Tool.Move);
924
+ this.transformer.float(this.layerStack.active, image, area);
925
+ }
926
+ /**
833
927
  * Apply a finished lasso action to the mask: a shape is added, subtracted or replaces it; a plain click deselects.
834
928
  */
835
929
  applyLasso(result) {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Encode a canvas as a PNG on the page's own thread (which blocks the page while it runs: seconds on phones for a large
3
+ * canvas).
4
+ *
5
+ * @example
6
+ * const png = await encodePngHere(layer.canvas);
7
+ */
8
+ export declare const encodePngHere: (canvas: OffscreenCanvas) => Promise<Blob>;
9
+ /** The worker's code: draws the image it's sent onto its own canvas and encodes that as a PNG */
10
+ export declare const PNG_WORKER_SOURCE = "self.onmessage = async (event) => {\n const { id, bitmap } = event.data;\n try {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n canvas.getContext(\"2d\").drawImage(bitmap, 0, 0);\n bitmap.close();\n self.postMessage({ id, blob: await canvas.convertToBlob({ type: \"image/png\" }) });\n } catch (error) {\n self.postMessage({ id, error: String(error) });\n }\n};";
11
+ /** Makes the worker, or gives null where workers can't be used */
12
+ export type WorkerFactory = () => Worker | null;
13
+ /**
14
+ * Make the PNG worker from its code (no separate file, so any bundler works), or null where there are no workers or
15
+ * `createImageBitmap`, or the page doesn't allow workers from `blob:` addresses.
16
+ */
17
+ export declare const createPngWorker: WorkerFactory;
18
+ /**
19
+ * Encodes canvases as PNGs in a background worker, so the page keeps responding while large layers are saved. The
20
+ * canvas is copied (`createImageBitmap`) and sent to the worker, which encodes the copy. Where a worker can't be used,
21
+ * or it fails, it encodes on the page's own thread instead (see `encodePngHere`).
22
+ *
23
+ * @example
24
+ * const encoder = new PngEncoder();
25
+ * const png = await encoder.encode(layer.canvas);
26
+ */
27
+ export default class PngEncoder {
28
+ private readonly createWorker;
29
+ /** The worker, once made (null when it can't be used) */
30
+ private worker?;
31
+ private nextId;
32
+ /** Encodings waiting for the worker, by ID */
33
+ private readonly waiting;
34
+ constructor(createWorker?: WorkerFactory);
35
+ /**
36
+ * Encode a canvas as a PNG, in the worker where it can be used.
37
+ */
38
+ encode: (canvas: OffscreenCanvas) => Promise<Blob>;
39
+ /** The worker, made on first use */
40
+ private getWorker;
41
+ }
@@ -0,0 +1,104 @@
1
+ // src/lib/classes/PngEncoder/index.ts
2
+ /**
3
+ * Encode a canvas as a PNG on the page's own thread (which blocks the page while it runs: seconds on phones for a large
4
+ * canvas).
5
+ *
6
+ * @example
7
+ * const png = await encodePngHere(layer.canvas);
8
+ */
9
+ var encodePngHere = (canvas) => {
10
+ canvas.getContext("2d");
11
+ return canvas.convertToBlob({ type: "image/png" });
12
+ };
13
+ /** The worker's code: draws the image it's sent onto its own canvas and encodes that as a PNG */
14
+ var PNG_WORKER_SOURCE = `self.onmessage = async (event) => {
15
+ const { id, bitmap } = event.data;
16
+ try {
17
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
18
+ canvas.getContext("2d").drawImage(bitmap, 0, 0);
19
+ bitmap.close();
20
+ self.postMessage({ id, blob: await canvas.convertToBlob({ type: "image/png" }) });
21
+ } catch (error) {
22
+ self.postMessage({ id, error: String(error) });
23
+ }
24
+ };`;
25
+ /**
26
+ * Make the PNG worker from its code (no separate file, so any bundler works), or null where there are no workers or
27
+ * `createImageBitmap`, or the page doesn't allow workers from `blob:` addresses.
28
+ */
29
+ var createPngWorker = () => {
30
+ if (typeof Worker === "undefined" || typeof createImageBitmap === "undefined") return null;
31
+ try {
32
+ const url = URL.createObjectURL(new Blob([PNG_WORKER_SOURCE], { type: "text/javascript" }));
33
+ return new Worker(url);
34
+ } catch {
35
+ return null;
36
+ }
37
+ };
38
+ /**
39
+ * Encodes canvases as PNGs in a background worker, so the page keeps responding while large layers are saved. The
40
+ * canvas is copied (`createImageBitmap`) and sent to the worker, which encodes the copy. Where a worker can't be used,
41
+ * or it fails, it encodes on the page's own thread instead (see `encodePngHere`).
42
+ *
43
+ * @example
44
+ * const encoder = new PngEncoder();
45
+ * const png = await encoder.encode(layer.canvas);
46
+ */
47
+ var PngEncoder = class {
48
+ createWorker;
49
+ /** The worker, once made (null when it can't be used) */
50
+ worker;
51
+ nextId = 0;
52
+ /** Encodings waiting for the worker, by ID */
53
+ waiting = /* @__PURE__ */ new Map();
54
+ constructor(createWorker = createPngWorker) {
55
+ this.createWorker = createWorker;
56
+ }
57
+ /**
58
+ * Encode a canvas as a PNG, in the worker where it can be used.
59
+ */
60
+ encode = async (canvas) => {
61
+ const worker = this.getWorker();
62
+ if (!worker) return encodePngHere(canvas);
63
+ try {
64
+ canvas.getContext("2d");
65
+ const bitmap = await createImageBitmap(canvas);
66
+ const id = this.nextId++;
67
+ return await new Promise((resolve, reject) => {
68
+ this.waiting.set(id, {
69
+ resolve,
70
+ reject
71
+ });
72
+ worker.postMessage({
73
+ id,
74
+ bitmap
75
+ }, [bitmap]);
76
+ });
77
+ } catch {
78
+ return encodePngHere(canvas);
79
+ }
80
+ };
81
+ /** The worker, made on first use */
82
+ getWorker() {
83
+ if (this.worker !== void 0) return this.worker;
84
+ const worker = this.createWorker();
85
+ this.worker = worker;
86
+ if (!worker) return null;
87
+ worker.addEventListener("message", (event) => {
88
+ const { id, blob, error } = event.data;
89
+ const waiting = this.waiting.get(id);
90
+ if (!waiting) return;
91
+ this.waiting.delete(id);
92
+ if (blob) waiting.resolve(blob);
93
+ else waiting.reject(new Error(error));
94
+ });
95
+ worker.addEventListener("error", () => {
96
+ worker.terminate();
97
+ this.worker = null;
98
+ for (const { reject } of this.waiting.values()) reject(/* @__PURE__ */ new Error("PNG worker failed"));
99
+ this.waiting.clear();
100
+ });
101
+ return worker;
102
+ }
103
+ };
104
+ export { PNG_WORKER_SOURCE, createPngWorker, PngEncoder as default, encodePngHere };
@@ -0,0 +1,87 @@
1
+ import { type ProfileGraph, type ProfilePoint } from "../../profiles";
2
+ /**
3
+ * Keep only valid points from an untrusted value, in order from left to right: each on the grid, with a known
4
+ * interpolation. Undefined if there aren't 2 to `MAX_PROFILE_POINTS` of them.
5
+ *
6
+ * @example
7
+ * sanitisePoints([{ x: 10, y: 0, i: "linear" }, { x: 0, y: -10, i: "linear" }]); // sorted by x
8
+ */
9
+ export declare const sanitisePoints: (input: unknown) => ProfilePoint[] | undefined;
10
+ /**
11
+ * Keep only valid profile graphs from an untrusted value (e.g. stored or imported): each with an ID and name and valid
12
+ * points (see `sanitisePoints`), each ID once and none of the presets', up to `MAX_SAVED_PROFILES`. Undefined if the
13
+ * value isn't an array.
14
+ *
15
+ * @example
16
+ * sanitiseProfiles([{ id: "a", name: "Soft", points: [...] }, { id: "a", name: "Again", points: [...] }]); // the first
17
+ */
18
+ export declare const sanitiseProfiles: (input: unknown) => ProfileGraph[] | undefined;
19
+ /** What a profile graph is saved from: its name and points, and its ID when replacing one */
20
+ export interface ProfileGraphDraft {
21
+ id?: string;
22
+ name: string;
23
+ points: readonly ProfilePoint[];
24
+ }
25
+ /**
26
+ * The user's own profile graphs, alongside the ones PaintCanvas comes with (`PROFILE_PRESETS`). Kept in memory; the app
27
+ * settings save and restore them (see `set` and the `change` event). Each gets a UUID when it's first saved.
28
+ *
29
+ * Dispatches a `change` event when the user's graphs change.
30
+ *
31
+ * @example
32
+ * const profiles = new ProfileGraphs();
33
+ * const soft = profiles.save({ name: "Soft", points: PROFILE_PRESETS[1].points });
34
+ * profiles.get(soft.id)?.name; // "Soft"
35
+ */
36
+ export default class ProfileGraphs extends EventTarget {
37
+ private values;
38
+ /** The user's own graphs, in the order they were first saved */
39
+ get graphs(): readonly ProfileGraph[];
40
+ /** Every graph: the presets, then the user's own */
41
+ get all(): readonly ProfileGraph[];
42
+ /**
43
+ * A graph by its ID: a preset or one of the user's own.
44
+ *
45
+ * @example
46
+ * profiles.get("overweight")?.name; // "Overweight"
47
+ */
48
+ get(id: string | undefined): ProfileGraph | undefined;
49
+ /** Whether a graph is one PaintCanvas comes with (so it can't be renamed, replaced or deleted) */
50
+ isPreset(id: string | undefined): boolean;
51
+ /**
52
+ * One of the user's own graphs with a name, ignoring case and spaces around it.
53
+ *
54
+ * @example
55
+ * profiles.findByName(" soft "); // the graph called "Soft"
56
+ */
57
+ findByName(name: string): ProfileGraph | undefined;
58
+ /**
59
+ * Save a graph as one of the user's own: as a new one (with a new UUID), or in place of the one with its ID. Returns
60
+ * the saved graph, or undefined if it can't be saved (no name, invalid points, a preset's ID, or no room).
61
+ *
62
+ * @example
63
+ * profiles.save({ name: "Soft", points }); // { id: "1b9d…", name: "Soft", points }
64
+ */
65
+ save(draft: ProfileGraphDraft): ProfileGraph | undefined;
66
+ /**
67
+ * Rename one of the user's own graphs. Returns whether it was renamed.
68
+ *
69
+ * @example
70
+ * profiles.rename(id, "Softer");
71
+ */
72
+ rename(id: string, name: string): boolean;
73
+ /**
74
+ * Delete one of the user's own graphs. Returns whether it was deleted.
75
+ *
76
+ * @example
77
+ * profiles.remove(id);
78
+ */
79
+ remove(id: string): boolean;
80
+ /**
81
+ * Replace all the user's graphs (e.g. when restoring saved ones), keeping only valid ones (see `sanitiseProfiles`).
82
+ *
83
+ * @example
84
+ * profiles.set(settings.profiles);
85
+ */
86
+ set(graphs: unknown): void;
87
+ }