@lvl99dotcom/paintcanvas 0.7.0 → 0.8.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 (89) hide show
  1. package/README.md +1 -0
  2. package/dist/lib/assets/index.d.ts +92 -0
  3. package/dist/lib/assets/index.js +106 -0
  4. package/dist/lib/assets/schema.d.ts +151 -0
  5. package/dist/lib/assets/schema.js +76 -0
  6. package/dist/lib/brushes/presetFile/index.d.ts +6 -27
  7. package/dist/lib/brushes/presetFile/index.js +34 -47
  8. package/dist/lib/brushes/types/index.d.ts +2 -11
  9. package/dist/lib/classes/AssetSync/index.d.ts +45 -0
  10. package/dist/lib/classes/AssetSync/index.js +80 -0
  11. package/dist/lib/classes/ColourPalettes/index.d.ts +83 -0
  12. package/dist/lib/classes/ColourPalettes/index.js +165 -0
  13. package/dist/lib/classes/CustomPresets/index.d.ts +14 -15
  14. package/dist/lib/classes/CustomPresets/index.js +25 -10
  15. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -2
  16. package/dist/lib/classes/LayerAutosave/index.js +16 -6
  17. package/dist/lib/classes/PaintCanvas/index.d.ts +111 -23
  18. package/dist/lib/classes/PaintCanvas/index.js +219 -58
  19. package/dist/lib/classes/Palette/index.d.ts +3 -2
  20. package/dist/lib/classes/Palette/index.js +3 -2
  21. package/dist/lib/classes/ProfileGraphs/index.d.ts +13 -2
  22. package/dist/lib/classes/ProfileGraphs/index.js +22 -2
  23. package/dist/lib/classes/RemoteDocumentAutosave/index.d.ts +73 -0
  24. package/dist/lib/classes/RemoteDocumentAutosave/index.js +119 -0
  25. package/dist/lib/classes/SettingsAutosave/index.d.ts +7 -3
  26. package/dist/lib/classes/SettingsAutosave/index.js +28 -7
  27. package/dist/lib/classes/SettingsExtensions/index.d.ts +67 -0
  28. package/dist/lib/classes/SettingsExtensions/index.js +88 -0
  29. package/dist/lib/document/index.d.ts +61 -4
  30. package/dist/lib/document/index.js +173 -4
  31. package/dist/lib/document/schema.d.ts +154 -39
  32. package/dist/lib/document/schema.js +91 -25
  33. package/dist/lib/messages/schema.d.ts +176 -74
  34. package/dist/lib/messages/schema.js +2 -1
  35. package/dist/lib/palette/index.d.ts +8 -42
  36. package/dist/lib/palette/index.js +26 -59
  37. package/dist/lib/palette/schema.d.ts +111 -22
  38. package/dist/lib/palette/schema.js +51 -26
  39. package/dist/lib/presets/index.d.ts +1 -9
  40. package/dist/lib/presets/index.js +4 -10
  41. package/dist/lib/presets/schema.d.ts +118 -73
  42. package/dist/lib/presets/schema.js +28 -60
  43. package/dist/lib/profiles/profileFile/index.d.ts +10 -0
  44. package/dist/lib/profiles/profileFile/index.js +19 -0
  45. package/dist/lib/profiles/schema.d.ts +96 -3
  46. package/dist/lib/profiles/schema.js +31 -7
  47. package/dist/lib/schemas/index.d.ts +42 -0
  48. package/dist/lib/schemas/index.js +60 -0
  49. package/dist/lib/settings/index.d.ts +37 -22
  50. package/dist/lib/settings/index.js +78 -37
  51. package/dist/lib/settings/schema.d.ts +198 -76
  52. package/dist/lib/settings/schema.js +92 -86
  53. package/dist/lib/storage/index.d.ts +82 -3
  54. package/dist/lib/storage/index.js +82 -5
  55. package/dist/lib/validation/validators.generated.d.ts +19 -8
  56. package/dist/lib/validation/validators.generated.js +44716 -18900
  57. package/dist/lib/version/index.js +1 -1
  58. package/dist/locales/en-GB.js +23 -5
  59. package/dist/locales/en-US.js +23 -5
  60. package/dist/locales/fr.js +23 -5
  61. package/dist/locales/ja.js +23 -5
  62. package/dist/locales/ko.js +23 -5
  63. package/dist/locales/zh-Hans.js +23 -5
  64. package/dist/locales/zh-Hant.js +23 -5
  65. package/dist/package.js +1 -1
  66. package/dist/testing/app.d.ts +6 -2
  67. package/dist/testing/app.js +7 -2
  68. package/dist/testing/documentStore.d.ts +26 -0
  69. package/dist/testing/documentStore.js +31 -0
  70. package/dist/ui/elements/PaintCanvas/index.d.ts +6 -2
  71. package/dist/ui/elements/PaintCanvas/index.js +20 -6
  72. package/dist/ui/elements/PcAppSettings/index.d.ts +5 -6
  73. package/dist/ui/elements/PcAppSettings/index.js +17 -27
  74. package/dist/ui/elements/PcPanelBrushes/index.d.ts +1 -1
  75. package/dist/ui/elements/PcPanelBrushes/index.js +22 -28
  76. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  77. package/dist/ui/elements/PcPanelColours/index.d.ts +56 -19
  78. package/dist/ui/elements/PcPanelColours/index.js +251 -104
  79. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  80. package/dist/ui/elements/PcStats/index.d.ts +8 -4
  81. package/dist/ui/elements/PcStats/index.js +39 -5
  82. package/locales/en-GB.json +23 -5
  83. package/locales/en-US.json +23 -5
  84. package/locales/fr.json +23 -5
  85. package/locales/ja.json +23 -5
  86. package/locales/ko.json +23 -5
  87. package/locales/zh-Hans.json +23 -5
  88. package/locales/zh-Hant.json +23 -5
  89. package/package.json +2 -1
