@lvl99dotcom/paintcanvas 0.5.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 (97) hide show
  1. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  2. package/dist/lib/brushes/configRanges/index.js +31 -1
  3. package/dist/lib/brushes/params/index.d.ts +2 -1
  4. package/dist/lib/brushes/params/index.js +7 -4
  5. package/dist/lib/brushes/types/index.d.ts +8 -0
  6. package/dist/lib/classes/Layer/index.d.ts +10 -0
  7. package/dist/lib/classes/Layer/index.js +15 -1
  8. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  9. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  10. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  11. package/dist/lib/classes/LayerStack/index.js +26 -2
  12. package/dist/lib/classes/PaintCanvas/index.d.ts +26 -3
  13. package/dist/lib/classes/PaintCanvas/index.js +39 -12
  14. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  15. package/dist/lib/classes/PngEncoder/index.js +104 -0
  16. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  17. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  18. package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
  19. package/dist/lib/classes/SettingsAutosave/index.js +2 -1
  20. package/dist/lib/messages/schema.d.ts +720 -0
  21. package/dist/lib/presets/schema.d.ts +360 -0
  22. package/dist/lib/presets/schema.js +8 -1
  23. package/dist/lib/profiles/index.d.ts +61 -0
  24. package/dist/lib/profiles/index.js +131 -0
  25. package/dist/lib/profiles/schema.d.ts +73 -0
  26. package/dist/lib/profiles/schema.js +53 -0
  27. package/dist/lib/rendering/sample/index.d.ts +11 -0
  28. package/dist/lib/rendering/sample/index.js +36 -5
  29. package/dist/lib/settings/index.d.ts +2 -1
  30. package/dist/lib/settings/index.js +4 -1
  31. package/dist/lib/settings/schema.d.ts +49 -0
  32. package/dist/lib/settings/schema.js +6 -0
  33. package/dist/lib/validation/validators.generated.js +13064 -3696
  34. package/dist/locales/en-GB.js +30 -0
  35. package/dist/locales/en-US.js +30 -0
  36. package/dist/locales/fr.js +30 -0
  37. package/dist/locales/ja.js +30 -0
  38. package/dist/locales/ko.js +30 -0
  39. package/dist/locales/zh-Hans.js +30 -0
  40. package/dist/locales/zh-Hant.js +30 -0
  41. package/dist/package.js +1 -1
  42. package/dist/ui/ask/index.d.ts +48 -0
  43. package/dist/ui/ask/index.js +67 -0
  44. package/dist/ui/base/index.d.ts +8 -1
  45. package/dist/ui/base/index.js +19 -4
  46. package/dist/ui/elements/PaintCanvas/index.js +3 -1
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
  49. package/dist/ui/elements/PcAppSettings/index.js +21 -65
  50. package/dist/ui/elements/PcBrushConfig/index.d.ts +25 -14
  51. package/dist/ui/elements/PcBrushConfig/index.js +216 -146
  52. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  53. package/dist/ui/elements/PcBrushTest/index.js +7 -2
  54. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  55. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  56. package/dist/ui/elements/PcButton/index.js +18 -10
  57. package/dist/ui/elements/PcCanvas/index.d.ts +6 -1
  58. package/dist/ui/elements/PcCanvas/index.js +10 -3
  59. package/dist/ui/elements/PcCanvas/style.js +1 -1
  60. package/dist/ui/elements/PcClipboardTools/index.d.ts +5 -4
  61. package/dist/ui/elements/PcClipboardTools/index.js +12 -5
  62. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  63. package/dist/ui/elements/PcDialog/index.js +102 -51
  64. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  65. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  66. package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
  67. package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
  68. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  69. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  70. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  71. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  72. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  73. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  74. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  75. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  76. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  77. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  78. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  79. package/dist/ui/elements/PcSliderRange/index.js +307 -0
  80. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  81. package/dist/ui/elements/PcStats/index.js +29 -13
  82. package/dist/ui/elements/PcToolSettings/index.js +6 -6
  83. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  84. package/dist/ui/icons/index.d.ts +1 -1
  85. package/dist/ui/icons/index.js +2 -0
  86. package/dist/ui/index.d.ts +6 -1
  87. package/dist/ui/index.js +6 -1
  88. package/dist/ui/styles/index.d.ts +21 -1
  89. package/dist/ui/styles/index.js +73 -14
  90. package/locales/en-GB.json +30 -0
  91. package/locales/en-US.json +30 -0
  92. package/locales/fr.json +30 -0
  93. package/locales/ja.json +30 -0
  94. package/locales/ko.json +30 -0
  95. package/locales/zh-Hans.json +30 -0
  96. package/locales/zh-Hant.json +30 -0
  97. package/package.json +25 -23
