@lvl99dotcom/paintcanvas 0.6.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 (119) hide show
  1. package/README.md +3 -2
  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 +124 -28
  18. package/dist/lib/classes/PaintCanvas/index.js +228 -63
  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/classes/UiSettings/index.d.ts +24 -2
  30. package/dist/lib/classes/UiSettings/index.js +32 -7
  31. package/dist/lib/document/index.d.ts +61 -4
  32. package/dist/lib/document/index.js +173 -4
  33. package/dist/lib/document/schema.d.ts +154 -39
  34. package/dist/lib/document/schema.js +91 -25
  35. package/dist/lib/messages/schema.d.ts +176 -74
  36. package/dist/lib/messages/schema.js +2 -1
  37. package/dist/lib/palette/index.d.ts +8 -42
  38. package/dist/lib/palette/index.js +26 -59
  39. package/dist/lib/palette/schema.d.ts +111 -22
  40. package/dist/lib/palette/schema.js +51 -26
  41. package/dist/lib/presets/index.d.ts +1 -9
  42. package/dist/lib/presets/index.js +4 -10
  43. package/dist/lib/presets/schema.d.ts +118 -73
  44. package/dist/lib/presets/schema.js +28 -60
  45. package/dist/lib/profiles/profileFile/index.d.ts +10 -0
  46. package/dist/lib/profiles/profileFile/index.js +19 -0
  47. package/dist/lib/profiles/schema.d.ts +96 -3
  48. package/dist/lib/profiles/schema.js +31 -7
  49. package/dist/lib/schemas/index.d.ts +42 -0
  50. package/dist/lib/schemas/index.js +60 -0
  51. package/dist/lib/settings/index.d.ts +37 -22
  52. package/dist/lib/settings/index.js +78 -37
  53. package/dist/lib/settings/schema.d.ts +202 -77
  54. package/dist/lib/settings/schema.js +93 -86
  55. package/dist/lib/storage/index.d.ts +108 -3
  56. package/dist/lib/storage/index.js +119 -5
  57. package/dist/lib/validation/validators.generated.d.ts +19 -8
  58. package/dist/lib/validation/validators.generated.js +44723 -18889
  59. package/dist/lib/version/index.d.ts +25 -0
  60. package/dist/lib/version/index.js +35 -0
  61. package/dist/locales/en-GB.js +28 -5
  62. package/dist/locales/en-US.js +28 -5
  63. package/dist/locales/fr.js +28 -5
  64. package/dist/locales/ja.js +28 -5
  65. package/dist/locales/ko.js +28 -5
  66. package/dist/locales/zh-Hans.js +28 -5
  67. package/dist/locales/zh-Hant.js +28 -5
  68. package/dist/package.js +1 -1
  69. package/dist/testing/app.d.ts +6 -2
  70. package/dist/testing/app.js +7 -2
  71. package/dist/testing/documentStore.d.ts +26 -0
  72. package/dist/testing/documentStore.js +31 -0
  73. package/dist/ui/elements/PaintCanvas/index.d.ts +19 -5
  74. package/dist/ui/elements/PaintCanvas/index.js +41 -10
  75. package/dist/ui/elements/PcAppSettings/index.d.ts +14 -7
  76. package/dist/ui/elements/PcAppSettings/index.js +58 -33
  77. package/dist/ui/elements/PcBrushConfig/index.d.ts +1 -0
  78. package/dist/ui/elements/PcBrushConfig/index.js +8 -9
  79. package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
  80. package/dist/ui/elements/PcBrushTest/index.js +8 -7
  81. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  82. package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
  83. package/dist/ui/elements/PcButtonMenu/index.js +125 -0
  84. package/dist/ui/elements/PcCanvas/index.d.ts +9 -0
  85. package/dist/ui/elements/PcCanvas/index.js +33 -17
  86. package/dist/ui/elements/PcClipboardTools/index.d.ts +7 -4
  87. package/dist/ui/elements/PcClipboardTools/index.js +20 -26
  88. package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
  89. package/dist/ui/elements/PcInputSettings/index.js +9 -15
  90. package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
  91. package/dist/ui/elements/PcLayerItem/index.js +53 -26
  92. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  93. package/dist/ui/elements/PcPanelBrushes/index.d.ts +1 -1
  94. package/dist/ui/elements/PcPanelBrushes/index.js +22 -28
  95. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  96. package/dist/ui/elements/PcPanelColours/index.d.ts +56 -19
  97. package/dist/ui/elements/PcPanelColours/index.js +251 -104
  98. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  99. package/dist/ui/elements/PcSliderRange/index.js +3 -2
  100. package/dist/ui/elements/PcStats/index.d.ts +8 -4
  101. package/dist/ui/elements/PcStats/index.js +39 -5
  102. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
  103. package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
  104. package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
  105. package/dist/ui/elements/PcToolSettings/index.js +8 -8
  106. package/dist/ui/elements/PcVersion/index.d.ts +12 -0
  107. package/dist/ui/elements/PcVersion/index.js +43 -0
  108. package/dist/ui/index.d.ts +3 -1
  109. package/dist/ui/index.js +4 -2
  110. package/dist/ui/layout/index.d.ts +2 -2
  111. package/dist/ui/layout/index.js +2 -2
  112. package/locales/en-GB.json +28 -5
  113. package/locales/en-US.json +28 -5
  114. package/locales/fr.json +28 -5
  115. package/locales/ja.json +28 -5
  116. package/locales/ko.json +28 -5
  117. package/locales/zh-Hans.json +28 -5
  118. package/locales/zh-Hant.json +28 -5
  119. package/package.json +3 -2