@@ -14,18 +14,24 @@ import Lasso, { type LassoResult } from "../../selection/Lasso";
14
14
  import Transformer from "../../transform/Transformer";
15
15
  import { type CanvasFactory, type LayerConfig } from "../Layer";
16
16
  import LayerAutosave, { type AutosaveOptions } from "../LayerAutosave";
17
- import type { LayerStoreLike } from "../../storage";
17
+ import type { AssetStores, DocumentStoreLike, LayerStoreLike } from "../../storage";
18
+ import RemoteDocumentAutosave, { type RemoteAutosaveOptions } from "../RemoteDocumentAutosave";
19
+ import type { ProfileGraph } from "../../profiles";
18
20
  import type { AssetAuthor, BrushConfig } from "../../brushes/types";
19
- import CustomPresets, { type PresetStoreLike } from "../CustomPresets";
21
+ import CustomPresets from "../CustomPresets";
22
+ import AssetSync from "../AssetSync";
23
+ import ColourPalettes from "../ColourPalettes";
24
+ import type { ColourPalette } from "../../palette";
20
25
  import InputSettings from "../InputSettings";
21
26
  import UiSettings, { type Rendering } from "../UiSettings";
22
27
  import Palette from "../Palette";
23
28
  import ProfileGraphs from "../ProfileGraphs";
24
29
  import ShortcutSettings from "../ShortcutSettings";
30
+ import SettingsExtensions from "../SettingsExtensions";
25
31
  import ExternalSheets from "../ExternalSheets";
26
32
  import { type CanvasMessage, type PatchMessage, type StrokeMessage } from "../../messages";
27
33
  import { type BoundingBox } from "../../image";
28
- import { type PaintCanvasDocument } from "../../document";
34
+ import { type DocumentFile, type DocumentInfo, type DrawingDocument } from "../../document";
29
35
  export { Tool };
30
36
  /** How the colour sample tool samples: the pixel under the pointer, or the average of a dragged rectangle */
31
37
  export type SampleMode = "point" | "area";