@@ -23,6 +23,13 @@ export declare const PARAM_LIMITS: Record<VaryingParam, {
23
23
  min: number;
24
24
  max: number;
25
25
  }>;
26
+ /**
27
+ * The config key of a setting's variation.
28
+ *
29
+ * @example
30
+ * configKey("width"); // "widthConfig"
31
+ */
32
+ export declare const configKey: (param: VaryingParam) => `${VaryingParam}Config`;
26
33
  /**
27
34
  * Read a setting as the configuration window shows it. When it varies, its range comes from the first source turned
28
35
  * on: scale settings (width, height, opacity) go from `value × min factor` to the value; rotation goes from the angle
@@ -50,3 +57,13 @@ export declare const writeRange: (config: BrushConfig, param: VaryingParam, rang
50
57
  * when it is the first, a starting range (half to full size, the angle itself, or no scatter).
51
58
  */
52
59
  export declare const toggleSource: (config: BrushConfig, param: VaryingParam, source: VariationSource, on: boolean) => Partial<BrushConfig>;
60
+ /**
61
+ * Make a setting vary with one source only (or none, with `undefined`), as config changes. Switching from another
62
+ * source keeps the setting's range; the first source takes a starting range (see `toggleSource`). The setting's profile
63
+ * graph is kept.
64
+ *
65
+ * @example
66
+ * selectSource({ id: "a", widthConfig: { usePressure: true, minPressure: 0.5 } }, "width", "tilt");
67
+ * // { widthConfig: { usePressure: false, useTilt: true, minTilt: 0.5, maxTilt: 1, useRandomise: false, … } }
68
+ */
69
+ export declare const selectSource: (config: BrushConfig, param: VaryingParam, source: VariationSource | undefined) => Partial<BrushConfig>;
@@ -65,6 +65,12 @@ var SOURCE_DEFAULTS = {
65
65
  tilt: [0, 1],
66
66
  randomise: [1, 1]
67
67
  };
68
+ /**
69
+ * The config key of a setting's variation.
70
+ *
71
+ * @example
72
+ * configKey("width"); // "widthConfig"
73
+ */
68
74
  var configKey = (param) => `${param}Config`;
69
75
  var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
70
76
  /** A setting's value as stored (alpha as 0–1) */
@@ -203,4 +209,28 @@ var toggleSource = (config, param, source, on) => {
203
209
  });
204
210
  return { [key]: variation };
205
211
  };
206
- export { PARAM_LIMITS, VARIATION_SOURCES, readRange, toggleSource, writeRange };
212
+ /**
213
+ * Make a setting vary with one source only (or none, with `undefined`), as config changes. Switching from another
214
+ * source keeps the setting's range; the first source takes a starting range (see `toggleSource`). The setting's profile
215
+ * graph is kept.
216
+ *
217
+ * @example
218
+ * selectSource({ id: "a", widthConfig: { usePressure: true, minPressure: 0.5 } }, "width", "tilt");
219
+ * // { widthConfig: { usePressure: false, useTilt: true, minTilt: 0.5, maxTilt: 1, useRandomise: false, … } }
220
+ */
221
+ var selectSource = (config, param, source) => {
222
+ const key = configKey(param);
223
+ const previous = VARIATION_SOURCES.find((each) => readRange(config, param).sources[each]);
224
+ if (source && !previous) return toggleSource(config, param, source, true);
225
+ const variation = { ...config[key] };
226
+ for (const each of VARIATION_SOURCES) variation[SOURCE_KEYS[each][0]] = each === source;
227
+ if (source && previous && source !== previous) {
228
+ const [, fromMin, fromMax] = SOURCE_KEYS[previous];
229
+ const [, toMin, toMax] = SOURCE_KEYS[source];
230
+ const [minDefault, maxDefault] = SOURCE_DEFAULTS[previous];
231
+ variation[toMin] = variation[fromMin] ?? minDefault;
232
+ variation[toMax] = variation[fromMax] ?? maxDefault;
233
+ }
234
+ return { [key]: variation };
235
+ };
236
+ export { PARAM_LIMITS, VARIATION_SOURCES, configKey, readRange, selectSource, toggleSource, writeRange };
@@ -49,7 +49,8 @@ export declare const tiltAmount: (tiltX?: number, tiltY?: number) => number;
49
49
  export declare const tiltDirection: (tiltX?: number, tiltY?: number) => number;