@@ -11,30 +11,29 @@ 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, { createMemoryCanvas } from "../Layer/index.js";
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
- import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
20
19
  import CustomPresets from "../CustomPresets/index.js";
20
+ import AssetSync from "../AssetSync/index.js";
21
+ import ColourPalettes from "../ColourPalettes/index.js";
21
22
  import InputSettings from "../InputSettings/index.js";
22
- import UiSettings from "../UiSettings/index.js";
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) => {
@@ -127,12 +116,26 @@ var PaintCanvas = class extends EventTarget {
127
116
  background = new BackgroundWork(this.strokeLog);
128
117
  /** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
129
118
  input = new InputSettings();
130
- /** The user's interface choices (theme and language), kept with the other app settings */
131
- ui = new UiSettings();
119
+ /** The user's interface choices (theme, language and where canvases draw), kept with the other app settings */
120
+ ui;
132
121
  /** The user's saved colours, shown as swatches */
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) */
@@ -212,10 +215,11 @@ var PaintCanvas = class extends EventTarget {
212
215
  /** The layer stack whose changes are being sent */
213
216
  sendingLayers;
214
217
  onLayersChangeToSend = () => this.sender?.schedule();
218
+ /** Where the canvases draw, chosen when the canvas started (the user's choice for next time is `ui.data.rendering`) */
219
+ rendering;
215
220
  /** Creates the canvases the app draws on (fake ones in tests) */
216
221
  createCanvas;
217
222
  /** Who the custom presets belong to (passed to the preset store) */
218
- ownerId;
219
223
  /** The largest canvas width or height allowed */
220
224
  maxCanvasSize;
221
225
  /** Most layers the user can have */
@@ -229,7 +233,6 @@ var PaintCanvas = class extends EventTarget {
229
233
  constructor(options = {}) {
230
234
  super();
231
235
  this.strokeLog.enabled = false;
232
- this.ownerId = options.ownerId ?? "local";
233
236
  this.maxCanvasSize = options.maxCanvasSize ?? 2e3;
234
237
  this.maxLayers = options.maxLayers ?? 5;
235
238
  this.names = {
@@ -237,12 +240,15 @@ var PaintCanvas = class extends EventTarget {
237
240
  ...options.names
238
241
  };
239
242
  this.presetAuthor = options.presetAuthor ?? (() => ({ name: DEFAULT_AUTHOR }));
243
+ this.info = newDocumentInfo(this.author);
240
244
  this.tips.preload(this.brush.presets);
241
245
  this.tips.preload(this.eraser.presets);
242
- this.customPresets = new CustomPresets(this.ownerId, options.presetStore);
246
+ this.customPresets = new CustomPresets();
243
247
  this.customPresets.addEventListener("change", () => this.listCustomPresets());
244
- this.customPresets.load().catch(() => void 0);
245
- this.createCanvas = options.createCanvas ?? createMemoryCanvas;
248
+ this.useAssetStores(options.stores ?? {});
249
+ this.rendering = options.rendering ?? DEFAULT_UI_SETTINGS.rendering;
250
+ this.ui = new UiSettings({ rendering: this.rendering });
251
+ this.createCanvas = options.createCanvas ?? (this.rendering === "gpu" ? createOffscreenCanvas : createMemoryCanvas);
246
252
  this.systemClipboard = options.systemClipboard === null ? void 0 : options.systemClipboard ?? globalThis.navigator?.clipboard;
247
253
  this.canvasWidth = DEFAULT_CANVAS_SIZE;
248
254
  this.canvasHeight = DEFAULT_CANVAS_SIZE;
@@ -255,6 +261,7 @@ var PaintCanvas = class extends EventTarget {
255
261
  this.watchLayersAndMask();
256
262
  if (options.width !== void 0 || options.height !== void 0) this.setCanvasSize(options.width ?? this.canvasWidth, options.height ?? this.canvasHeight);
257
263
  if (options.layerStore) this.useLayerStore(options.layerStore);
264
+ if (options.remoteDocumentStore) this.useRemoteDocumentStore(options.remoteDocumentStore);
258
265
  }
259
266
  /** Canvas width in pixels */
260
267
  get width() {
@@ -392,65 +399,161 @@ var PaintCanvas = class extends EventTarget {
392
399
  } }));
393
400
  return true;
394
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
+ }
395
406
  /**
396
- * The drawing as a document: its size, the user's layers and drawings from elsewhere, with PNG images (see
397
- * `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).
398
408
  *
399
409
  * @example
400
- * 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.
401
423
  */
402
- 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));
403
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
+ ]);
404
471
  return {
405
- format: DOCUMENT_FORMAT,
406
- version: 1,
407
- width: this.canvasWidth,
408
- height: this.canvasHeight,
409
- layers: await Promise.all(layers.map(async (layer) => {
410
- 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();
411
477
  return {
478
+ id,
412
479
  name,
413
480
  visible,
414
481
  opacity,
415
- blendMode,
482
+ blendingMode: blendMode,
416
483
  alphaLock,
417
- image: await toPngDataUrl(layer.canvas)
484
+ content: layerImages[i]
418
485
  };
419
- })),
420
- activeLayer: layers.indexOf(this.layerStack.active),
421
- 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) => ({
422
489
  source,
423
- image: await toPngDataUrl(canvas)
424
- })))
490
+ content: sheetImages[i]
491
+ })),
492
+ ...thumbnailImage ? { thumbnail: {
493
+ width: thumbnailWidth,
494
+ height: thumbnailHeight,
495
+ content: thumbnailImage
496
+ } } : {}
425
497
  };