@@ -54,8 +60,6 @@ export declare const DEFAULT_BACKGROUND_COLOUR = "#ffffff";
54
60
  * (painted by the eraser and Delete when the layer's alpha lock is on, and by background fills).
55
61
  */
56
62
  export type ColourSlot = "foreground" | "background";
57
- /** Owner of the custom presets when the `ownerId` option isn't given */
58
- export declare const DEFAULT_OWNER_ID = "local";
59
63
  /** Author name of presets made with `newPreset` when the `presetAuthor` option isn't given */
60
64
  export declare const DEFAULT_AUTHOR = "Unknown";
61
65
  /** Names the implementer can change: what users see and what files are called */
@@ -64,12 +68,6 @@ export interface PaintCanvasNames {
64
68
  app: string;
65
69
  /** Start of the names of saved files, e.g. screenshots and exported presets */
66
70
  filePrefix: string;
67
- /** The `format` written in exported brush preset files */
68
- presetFileFormat: string;
69
- /** The `format` written in exported colour palette files */
70
- paletteFileFormat: string;
71
- /** The `format` written in exported settings files */
72
- settingsFileFormat: string;
73
71
  /** Author name of presets made by a user without a handle, e.g. from `presetAuthor` */
74
72
  unknownAuthor: string;
75
73
  }
@@ -93,6 +91,11 @@ export interface CanvasStats {
93
91
  /** Most undo steps kept */
94
92
  historyLimit: number;
95
93
  }
94
+ /** Options for `PaintCanvas.createDocument` */
95
+ export interface CreateDocumentOptions {
96
+ /** Whether to include a thumbnail (default true) */
97
+ thumbnail?: boolean;
98
+ }
96
99
  /** Receives the drawing messages PaintCanvas sends; a returned promise that rejects means a patch wasn't delivered */
97
100
  export type MessageListener = (message: CanvasMessage) => void | Promise<void>;
98
101
  /** Options for sending drawing messages (see `startSending`) */
@@ -127,16 +130,16 @@ export interface PaintCanvasOptions {
127
130
  * given (replaceable for tests).
128
131
  */
129
132
  createCanvas?: CanvasFactory;
130
- /** Where custom brush presets are kept (defaults to keeping them in memory only) */
131
- presetStore?: PresetStoreLike;
132
- /** Who the custom presets belong to, passed to the preset store (default `DEFAULT_OWNER_ID`) */
133
- ownerId?: string;
133
+ /** Where the user's assets (brush presets, colour palettes, profile graphs) are kept (see `useAssetStores`) */
134
+ stores?: AssetStores;
134
135
  /** Gives the author of presets made with `newPreset` (asked each time, so it can follow name changes) */
135
136
  presetAuthor?: () => AssetAuthor;
136
137
  /** Names to use instead of PaintCanvas's own (see `DEFAULT_NAMES`) */
137
138
  names?: Partial<PaintCanvasNames>;
138
139
  /** Where to keep the user's layers so the drawing survives reloading the page (see `useLayerStore`) */
139
140
  layerStore?: LayerStoreLike;
141
+ /** Where to keep the whole drawing away from the browser, saved less often (see `useRemoteDocumentStore`) */
142
+ remoteDocumentStore?: DocumentStoreLike;
140
143
  /**
141
144
  * The system clipboard, which copying and cutting also put a PNG of the copied image data on so other apps (and brush
142
145
  * shapes) can use it (the browser's by default; `null` for none)
@@ -184,6 +187,20 @@ export default class PaintCanvas extends EventTarget {
184
187
  readonly palette: Palette;
185
188
  /** The user's own profile graphs (and the presets), for shaping how brush settings vary */
186
189
  readonly profiles: ProfileGraphs;
190
+ /** The drawing's details (see `documentInfo`) */
191
+ private info;
192
+ /** Keeps the drawing in a remote store */
193
+ private remoteDocumentAutosave?;
194
+ /** Settings wrapper apps add to the app settings (see `SettingsExtension`) */
195
+ readonly settingsExtensions: SettingsExtensions;
196
+ /** The user's own colour palettes (the one PaintCanvas comes with is `palette`) */
197
+ readonly palettes: ColourPalettes;
198
+ /** Where the user's assets are kept */
199
+ private stores;
200
+ /** Keeps the colour palettes in their store */
201
+ private palettesAutosave?;
202
+ /** Keeps the profile graphs in their store */
203
+ private profilesAutosave?;
187
204
  /** The user's keyboard shortcuts: each configurable action's key combination */
188
205
  readonly shortcuts: ShortcutSettings;
189
206
  /** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
@@ -244,7 +261,6 @@ export default class PaintCanvas extends EventTarget {
244
261
  /** Creates the canvases the app draws on (fake ones in tests) */
245
262
  readonly createCanvas: CanvasFactory;
246
263
  /** Who the custom presets belong to (passed to the preset store) */
247
- readonly ownerId: string;
248
264
  /** The largest canvas width or height allowed */
249
265
  readonly maxCanvasSize: number;
250
266
  /** Most layers the user can have */
@@ -316,23 +332,60 @@ export default class PaintCanvas extends EventTarget {
316
332
  * transport.onMessage((from, message) => canvas.receive(from, message));
317
333
  */
318
334
  receive(source: string, message: unknown): Promise<boolean>;
335
+ /** The drawing's details: its ID, name, authors and when it was made and last opened */
336
+ get documentInfo(): Readonly<DocumentInfo>;
337
+ /**
338
+ * Rename the drawing. Returns whether it was renamed (not if the name is empty).
339
+ *
340
+ * @example
341
+ * canvas.renameDocument("Sunset at sea");
342
+ */
343
+ renameDocument(name: string): boolean;
344
+ /**
345
+ * Use details of a drawing being restored (e.g. kept in the browser), marking it opened now.
346
+ */
347
+ restoreDocumentInfo(info: DocumentInfo): void;
348
+ /**
349
+ * Start a new drawing: new details (a new ID, the user as its author, made now), and the layers replaced with one
350
+ * empty layer. Undo history starts again.
351
+ *
352
+ * @example
353
+ * canvas.newDocument("Sketch");
354
+ */
355
+ newDocument(name?: string): void;
356
+ /**
357
+ * The drawing as a document (see `DrawingDocument`): its details, size, the user's layers and drawings from elsewhere
358
+ * as PNG blobs, and a thumbnail (at most `THUMBNAIL_SIZE` pixels on its longest side) unless left out. The user is
359
+ * added to its authors.
360
+ *
361
+ * @example
362
+ * await myStore.save(await canvas.createDocument());
363
+ */
364
+ createDocument({ thumbnail, }?: CreateDocumentOptions): Promise<DrawingDocument>;
319
365
  /**
320
- * The drawing as a document: its size, the user's layers and drawings from elsewhere, with PNG images (see
321
- * `PaintCanvasDocument`). Turn it into JSON to save it as a file.
366
+ * Replace the drawing with a document (e.g. from a store): its details (marked opened now), size, layers and drawings
367
+ * from elsewhere. Undo history starts again. Returns whether it was opened: not when it has too many layers, is too
368
+ * big, doesn't have its selected layer, or an image can't be decoded, and then nothing changes.
369
+ *
370
+ * @example
371
+ * await canvas.openDocument(await myStore.load(id));
372
+ */
373
+ openDocument(document: DrawingDocument): Promise<boolean>;
374
+ /**
375
+ * The drawing as a document file (see `DocumentFile`): turn it into JSON to save it as a file.
322
376
  *
323
377
  * @example
324
378
  * const json = JSON.stringify(await canvas.exportDocument());
325
379
  */
326
- exportDocument(): Promise<PaintCanvasDocument>;
380
+ exportDocument(): Promise<DocumentFile>;
327
381
  /**
328
- * Replace the drawing with a document (e.g. read from a file): its size, layers and drawings from elsewhere. Undo
329
- * history starts again. Returns whether it was loaded: not when the document is invalid or an image can't be
330
- * decoded, and then nothing changes.
382
+ * Replace the drawing with a document file (e.g. read from a file, see `documentFromFile`). Returns whether it was
383
+ * opened: not when it isn't a valid document or can't be opened (see `openDocument`), and then nothing changes.
331
384
  *
332
385
  * @example
333
386
  * await canvas.importDocument(JSON.parse(text));
334
387
  */
335
- importDocument(document: unknown): Promise<boolean>;
388
+ importDocument(file: unknown): Promise<boolean>;
336
389
  /**
337
390
  * Replace the canvas size and the user's layers (top first, each with its image) and select one by ID. Undo history
338
391
  * starts again. Throws if the size or number of layers isn't allowed.
@@ -341,6 +394,32 @@ export default class PaintCanvas extends EventTarget {
341
394
  * canvas.restoreLayers(800, 600, [{ id: "a", name: "Sketch" }], [sketchImage], "a");
342
395
  */
343
396
  restoreLayers(width: number, height: number, layers: readonly LayerConfig[], images: readonly CanvasImageSource[], activeLayer: string): void;
397
+ /** Where the user's assets are kept (see `useAssetStores`) */
398
+ get assetStores(): Readonly<AssetStores>;
399
+ /** Keeps the user's profile graphs in their store, while one is used (see `useAssetStores`) */
400
+ get profileSync(): AssetSync<ProfileGraph> | undefined;
401
+ /** Keeps the user's colour palettes in their store, while one is used (see `useAssetStores`) */
402
+ get paletteSync(): AssetSync<ColourPalette> | undefined;
403
+ /**
404
+ * Keep the user's assets in stores, by kind: custom brush presets are loaded from theirs (in place of the current
405
+ * ones) and saved when they change; colour palettes and profile graphs saved there are added and changes saved (see
406
+ * `AssetSync`). Assets of a kind without a store are kept in memory only.
407
+ *
408
+ * @example
409
+ * canvas.useAssetStores({ brushes: new IndexedDbAssetStore("brushes"), profiles: new IndexedDbAssetStore("profiles") });
410
+ */
411
+ useAssetStores(stores: AssetStores): void;
412
+ /** Keeps the drawing in a remote store, while one is used (see `useRemoteDocumentStore`) */
413
+ get remoteAutosave(): RemoteDocumentAutosave | undefined;
414
+ /**
415
+ * Keep the whole drawing in a remote store (e.g. the user's account online), or stop, without one: changes are saved
416
+ * every few minutes (see `RemoteDocumentAutosave`), or when asked with `canvas.remoteAutosave.save()`. Returns the
417
+ * autosave.
418
+ *
419
+ * @example
420
+ * canvas.useRemoteDocumentStore(myAccountStore, { intervalMs: 10 * 60 * 1000 });
421
+ */
422
+ useRemoteDocumentStore(store: DocumentStoreLike | undefined, options?: RemoteAutosaveOptions): RemoteDocumentAutosave | undefined;
344
423
  /** Keeps the user's layers in a store, while one is used (see `useLayerStore`) */
345
424
  get autosave(): LayerAutosave | undefined;
346
425
  /**
@@ -561,6 +640,13 @@ export default class PaintCanvas extends EventTarget {
561
640
  * own presets are the ones with this handle (see `isOwnAuthor`).
562
641
  */
563
642
  get authorHandle(): string;
643
+ /**
644
+ * The author of assets the user makes, e.g. colour palettes (from the `presetAuthor` option).
645
+ *
646
+ * @example
647
+ * app.palettes.create("Sunset", [], app.author);
648
+ */
649
+ get author(): AssetAuthor;
564
650
  /**
565
651
  * Make a new custom preset from settings: a new ID, the given name, and the author from the `presetAuthor` option.
566
652
  *
@@ -660,5 +746,7 @@ export default class PaintCanvas extends EventTarget {
660
746
  /**
661
747
  * Dispatch a `change` event.
662
748
  */
749
+ /** Use the drawing's details and tell listeners */
750
+ private setDocumentInfo;
663
751
  protected notify(): void;
664
752
  }
@@ -13,28 +13,27 @@ import Transformer, { applyMask, contentBounds, copyArea, drawTransformed } from
13
13
  import { boxBounds } from "../../transform/geometry/index.js";
14
14
  import Layer, { createMemoryCanvas, createOffscreenCanvas } from "../Layer/index.js";
15
15
  import LayerAutosave from "../LayerAutosave/index.js";
16
+ import RemoteDocumentAutosave from "../RemoteDocumentAutosave/index.js";
16
17
  import { brushPresets, eraserPresets } from "../../brushes/index.js";
17
18
  import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
18
- import "../../presets/index.js";
19
19
  import CustomPresets from "../CustomPresets/index.js";
20
- import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
20
+ import AssetSync from "../AssetSync/index.js";
21
+ import ColourPalettes from "../ColourPalettes/index.js";
21
22
  import InputSettings from "../InputSettings/index.js";
22
23
  import UiSettings, { DEFAULT_UI_SETTINGS } from "../UiSettings/index.js";
23
24
  import Palette from "../Palette/index.js";
24
- import { PALETTE_FILE_FORMAT } from "../../palette/schema.js";
25
25
  import ProfileGraphs from "../ProfileGraphs/index.js";
26
26
  import ShortcutSettings from "../ShortcutSettings/index.js";
27
- import "../../palette/index.js";
28
- import { SETTINGS_FILE_FORMAT } from "../../settings/schema.js";
27
+ import SettingsExtensions from "../SettingsExtensions/index.js";
29
28
  import ExternalSheets from "../ExternalSheets/index.js";
30
29
  import PatchSender from "../../messages/PatchSender/index.js";
31
30
  import { createPatchMessage, validateCanvasMessage } from "../../messages/index.js";
32
31
  import { REDO_EVENT, UNDO_EVENT } from "../History/index.js";
33
32
  import { CanvasMessageType } from "../../messages/schema.js";
34
- import { ImageEncoding, base64ToBytes, boundingBoxSize, bytesToBase64, decodeRgbaDeflate, encodeRgbaDeflate } from "../../image/index.js";
33
+ import { ImageEncoding, base64ToBytes, boundingBoxSize, decodeRgbaDeflate, encodeRgbaDeflate } from "../../image/index.js";
35
34
  import { drawLayers } from "../../rendering/compositing/index.js";
36
- import { DOCUMENT_FORMAT } from "../../document/schema.js";
37
- import { isValidDocument } from "../../document/index.js";
35
+ import "../../document/schema.js";
36
+ import { documentFromFile, documentToFile, newDocumentInfo, withAuthor } from "../../document/index.js";
38
37
  import { averageColour, flattenArea, pixelRect, readPixels } from "../../rendering/sample/index.js";
39
38
  // src/lib/classes/PaintCanvas/index.ts
40
39
  /** Toggleable panels */
@@ -61,32 +60,22 @@ var MAX_CANVAS_SIZE = 2e3;
61
60
  var DEFAULT_COLOUR = "#000000";
62
61
  /** Background colour to start with */
63
62
  var DEFAULT_BACKGROUND_COLOUR = "#ffffff";
64
- /** Owner of the custom presets when the `ownerId` option isn't given */
65
- var DEFAULT_OWNER_ID = "local";
66
63
  /** Author name of presets made with `newPreset` when the `presetAuthor` option isn't given */
67
64
  var DEFAULT_AUTHOR = UNKNOWN_AUTHOR;
68
65
  /** PaintCanvas's own names */
69
66
  var DEFAULT_NAMES = {
70
67
  app: BUILT_IN_AUTHOR,
71
68
  filePrefix: "paintcanvas",
72
- presetFileFormat: BRUSH_PRESET_FILE_FORMAT,
73
- paletteFileFormat: PALETTE_FILE_FORMAT,
74
- settingsFileFormat: SETTINGS_FILE_FORMAT,
75
69
  unknownAuthor: UNKNOWN_AUTHOR
76
70
  };
77
71
  /**
78
- * A canvas's pixels as a PNG data URL.
72
+ * A canvas's pixels as a PNG.
79
73
  */
80
- var toPngDataUrl = async (canvas) => {
74
+ var toPng = (canvas) => {
81
75
  canvas.getContext("2d");
82
- const blob = await canvas.convertToBlob({ type: "image/png" });
83
- return `data:image/png;base64,${bytesToBase64(new Uint8Array(await blob.arrayBuffer()))}`;
76
+ return canvas.convertToBlob({ type: "image/png" });
84
77
  };
85
78
  /**
86
- * Decode a PNG data URL into an image.
87
- */
88
- var fromPngDataUrl = (dataUrl) => createImageBitmap(new Blob([base64ToBytes(dataUrl.slice(dataUrl.indexOf(",") + 1))], { type: "image/png" }));
89
- /**
90
79
  * Decode a patch into pixels.
91
80
  */
92
81
  var decodePatch = async (patch) => {
@@ -133,6 +122,20 @@ var PaintCanvas = class extends EventTarget {
133
122
  palette = new Palette();
134
123
  /** The user's own profile graphs (and the presets), for shaping how brush settings vary */
135
124
  profiles = new ProfileGraphs();
125
+ /** The drawing's details (see `documentInfo`) */
126
+ info;
127
+ /** Keeps the drawing in a remote store */
128
+ remoteDocumentAutosave;
129
+ /** Settings wrapper apps add to the app settings (see `SettingsExtension`) */
130
+ settingsExtensions = new SettingsExtensions();
131
+ /** The user's own colour palettes (the one PaintCanvas comes with is `palette`) */
132
+ palettes = new ColourPalettes();
133
+ /** Where the user's assets are kept */
134
+ stores = {};
135
+ /** Keeps the colour palettes in their store */
136
+ palettesAutosave;
137
+ /** Keeps the profile graphs in their store */
138
+ profilesAutosave;
136
139
  /** The user's keyboard shortcuts: each configurable action's key combination */
137
140
  shortcuts = new ShortcutSettings();
138
141
  /** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
@@ -217,7 +220,6 @@ var PaintCanvas = class extends EventTarget {
217
220
  /** Creates the canvases the app draws on (fake ones in tests) */
218
221
  createCanvas;
219
222
  /** Who the custom presets belong to (passed to the preset store) */
220
- ownerId;
221
223
  /** The largest canvas width or height allowed */
222
224
  maxCanvasSize;
223
225
  /** Most layers the user can have */
@@ -231,7 +233,6 @@ var PaintCanvas = class extends EventTarget {
231
233
  constructor(options = {}) {
232
234
  super();
233
235
  this.strokeLog.enabled = false;
234
- this.ownerId = options.ownerId ?? "local";
235
236
  this.maxCanvasSize = options.maxCanvasSize ?? 2e3;
236
237
  this.maxLayers = options.maxLayers ?? 5;
237
238
  this.names = {
@@ -239,11 +240,12 @@ var PaintCanvas = class extends EventTarget {
239
240
  ...options.names
240
241
  };
241
242
  this.presetAuthor = options.presetAuthor ?? (() => ({ name: DEFAULT_AUTHOR }));
243
+ this.info = newDocumentInfo(this.author);
242
244
  this.tips.preload(this.brush.presets);
243
245
  this.tips.preload(this.eraser.presets);
244
- this.customPresets = new CustomPresets(this.ownerId, options.presetStore);
246
+ this.customPresets = new CustomPresets();
245
247
  this.customPresets.addEventListener("change", () => this.listCustomPresets());
246
- this.customPresets.load().catch(() => void 0);
248
+ this.useAssetStores(options.stores ?? {});
247
249
  this.rendering = options.rendering ?? DEFAULT_UI_SETTINGS.rendering;
248
250
  this.ui = new UiSettings({ rendering: this.rendering });
249
251
  this.createCanvas = options.createCanvas ?? (this.rendering === "gpu" ? createOffscreenCanvas : createMemoryCanvas);
@@ -259,6 +261,7 @@ var PaintCanvas = class extends EventTarget {
259
261
  this.watchLayersAndMask();
260
262
  if (options.width !== void 0 || options.height !== void 0) this.setCanvasSize(options.width ?? this.canvasWidth, options.height ?? this.canvasHeight);
261
263
  if (options.layerStore) this.useLayerStore(options.layerStore);
264
+ if (options.remoteDocumentStore) this.useRemoteDocumentStore(options.remoteDocumentStore);
262
265
  }
263
266
  /** Canvas width in pixels */
264
267
  get width() {
@@ -396,65 +399,161 @@ var PaintCanvas = class extends EventTarget {
396
399
  } }));
397
400
  return true;
398
401
  }
402
+ /** The drawing's details: its ID, name, authors and when it was made and last opened */
403
+ get documentInfo() {
404
+ return this.info;
405
+ }
399
406
  /**
400
- * The drawing as a document: its size, the user's layers and drawings from elsewhere, with PNG images (see
401
- * `PaintCanvasDocument`). Turn it into JSON to save it as a file.
407
+ * Rename the drawing. Returns whether it was renamed (not if the name is empty).
402
408
  *
403
409
  * @example
404
- * const json = JSON.stringify(await canvas.exportDocument());
410
+ * canvas.renameDocument("Sunset at sea");
411
+ */
412
+ renameDocument(name) {
413
+ const trimmed = name.trim();
414
+ if (!trimmed) return false;
415
+ this.setDocumentInfo({
416
+ ...this.info,
417
+ name: trimmed
418
+ });
419
+ return true;
420
+ }
421
+ /**
422
+ * Use details of a drawing being restored (e.g. kept in the browser), marking it opened now.
405
423
  */
406
- async exportDocument() {
424
+ restoreDocumentInfo(info) {
425
+ this.setDocumentInfo({
426
+ ...info,
427
+ dateOpened: (/* @__PURE__ */ new Date()).toISOString()
428
+ });
429
+ }
430
+ /**
431
+ * Start a new drawing: new details (a new ID, the user as its author, made now), and the layers replaced with one
432
+ * empty layer. Undo history starts again.
433
+ *
434
+ * @example
435
+ * canvas.newDocument("Sketch");
436
+ */
437
+ newDocument(name) {
438
+ this.layerStack.replace([{ name: "Layer 1" }]);
439
+ this.history.clear();
440
+ this.setDocumentInfo(newDocumentInfo(this.author, name));
441
+ }
442
+ /**
443
+ * The drawing as a document (see `DrawingDocument`): its details, size, the user's layers and drawings from elsewhere
444
+ * as PNG blobs, and a thumbnail (at most `THUMBNAIL_SIZE` pixels on its longest side) unless left out. The user is
445
+ * added to its authors.
446
+ *
447
+ * @example
448
+ * await myStore.save(await canvas.createDocument());
449
+ */
450
+ async createDocument({ thumbnail = true } = {}) {
451
+ const author = withAuthor(this.info.author, this.author);
452
+ if (author.length !== this.info.author.length) this.setDocumentInfo({
453
+ ...this.info,
454
+ author
455
+ });
456
+ const { width, height } = this;
457
+ const scale = Math.min(1, 200 / Math.max(width, height));
458
+ const thumbnailWidth = Math.max(1, Math.round(width * scale));
459
+ const thumbnailHeight = Math.max(1, Math.round(height * scale));
407
460
  const layers = this.layerStack.layers;
461
+ const [layerImages, sheetImages, thumbnailImage] = await Promise.all([
462
+ Promise.all(layers.map((layer) => toPng(layer.canvas))),
463
+ Promise.all(this.externalSheets.sheets.map(({ canvas }) => toPng(canvas))),
464
+ thumbnail ? toPng(this.flatten({
465
+ x: 0,
466
+ y: 0,
467
+ width,
468
+ height
469
+ }, thumbnailWidth, thumbnailHeight)) : void 0
470
+ ]);
408
471
  return {
409
- format: DOCUMENT_FORMAT,
410
- version: 1,
411
- width: this.canvasWidth,
412
- height: this.canvasHeight,
413
- layers: await Promise.all(layers.map(async (layer) => {
414
- const { name, visible, opacity, blendMode, alphaLock } = layer.toConfig();
472
+ ...structuredClone(this.info),
473
+ width,
474
+ height,
475
+ layers: layers.map((layer, i) => {
476
+ const { id, name, visible, opacity, blendMode, alphaLock } = layer.toConfig();
415
477
  return {
478
+ id,
416
479
  name,
417
480
  visible,
418
481
  opacity,
419
- blendMode,
482
+ blendingMode: blendMode,
420
483
  alphaLock,
421
- image: await toPngDataUrl(layer.canvas)
484
+ content: layerImages[i]
422
485
  };
423
- })),
424
- activeLayer: layers.indexOf(this.layerStack.active),
425
- external: await Promise.all(this.externalSheets.sheets.map(async ({ source, canvas }) => ({
486
+ }),
487
+ activeLayer: this.layerStack.active.id,
488
+ external: this.externalSheets.sheets.map(({ source }, i) => ({
426
489
  source,
427
- image: await toPngDataUrl(canvas)
428
- })))
490
+ content: sheetImages[i]
491
+ })),
492
+ ...thumbnailImage ? { thumbnail: {
493
+ width: thumbnailWidth,
494
+ height: thumbnailHeight,
495
+ content: thumbnailImage
496
+ } } : {}
429
497
  };
430
498
  }
431
499
  /**
432
- * Replace the drawing with a document (e.g. read from a file): its size, layers and drawings from elsewhere. Undo
433
- * history starts again. Returns whether it was loaded: not when the document is invalid or an image can't be
434
- * decoded, and then nothing changes.
500
+ * Replace the drawing with a document (e.g. from a store): its details (marked opened now), size, layers and drawings
501
+ * from elsewhere. Undo history starts again. Returns whether it was opened: not when it has too many layers, is too
502
+ * big, doesn't have its selected layer, or an image can't be decoded, and then nothing changes.
435
503
  *
436
504
  * @example
437
- * await canvas.importDocument(JSON.parse(text));
505
+ * await canvas.openDocument(await myStore.load(id));
438
506
  */
439
- async importDocument(document) {
440
- const result = isValidDocument(document);
441
- if (!result.valid || result.value.layers.length > this.maxLayers) return false;
442
- const doc = result.value;
443
- const images = await Promise.all([...doc.layers.map((layer) => fromPngDataUrl(layer.image)), ...doc.external.map((sheet) => fromPngDataUrl(sheet.image))]).catch(() => void 0);
507
+ async openDocument(document) {
508
+ const { layers, external, width, height, activeLayer } = document;
509
+ if (layers.length === 0 || layers.length > this.maxLayers || Math.max(width, height) > Math.min(this.maxCanvasSize, 1e4) || !layers.some((layer) => layer.id === activeLayer)) return false;
510
+ const images = await Promise.all([...layers, ...external].map((item) => createImageBitmap(item.content))).catch(() => void 0);
444
511
  if (!images) return false;
445
- const configs = doc.layers.map(({ name, visible, opacity, blendMode, alphaLock }) => ({
446
- id: crypto.randomUUID(),
512
+ this.restoreLayers(width, height, layers.map(({ id, name, visible, opacity, blendingMode, alphaLock }) => ({
513
+ id,
447
514
  name,
448
515
  visible,
449
516
  opacity,
450
- blendMode,
517
+ blendMode: blendingMode,
451
518
  alphaLock
452
- }));
453
- this.restoreLayers(doc.width, doc.height, configs, images, configs[doc.activeLayer].id);
454
- doc.external.forEach((sheet, i) => this.externalSheets.restore(sheet.source, images[configs.length + i]));
519
+ })), images, activeLayer);
520
+ external.forEach((sheet, i) => this.externalSheets.restore(sheet.source, images[layers.length + i]));
521
+ const { id, name, author, dateCreated, dateOpened } = document;
522
+ this.restoreDocumentInfo({
523
+ id,
524
+ name,
525
+ author,
526
+ dateCreated,
527
+ dateOpened
528
+ });
455
529
  return true;
456
530
  }
457
531
  /**
532
+ * The drawing as a document file (see `DocumentFile`): turn it into JSON to save it as a file.
533
+ *
534
+ * @example
535
+ * const json = JSON.stringify(await canvas.exportDocument());
536
+ */
537
+ async exportDocument() {
538
+ return documentToFile(await this.createDocument());
539
+ }
540
+ /**
541
+ * Replace the drawing with a document file (e.g. read from a file, see `documentFromFile`). Returns whether it was
542
+ * opened: not when it isn't a valid document or can't be opened (see `openDocument`), and then nothing changes.
543
+ *
544
+ * @example
545
+ * await canvas.importDocument(JSON.parse(text));
546
+ */
547
+ async importDocument(file) {
548
+ let document;
549
+ try {
550
+ document = documentFromFile(file);
551
+ } catch {
552
+ return false;
553
+ }
554
+ return this.openDocument(document);
555
+ }
556
+ /**
458
557
  * Replace the canvas size and the user's layers (top first, each with its image) and select one by ID. Undo history
459
558
  * starts again. Throws if the size or number of layers isn't allowed.
460
559
  *
@@ -468,6 +567,54 @@ var PaintCanvas = class extends EventTarget {
468
567
  this.layerStack.select(activeLayer);
469
568
  this.history.clear();
470
569
  }
570
+ /** Where the user's assets are kept (see `useAssetStores`) */
571
+ get assetStores() {
572
+ return this.stores;
573
+ }
574
+ /** Keeps the user's profile graphs in their store, while one is used (see `useAssetStores`) */
575
+ get profileSync() {
576
+ return this.profilesAutosave;
577
+ }
578
+ /** Keeps the user's colour palettes in their store, while one is used (see `useAssetStores`) */
579
+ get paletteSync() {
580
+ return this.palettesAutosave;
581
+ }
582
+ /**
583
+ * Keep the user's assets in stores, by kind: custom brush presets are loaded from theirs (in place of the current
584
+ * ones) and saved when they change; colour palettes and profile graphs saved there are added and changes saved (see
585
+ * `AssetSync`). Assets of a kind without a store are kept in memory only.
586
+ *
587
+ * @example
588
+ * canvas.useAssetStores({ brushes: new IndexedDbAssetStore("brushes"), profiles: new IndexedDbAssetStore("profiles") });
589
+ */
590
+ useAssetStores(stores) {
591
+ this.stores = { ...stores };
592
+ this.customPresets.useStore(stores.brushes).catch(() => void 0);
593
+ this.profilesAutosave?.stop();
594
+ this.profilesAutosave = stores.profiles ? new AssetSync(this.profiles, stores.profiles) : void 0;
595
+ this.palettesAutosave?.stop();
596
+ this.palettesAutosave = stores.palettes ? new AssetSync(this.palettes, stores.palettes) : void 0;
597
+ this.notify();
598
+ }
599
+ /** Keeps the drawing in a remote store, while one is used (see `useRemoteDocumentStore`) */
600
+ get remoteAutosave() {
601
+ return this.remoteDocumentAutosave;
602
+ }
603
+ /**
604
+ * Keep the whole drawing in a remote store (e.g. the user's account online), or stop, without one: changes are saved
605
+ * every few minutes (see `RemoteDocumentAutosave`), or when asked with `canvas.remoteAutosave.save()`. Returns the
606
+ * autosave.
607
+ *
608
+ * @example
609
+ * canvas.useRemoteDocumentStore(myAccountStore, { intervalMs: 10 * 60 * 1000 });
610
+ */
611
+ useRemoteDocumentStore(store, options) {
612
+ this.remoteDocumentAutosave?.stop();
613
+ this.remoteDocumentAutosave = store ? new RemoteDocumentAutosave(this, store, options) : void 0;
614
+ this.remoteDocumentAutosave?.start();
615
+ this.notify();
616
+ return this.remoteDocumentAutosave;
617
+ }
471
618
  /** Keeps the user's layers in a store, while one is used (see `useLayerStore`) */
472
619
  get autosave() {
473
620
  return this.layerAutosave;
@@ -949,6 +1096,15 @@ var PaintCanvas = class extends EventTarget {
949
1096
  return authorHandle(this.presetAuthor()) ?? "";
950
1097
  }
951
1098
  /**
1099
+ * The author of assets the user makes, e.g. colour palettes (from the `presetAuthor` option).
1100
+ *
1101
+ * @example
1102
+ * app.palettes.create("Sunset", [], app.author);
1103
+ */
1104
+ get author() {
1105
+ return this.presetAuthor();
1106
+ }
1107
+ /**
952
1108
  * Make a new custom preset from settings: a new ID, the given name, and the author from the `presetAuthor` option.
953
1109
  *
954
1110
  * @example
@@ -1157,8 +1313,13 @@ var PaintCanvas = class extends EventTarget {
1157
1313
  /**
1158
1314
  * Dispatch a `change` event.
1159
1315
  */
1316
+ /** Use the drawing's details and tell listeners */
1317
+ setDocumentInfo(info) {
1318
+ this.info = info;
1319
+ this.notify();
1320
+ }
1160
1321
  notify() {
1161
1322
  this.dispatchEvent(new Event("change"));
1162
1323
  }
1163
1324
  };
1164
- export { DEFAULT_AUTHOR, DEFAULT_BACKGROUND_COLOUR, DEFAULT_CANVAS_SIZE, DEFAULT_COLOUR, DEFAULT_NAMES, DEFAULT_OWNER_ID, MAX_CANVAS_SIZE, Panel, TOOL_PANELS, Tool, PaintCanvas as default, isHexColour };
1325
+ export { DEFAULT_AUTHOR, DEFAULT_BACKGROUND_COLOUR, DEFAULT_CANVAS_SIZE, DEFAULT_COLOUR, DEFAULT_NAMES, MAX_CANVAS_SIZE, Panel, TOOL_PANELS, Tool, PaintCanvas as default, isHexColour };
@@ -8,8 +8,9 @@
8
8
  */
9
9
  export declare const sanitisePalette: (input: unknown) => string[] | undefined;
10
10
  /**
11
- * The user's saved colours, shown as swatches. Each colour is kept once, as lowercase `#rrggbb`, up to
12
- * `MAX_PALETTE_COLOURS`. Kept in memory; the implementer saves and restores them (see `set` and the `change` event).
11
+ * The colours of the palette PaintCanvas comes with (`canvas.palette`), shown as swatches the user can pick from but not
12
+ * change. Each colour is kept once, as lowercase `#rrggbb`, up to `MAX_PALETTE_COLOURS`. An implementer can give it other
13
+ * colours (see `set` and the `change` event); it isn't saved.
13
14
  *
14
15
  * Dispatches a `change` event when the colours change.
15
16
  *