50
50
  /**
51
51
  * Combine a parameter's randomise, pressure and tilt factors into one factor (they multiply). Returns 1 when nothing is enabled.
52
- * Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`).
52
+ * Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`). Each input goes
53
+ * through the parameter's profile graph (see `applyProfile`) before it sets the factor between its min and max.
53
54
  *
54
55
  * @example
55
56
  * variationFactor({ useRandomise: true, minRandomise: 0.5, maxRandomise: 1 }, { isPen: false }, () => 0); // 0.5
@@ -1,3 +1,4 @@
1
+ import { applyProfile } from "../../profiles/index.js";
1
2
  // src/lib/brushes/params/index.ts
2
3
  /**
3
4
  * Linear interpolation between two values.
@@ -27,22 +28,24 @@ var tiltAmount = (tiltX = 0, tiltY = 0) => clamp01(Math.hypot(tiltX, tiltY) / 90
27
28
  var tiltDirection = (tiltX = 0, tiltY = 0) => Math.atan2(tiltY, tiltX) * 180 / Math.PI;
28
29
  /**
29
30
  * Combine a parameter's randomise, pressure and tilt factors into one factor (they multiply). Returns 1 when nothing is enabled.
30
- * Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`).
31
+ * Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`). Each input goes
32
+ * through the parameter's profile graph (see `applyProfile`) before it sets the factor between its min and max.
31
33
  *
32
34
  * @example
33
35
  * variationFactor({ useRandomise: true, minRandomise: 0.5, maxRandomise: 1 }, { isPen: false }, () => 0); // 0.5
34
36
  */
35
37
  var variationFactor = (config, input, random = Math.random) => {
36
38
  if (!config) return 1;
39
+ const { profile } = config;
37
40
  let factor = 1;
38
- if (config.useRandomise) factor *= lerp(config.minRandomise ?? 1, config.maxRandomise ?? 1, random());
41
+ if (config.useRandomise) factor *= lerp(config.minRandomise ?? 1, config.maxRandomise ?? 1, applyProfile(profile, random()));
39
42
  if (config.usePressure) {
40
43
  const pressure = input.isPen ? clamp01(input.pressure ?? 1) : 1;
41
- factor *= lerp(config.minPressure ?? 0, config.maxPressure ?? 1, pressure);
44
+ factor *= lerp(config.minPressure ?? 0, config.maxPressure ?? 1, applyProfile(profile, pressure));
42
45
  }
43
46
  if (config.useTilt) {
44
47
  const tilt = input.isPen ? tiltAmount(input.tiltX, input.tiltY) : 1;
45
- factor *= lerp(config.minTilt ?? 0, config.maxTilt ?? 1, tilt);
48
+ factor *= lerp(config.minTilt ?? 0, config.maxTilt ?? 1, applyProfile(profile, tilt));
46
49
  }
47
50
  return factor;
48
51
  };