426
498
  }
427
499
  /**
428
- * Replace the drawing with a document (e.g. read from a file): its size, layers and drawings from elsewhere. Undo
429
- * history starts again. Returns whether it was loaded: not when the document is invalid or an image can't be
430
- * 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.
431
503
  *
432
504
  * @example
433
- * await canvas.importDocument(JSON.parse(text));
505
+ * await canvas.openDocument(await myStore.load(id));
434
506
  */
435
- async importDocument(document) {
436
- const result = isValidDocument(document);
437
- if (!result.valid || result.value.layers.length > this.maxLayers) return false;
438
- const doc = result.value;
439
- 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);
440
511
  if (!images) return false;
441
- const configs = doc.layers.map(({ name, visible, opacity, blendMode, alphaLock }) => ({
442
- id: crypto.randomUUID(),
512
+ this.restoreLayers(width, height, layers.map(({ id, name, visible, opacity, blendingMode, alphaLock }) => ({
513
+ id,
443
514
  name,
444
515
  visible,
445
516
  opacity,
446
- blendMode,
517
+ blendMode: blendingMode,
447
518
  alphaLock
448
- }));
449
- this.restoreLayers(doc.width, doc.height, configs, images, configs[doc.activeLayer].id);
450
- 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
+ });
451
529
  return true;
452
530
  }
453
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
+ /**
454
557
  * Replace the canvas size and the user's layers (top first, each with its image) and select one by ID. Undo history
455
558
  * starts again. Throws if the size or number of layers isn't allowed.
456
559
  *
@@ -464,6 +567,54 @@ var PaintCanvas = class extends EventTarget {
464
567
  this.layerStack.select(activeLayer);
465
568
  this.history.clear();
466
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
+ }
467
618
  /** Keeps the user's layers in a store, while one is used (see `useLayerStore`) */
468
619
  get autosave() {
469
620
  return this.layerAutosave;
@@ -945,6 +1096,15 @@ var PaintCanvas = class extends EventTarget {
945
1096
  return authorHandle(this.presetAuthor()) ?? "";
946
1097
  }
947
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
+ /**
948
1108
  * Make a new custom preset from settings: a new ID, the given name, and the author from the `presetAuthor` option.
949
1109
  *
950
1110
  * @example
@@ -1153,8 +1313,13 @@ var PaintCanvas = class extends EventTarget {
1153
1313
  /**
1154
1314
  * Dispatch a `change` event.
1155
1315
  */
1316
+ /** Use the drawing's details and tell listeners */
1317
+ setDocumentInfo(info) {
1318
+ this.info = info;
1319
+ this.notify();
1320
+ }
1156
1321
  notify() {
1157
1322
  this.dispatchEvent(new Event("change"));
1158
1323
  }
1159
1324
  };
1160
- 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
  *
@@ -20,8 +20,9 @@ var sanitisePalette = (input) => {
20
20
  return [...colours];
21
21
  };
22
22
  /**
23
- * The user's saved colours, shown as swatches. Each colour is kept once, as lowercase `#rrggbb`, up to
24
- * `MAX_PALETTE_COLOURS`. Kept in memory; the implementer saves and restores them (see `set` and the `change` event).
23
+ * The colours of the palette PaintCanvas comes with (`canvas.palette`), shown as swatches the user can pick from but not
24
+ * change. Each colour is kept once, as lowercase `#rrggbb`, up to `MAX_PALETTE_COLOURS`. An implementer can give it other
25
+ * colours (see `set` and the `change` event); it isn't saved.
25
26
  *
26
27
  * Dispatches a `change` event when the colours change.
27
28
  *
@@ -23,8 +23,9 @@ export interface ProfileGraphDraft {
23
23
  points: readonly ProfilePoint[];
24
24
  }
25
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.
26
+ * The user's own profile graphs, alongside the ones PaintCanvas comes with (`PROFILE_PRESETS`). Kept in memory; a
27
+ * profile store keeps them (see `PaintCanvas.useAssetStores`), or save and restore them yourself (see `set`, `merge` and
28
+ * the `change` event). Each gets a UUID when it's first saved.
28
29
  *
29
30
  * Dispatches a `change` event when the user's graphs change.
30
31
  *
@@ -37,6 +38,8 @@ export default class ProfileGraphs extends EventTarget {
37
38
  private values;
38
39
  /** The user's own graphs, in the order they were first saved */
39
40
  get graphs(): readonly ProfileGraph[];
41
+ /** The user's own graphs (the same as `graphs`, so a store can keep them, see `AssetSync`) */
42
+ get items(): readonly ProfileGraph[];
40
43
  /** Every graph: the presets, then the user's own */
41
44
  get all(): readonly ProfileGraph[];
42
45
  /**
@@ -84,4 +87,12 @@ export default class ProfileGraphs extends EventTarget {
84
87
  * profiles.set(settings.profiles);
85
88
  */
86
89
  set(graphs: unknown): void;
90
+ /**
91
+ * Add graphs (e.g. loaded from a store or imported), replacing the user's graphs with the same IDs and keeping the
92
+ * others, up to `MAX_SAVED_PROFILES`. Invalid graphs are left out (see `sanitiseProfiles`).
93
+ *
94
+ * @example
95
+ * profiles.merge(await store.list());
96
+ */
97
+ merge(graphs: unknown): void;
87
98
  }
@@ -45,8 +45,9 @@ var sanitiseProfiles = (input) => {
45
45
  return [...graphs.values()];
46
46
  };
47
47
  /**
48
- * The user's own profile graphs, alongside the ones PaintCanvas comes with (`PROFILE_PRESETS`). Kept in memory; the app
49
- * settings save and restore them (see `set` and the `change` event). Each gets a UUID when it's first saved.
48
+ * The user's own profile graphs, alongside the ones PaintCanvas comes with (`PROFILE_PRESETS`). Kept in memory; a
49
+ * profile store keeps them (see `PaintCanvas.useAssetStores`), or save and restore them yourself (see `set`, `merge` and
50
+ * the `change` event). Each gets a UUID when it's first saved.
50
51
  *
51
52
  * Dispatches a `change` event when the user's graphs change.
52
53
  *
@@ -61,6 +62,10 @@ var ProfileGraphs = class extends EventTarget {
61
62
  get graphs() {
62
63
  return this.values;
63
64
  }
65
+ /** The user's own graphs (the same as `graphs`, so a store can keep them, see `AssetSync`) */
66
+ get items() {
67
+ return this.values;
68
+ }
64
69
  /** Every graph: the presets, then the user's own */
65
70
  get all() {
66
71
  return [...PROFILE_PRESETS, ...this.values];
@@ -144,5 +149,20 @@ var ProfileGraphs = class extends EventTarget {
144
149
  this.values = next;
145
150
  this.dispatchEvent(new Event("change"));
146
151
  }
152
+ /**
153
+ * Add graphs (e.g. loaded from a store or imported), replacing the user's graphs with the same IDs and keeping the
154
+ * others, up to `MAX_SAVED_PROFILES`. Invalid graphs are left out (see `sanitiseProfiles`).
155
+ *
156
+ * @example
157
+ * profiles.merge(await store.list());
158
+ */
159
+ merge(graphs) {
160
+ const added = sanitiseProfiles(graphs);
161
+ if (!added?.length) return;
162
+ const byId = new Map(this.values.map((graph) => [graph.id, graph]));
163
+ for (const graph of added) byId.set(graph.id, graph);
164
+ this.values = [...byId.values()].slice(0, 100);
165
+ this.dispatchEvent(new Event("change"));
166
+ }
147
167
  };