@@ -1,3 +1,4 @@
1
+ import type { ProfilePoint } from "../../profiles";
1
2
  export interface AssetAuthorDetails {
2
3
  /** Author's name */
3
4
  name: string;
@@ -94,6 +95,13 @@ export interface BrushParamConfig {
94
95
  * @default 1
95
96
  */
96
97
  maxTilt?: number;
98
+ /**
99
+ * The profile graph the input (pressure, tilt or the random number) goes through before setting the factor between
100
+ * its min and max, e.g. so a light touch goes most of the way (see `applyProfile`). A straight line by default.
101
+ */
102
+ profile?: ProfilePoint[];
103
+ /** The ID of the profile graph `profile` came from (a preset's or one of the user's), to show its name */
104
+ profileId?: string;
97
105
  }
98
106
  export interface BrushConfig {
99
107
  /** Unique ID */
@@ -29,6 +29,16 @@ export type CanvasFactory = (width: number, height: number) => OffscreenCanvas;
29
29
  * Create an `OffscreenCanvas` of the given size.
30
30
  */
31
31
  export declare const createOffscreenCanvas: CanvasFactory;
32
+ /**
33
+ * Create an `OffscreenCanvas` whose pixels are kept in regular memory rather than on the GPU (its 2D context is made
34
+ * with `willReadFrequently`). Reading its pixels, encoding it and copying it are then plain memory copies, which on
35
+ * phones avoids long stalls waiting for the GPU (up to 15 seconds were seen on an iPhone), at the cost of drawing on
36
+ * the CPU. PaintCanvas makes its canvases this way by default.
37
+ *
38
+ * @example
39
+ * const canvas = createMemoryCanvas(2000, 2000);
40
+ */
41
+ export declare const createMemoryCanvas: CanvasFactory;
32
42
  /**
33
43
  * Clamp an opacity value to 0–1.
34
44
  *
@@ -16,6 +16,20 @@ var MAX_LAYER_NAME_LENGTH = 64;
16
16
  */
17
17
  var createOffscreenCanvas = (width, height) => new OffscreenCanvas(width, height);
18
18
  /**
19
+ * Create an `OffscreenCanvas` whose pixels are kept in regular memory rather than on the GPU (its 2D context is made
20
+ * with `willReadFrequently`). Reading its pixels, encoding it and copying it are then plain memory copies, which on
21
+ * phones avoids long stalls waiting for the GPU (up to 15 seconds were seen on an iPhone), at the cost of drawing on
22
+ * the CPU. PaintCanvas makes its canvases this way by default.
23
+ *
24
+ * @example
25
+ * const canvas = createMemoryCanvas(2000, 2000);
26
+ */
27
+ var createMemoryCanvas = (width, height) => {
28
+ const canvas = new OffscreenCanvas(width, height);
29
+ canvas.getContext("2d", { willReadFrequently: true });
30
+ return canvas;
31
+ };
32
+ /**
19
33
  * Clamp an opacity value to 0–1.
20
34
  *
21
35
  * @example
@@ -108,4 +122,4 @@ var Layer = class {
108
122
  };
109
123
  }
110
124
  };
111
- export { BlendMode, MAX_LAYER_NAME_LENGTH, clampOpacity, createOffscreenCanvas, Layer as default };
125
+ export { BlendMode, MAX_LAYER_NAME_LENGTH, clampOpacity, createMemoryCanvas, createOffscreenCanvas, Layer as default };
@@ -34,7 +34,7 @@ export interface AutosaveOptions {
34
34
  locks?: LockManager | null;
35
35
  /** Tells when the page is hidden or shown again (the page's document by default) */
36
36
  document?: Pick<Document, "visibilityState" | "addEventListener" | "removeEventListener"> | null;
37
- /** Turns a layer's canvas into a PNG (replaceable for tests) */
37
+ /** Turns a layer's canvas into a PNG (in a background worker by default; replaceable for tests) */
38
38
  encode?: (canvas: OffscreenCanvas) => Promise<Blob>;
39
39
  /** Turns a saved PNG into an image to draw (replaceable for tests) */
40
40
  decode?: (image: Blob) => Promise<CanvasImageSource>;
@@ -44,8 +44,8 @@ export interface AutosaveOptions {
44
44
  /**
45
45
  * Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
46
46
  * starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
47
- * Only layers whose pixels changed are encoded and saved again; a quick fingerprint of each layer's pixels tells
48
- * which.
47
+ * Only layers whose pixels changed are encoded and saved again: the layers' change counts tell which (see
48
+ * `LayerStack.pixelVersion`), without reading their pixels, which is slow on phones.
49
49
  *
50
50
  * Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
51
51
  * (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
@@ -74,14 +74,10 @@ export default class LayerAutosave extends EventTarget {
74
74
  private readonly delayMs;
75
75
  /** The layers being watched for changes */
76
76
  private watched?;
77
- /** Fingerprints of each layer's pixels as last saved, by layer ID */
78
- private prints;
77
+ /** Each layer as last saved, by layer ID: the layer itself and its change count (see `LayerStack.pixelVersion`) */
78
+ private saved;
79
79
  /** Size in bytes of each layer's image as last saved or restored, by layer ID */
80
80
  private sizes;
81
- /** Layers whose pixels changed since the last save */
82
- private changed;
83
- /** Whether any layer may have changed since the last save (not only those in `changed`) */
84
- private checkAll;
85
81
  private timer?;
86
82
  /** The save being written, if any */
87
83
  private saving?;
@@ -133,11 +129,13 @@ export default class LayerAutosave extends EventTarget {
133
129
  */
134
130
  private restore;
135
131
  /**
136
- * Remember each layer's pixels as saved (just restored).
132
+ * Remember each layer as saved (just restored).
137
133
  */
138
134
  private remember;
135
+ /** A layer and its change count, to tell later whether it changed */
136
+ private record;
139
137
  /**
140
- * Save the drawing, encoding only the layers whose pixels changed since they were last saved.
138
+ * Save the drawing, encoding only the layers whose pixels changed since they were last saved (or that are new).
141
139
  */
142
140
  private write;
143
141
  /**
@@ -145,10 +143,6 @@ export default class LayerAutosave extends EventTarget {
145
143
  * changed.
146
144
  */
147
145
  private rememberSizes;
148
- /**
149
- * A fingerprint of a layer's pixels.
150
- */
151
- private fingerprint;
152
146
  /**
153
147
  * Save once the layers have stopped changing for a moment and drawing pauses.
154
148
  */
@@ -1,4 +1,4 @@
1
- import { pixelFingerprint } from "../../image/index.js";
1
+ import PngEncoder from "../PngEncoder/index.js";
2
2
  import "../../storage/index.js";
3
3
  // src/lib/classes/LayerAutosave/index.ts
4
4
  /** How long to wait after the layers change before saving, so quick changes are saved together (milliseconds) */
@@ -18,12 +18,14 @@ var AutosaveState = {
18
18
  /** Not saving any more (see `stop`) */
19
19
  Stopped: "stopped"
20
20
  };
21
+ /** Encodes layers as PNGs in a background worker, shared by every autosave on the page (made on first use) */
22
+ var sharedEncoder;
21
23
  /**
22
- * Encode a canvas as a PNG.
24
+ * Encode a canvas as a PNG, in a background worker where one can be used, so saving doesn't freeze the page.
23
25
  */
24
26
  var encodePng = (canvas) => {
25
- canvas.getContext("2d");
26
- return canvas.convertToBlob({ type: "image/png" });
27
+ sharedEncoder ??= new PngEncoder();
28
+ return sharedEncoder.encode(canvas);
27
29
  };
28
30
  /**
29
31
  * Whether a saved drawing has the expected shape and an image for each layer.
@@ -32,8 +34,8 @@ var isComplete = (drawing, images) => drawing.version === 1 && drawing.layers.le
32
34
  /**
33
35
  * Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
34
36
  * starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
35
- * Only layers whose pixels changed are encoded and saved again; a quick fingerprint of each layer's pixels tells
36
- * which.
37
+ * Only layers whose pixels changed are encoded and saved again: the layers' change counts tell which (see
38
+ * `LayerStack.pixelVersion`), without reading their pixels, which is slow on phones.
37
39
  *
38
40
  * Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
39
41
  * (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
@@ -62,14 +64,10 @@ var LayerAutosave = class extends EventTarget {
62
64
  delayMs;
63
65
  /** The layers being watched for changes */
64
66
  watched;
65
- /** Fingerprints of each layer's pixels as last saved, by layer ID */
66
- prints = /* @__PURE__ */ new Map();
67
+ /** Each layer as last saved, by layer ID: the layer itself and its change count (see `LayerStack.pixelVersion`) */
68
+ saved = /* @__PURE__ */ new Map();
67
69
  /** Size in bytes of each layer's image as last saved or restored, by layer ID */
68
70
  sizes = /* @__PURE__ */ new Map();
69
- /** Layers whose pixels changed since the last save */
70
- changed = /* @__PURE__ */ new Set();
71
- /** Whether any layer may have changed since the last save (not only those in `changed`) */
72
- checkAll = false;
73
71
  timer;
74
72
  /** The save being written, if any */
75
73
  saving;
@@ -77,12 +75,7 @@ var LayerAutosave = class extends EventTarget {
77
75
  again = false;
78
76
  /** Lets go of the drawing's lock, while held */
79
77
  release;
80
- onLayersChange = () => {
81
- const id = this.watched?.changedLayerId;
82
- if (id) this.changed.add(id);
83
- else this.checkAll = true;
84
- this.schedule();
85
- };
78
+ onLayersChange = () => this.schedule();
86
79
  onCanvasChange = () => {
87
80
  if (this.canvas.layers === this.watched) return;
88
81
  this.watch();
@@ -202,7 +195,7 @@ var LayerAutosave = class extends EventTarget {
202
195
  return;
203
196
  }
204
197
  if (this.currentState !== AutosaveState.Starting) return;
205
- this.prints = /* @__PURE__ */ new Map();
198
+ this.saved = /* @__PURE__ */ new Map();
206
199
  this.watch();
207
200
  if (restored) this.remember();
208
201
  this.setState(AutosaveState.Ready);
@@ -222,29 +215,32 @@ var LayerAutosave = class extends EventTarget {
222
215
  return true;
223
216
  }
224
217
  /**
225
- * Remember each layer's pixels as saved (just restored).
218
+ * Remember each layer as saved (just restored).
226
219
  */
227
220
  remember() {
228
- for (const layer of this.canvas.layers.layers) this.prints.set(layer.id, this.fingerprint(layer.id));
229
- this.changed.clear();
230
- this.checkAll = false;
221
+ const stack = this.canvas.layers;
222
+ for (const layer of stack.layers) this.saved.set(layer.id, this.record(layer));
223
+ }
224
+ /** A layer and its change count, to tell later whether it changed */
225
+ record(layer) {
226
+ return {
227
+ layer,
228
+ version: this.canvas.layers.pixelVersion(layer.id)
229
+ };
231
230
  }
232
231
  /**
233
- * Save the drawing, encoding only the layers whose pixels changed since they were last saved.
232
+ * Save the drawing, encoding only the layers whose pixels changed since they were last saved (or that are new).
234
233
  */
235
234
  async write() {
236
235
  const stack = this.canvas.layers;
237
- const { checkAll, changed } = this;
238
- this.checkAll = false;
239
- this.changed = /* @__PURE__ */ new Set();
240
- const prints = /* @__PURE__ */ new Map();
236
+ const encoded = /* @__PURE__ */ new Map();
241
237
  try {
242
238
  const encoding = [];
243
239
  for (const layer of stack.layers) {
244
- if (!!this.prints.has(layer.id) && !checkAll && !changed.has(layer.id)) continue;
245
- const print = this.fingerprint(layer.id);
246
- if (print === this.prints.get(layer.id)) continue;
247
- prints.set(layer.id, print);
240
+ const record = this.record(layer);
241
+ const saved = this.saved.get(layer.id);
242
+ if (saved?.layer === layer && saved.version === record.version) continue;
243
+ encoded.set(layer.id, record);
248
244
  encoding.push(this.encode(layer.canvas).then((image) => [layer.id, image]));
249
245
  }
250
246
  const drawing = {
@@ -258,13 +254,11 @@ var LayerAutosave = class extends EventTarget {
258
254
  if (this.currentState !== AutosaveState.Ready) return;
259
255
  await this.store.save(drawing, images);
260
256
  const kept = new Set(drawing.layers.map((layer) => layer.id));
261
- for (const id of this.prints.keys()) if (!kept.has(id)) this.prints.delete(id);
262
- for (const [id, print] of prints) this.prints.set(id, print);
257
+ for (const id of this.saved.keys()) if (!kept.has(id)) this.saved.delete(id);
258
+ for (const [id, record] of encoded) this.saved.set(id, record);
263
259
  this.error = void 0;
264
260
  this.rememberSizes(kept, images);
265
261
  } catch (error) {
266
- for (const id of [...changed, ...prints.keys()]) this.changed.add(id);
267
- this.checkAll ||= checkAll;
268
262
  this.fail(error);
269
263
  }
270
264
  }
@@ -279,13 +273,6 @@ var LayerAutosave = class extends EventTarget {
279
273
  if (this.storedBytes !== before) this.dispatchEvent(new Event("change"));
280
274
  }
281
275
  /**
282
- * A fingerprint of a layer's pixels.
283
- */
284
- fingerprint(id) {
285
- const layer = this.canvas.layers.get(id);
286
- return pixelFingerprint(layer.context.getImageData(0, 0, layer.width, layer.height).data);
287
- }
288
- /**
289
276
  * Save once the layers have stopped changing for a moment and drawing pauses.
290
277
  */
291
278
  schedule() {
@@ -29,6 +29,8 @@ export default class LayerStack extends EventTarget {
29
29
  /** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
30
30
  private onlyChanged?;
31
31
  private readonly createCanvas;
32
+ /** How many times each layer's image data has been changed through the stack (see `pixelVersion`) */
33
+ private readonly versions;
32
34
  /** Most layers the stack can have */
33
35
  readonly maxLayers: number;
34
36
  constructor(width: number, height: number, createCanvas?: CanvasFactory, maxLayers?: number);
@@ -71,6 +73,17 @@ export default class LayerStack extends EventTarget {
71
73
  * `markChanged`, e.g. a brush stroke); undefined when anything may have changed.
72
74
  */
73
75
  get changedLayerId(): string | undefined;
76
+ /**
77
+ * How many times a layer's image data has been changed through the stack (`markChanged`, `changeImage`, `clear`,
78
+ * merging into it, and undoing or redoing those), to tell whether it changed since some point without reading its
79
+ * pixels (which is slow on phones). A new layer starts at 0.
80
+ *
81
+ * @example
82
+ * const before = stack.pixelVersion(id);
83
+ * stack.markChanged(id);
84
+ * stack.pixelVersion(id) > before; // true
85
+ */
86
+ pixelVersion(id: string): number;
74
87
  /**
75
88
  * Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
76
89
  */
@@ -150,6 +163,8 @@ export default class LayerStack extends EventTarget {
150
163
  * An undoable change of image data: undo and redo both swap the stored pixels with the layer's, then select the layer.
151
164
  */
152
165
  private imageEntry;
166
+ /** Count a change to a layer's image data */
167
+ private touch;
153
168
  /**
154
169
  * Create a layer with the stack's size and canvas factory.
155
170
  */
@@ -31,6 +31,8 @@ var LayerStack = class extends EventTarget {
31
31
  /** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
32
32
  onlyChanged;
33
33
  createCanvas;
34
+ /** How many times each layer's image data has been changed through the stack (see `pixelVersion`) */
35
+ versions = /* @__PURE__ */ new WeakMap();
34
36
  /** Most layers the stack can have */
35
37
  maxLayers;
36
38
  constructor(width, height, createCanvas = createOffscreenCanvas, maxLayers = 5) {
@@ -130,10 +132,23 @@ var LayerStack = class extends EventTarget {
130
132
  return this.onlyChanged;
131
133
  }
132
134
  /**
135
+ * How many times a layer's image data has been changed through the stack (`markChanged`, `changeImage`, `clear`,
136
+ * merging into it, and undoing or redoing those), to tell whether it changed since some point without reading its
137
+ * pixels (which is slow on phones). A new layer starts at 0.
138
+ *
139
+ * @example
140
+ * const before = stack.pixelVersion(id);
141
+ * stack.markChanged(id);
142
+ * stack.pixelVersion(id) > before; // true
143
+ */
144
+ pixelVersion(id) {
145
+ return this.versions.get(this.get(id)) ?? 0;
146
+ }
147
+ /**
133
148
  * Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
134
149
  */
135
150
  markChanged(id) {
136
- this.get(id);
151
+ this.touch(this.get(id));
137
152
  this.onlyChanged = id;
138
153
  try {
139
154
  this.notify();
@@ -150,6 +165,7 @@ var LayerStack = class extends EventTarget {
150
165
  const area = contentBounds(data, this.width, this.height);
151
166
  if (area) this.recordImage(id, area);
152
167
  layer.clear();
168
+ this.touch(layer);
153
169
  this.notify();
154
170
  }
155
171
  /**
@@ -174,7 +190,9 @@ var LayerStack = class extends EventTarget {
174
190
  */
175
191
  changeImage(id, area, change) {
176
192
  this.recordImage(id, area);
177
- change(this.get(id).context);
193
+ const layer = this.get(id);
194
+ change(layer.context);
195
+ this.touch(layer);
178
196
  this.notify();
179
197
  }
180
198
  /**
@@ -209,6 +227,7 @@ var LayerStack = class extends EventTarget {
209
227
  ctx.globalCompositeOperation = layer.blendMode;
210
228
  ctx.drawImage(layer.canvas, x, y, width, height, x, y, width, height);
211
229
  ctx.restore();
230
+ this.touch(below);
212
231
  }
213
232
  this.items = this.items.filter((l) => l !== layer);
214
233
  this.activeId = below.id;
@@ -332,6 +351,7 @@ var LayerStack = class extends EventTarget {
332
351
  ctx.clearRect(area.x, area.y, area.width, area.height);
333
352
  ctx.drawImage(stored, area.x, area.y);
334
353
  stored = current;
354
+ this.touch(layer);
335
355
  this.select(id);
336
356
  };
337
357
  return {
@@ -339,6 +359,10 @@ var LayerStack = class extends EventTarget {
339
359
  redo: swap
340
360
  };
341
361
  }
362
+ /** Count a change to a layer's image data */
363
+ touch(layer) {
364
+ this.versions.set(layer, this.pixelVersion(layer.id) + 1);
365
+ }
342
366
  /**
343
367
  * Create a layer with the stack's size and canvas factory.
344
368
  */
@@ -20,6 +20,7 @@ import CustomPresets, { type PresetStoreLike } from "../CustomPresets";
20
20
  import InputSettings from "../InputSettings";
21
21
  import UiSettings from "../UiSettings";
22
22
  import Palette from "../Palette";
23
+ import ProfileGraphs from "../ProfileGraphs";
23
24
  import ShortcutSettings from "../ShortcutSettings";
24
25
  import ExternalSheets from "../ExternalSheets";
25
26
  import { type CanvasMessage, type PatchMessage, type StrokeMessage } from "../../messages";
@@ -114,7 +115,11 @@ export interface PaintCanvasOptions {
114
115
  maxCanvasSize?: number;
115
116
  /** Most layers the user can have (default `DEFAULT_MAX_LAYERS`) */
116
117
  maxLayers?: number;
117
- /** 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
+ */
118
123
  createCanvas?: CanvasFactory;
119
124
  /** Where custom brush presets are kept (defaults to keeping them in memory only) */
120
125
  presetStore?: PresetStoreLike;
@@ -171,6 +176,8 @@ export default class PaintCanvas extends EventTarget {
171
176
  readonly ui: UiSettings;
172
177
  /** The user's saved colours, shown as swatches */
173
178
  readonly palette: Palette;
179
+ /** The user's own profile graphs (and the presets), for shaping how brush settings vary */
180
+ readonly profiles: ProfileGraphs;
174
181
  /** The user's keyboard shortcuts: each configurable action's key combination */
175
182
  readonly shortcuts: ShortcutSettings;
176
183
  /** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
@@ -439,6 +446,20 @@ export default class PaintCanvas extends EventTarget {
439
446
  * nothing during a stroke or while a transform is pending. Returns the area copied, if any.
440
447
  */
441
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;
442
463
  /**
443
464
  * How many times image data has been copied or cut, to tell whether it was copied here since something else may have
444
465
  * been copied in another app (see `KeyboardShortcuts`).
@@ -484,9 +505,11 @@ export default class PaintCanvas extends EventTarget {
484
505
  * data when its alpha lock is on. None if that is empty.
485
506
  */
486
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();
487
510
  /**
488
- * The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
489
- * 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.
490
513
  */
491
514
  private maskedArea;
492
515
  /**