148
168
  export { ProfileGraphs as default, sanitisePoints, sanitiseProfiles };
@@ -0,0 +1,73 @@
1
+ import type { DocumentStoreLike } from "../../storage";
2
+ import type { DrawingDocument } from "../../document/schema";
3
+ /** How often changes are saved to the remote store, at most (milliseconds): every 5 minutes */
4
+ export declare const REMOTE_SAVE_INTERVAL_MS: number;
5
+ /** Event dispatched when saving fails (detail: the error) */
6
+ export declare const REMOTE_AUTOSAVE_ERROR_EVENT = "error";
7
+ /** The canvas whose drawing is saved (`PaintCanvas`) */
8
+ export interface RemoteAutosaveCanvas extends EventTarget {
9
+ /** The user's layers (replaced when the canvas size changes, followed by a `change` event) */
10
+ readonly layers: EventTarget;
11
+ /** The drawing as a document */
12
+ createDocument(): Promise<DrawingDocument>;
13
+ }
14
+ /** Options for `RemoteDocumentAutosave` */
15
+ export interface RemoteAutosaveOptions {
16
+ /** How often changes are saved, at most (default `REMOTE_SAVE_INTERVAL_MS`) */
17
+ intervalMs?: number;
18
+ }
19
+ /**
20
+ * Keeps a canvas's drawing in a remote store (e.g. a user's account online), less often than the browser keeps it:
21
+ * every `intervalMs` while there are changes not saved yet, or when asked (`save`). The whole document is saved each
22
+ * time, with a thumbnail.
23
+ *
24
+ * Dispatches `change` when saving starts, makes progress or ends, and `error` (detail: the error) when it fails (the
25
+ * changes are tried again next time).
26
+ *
27
+ * @example
28
+ * const autosave = new RemoteDocumentAutosave(canvas, myAccountStore);
29
+ * autosave.start();
30
+ * saveButton.onclick = () => autosave.save();
31
+ */
32
+ export default class RemoteDocumentAutosave extends EventTarget {
33
+ /** Where the drawing is kept */
34
+ readonly store: DocumentStoreLike;
35
+ /** The last saving error, until a save succeeds */
36
+ error?: unknown;
37
+ /** When the drawing was last saved, if it has been */
38
+ lastSaved?: Date;
39
+ /** How much of the save under way is done (0 to 1), if known */
40
+ progress?: number;
41
+ private readonly canvas;
42
+ private readonly intervalMs;
43
+ private timer?;
44
+ /** The layers being watched for changes */
45
+ private watched?;
46
+ /** Whether there are changes not saved yet */
47
+ private dirty;
48
+ /** The save being written, if any */
49
+ private current?;
50
+ private readonly onChange;
51
+ private readonly onCanvasChange;
52
+ constructor(canvas: RemoteAutosaveCanvas, store: DocumentStoreLike, options?: RemoteAutosaveOptions);
53
+ /** Whether a save is under way */
54
+ get saving(): boolean;
55
+ /** Whether there are changes not saved yet */
56
+ get hasChanges(): boolean;
57
+ /**
58
+ * Start saving changes every `intervalMs`.
59
+ */
60
+ start(): void;
61
+ /**
62
+ * Stop saving changes (a save under way finishes). Changes not saved yet aren't saved.
63
+ */
64
+ stop(): void;
65
+ /**
66
+ * Save the drawing now (e.g. when the user asks). While a save is under way, that save is returned.
67
+ */
68
+ save(): Promise<void>;
69
+ /** Save the whole drawing */
70
+ private write;
71
+ /** Tell listeners something changed */
72
+ private notify;
73
+ }