@lvl99dotcom/paintcanvas 0.5.0 → 0.7.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 (120) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  3. package/dist/lib/brushes/configRanges/index.js +31 -1
  4. package/dist/lib/brushes/params/index.d.ts +2 -1
  5. package/dist/lib/brushes/params/index.js +7 -4
  6. package/dist/lib/brushes/types/index.d.ts +8 -0
  7. package/dist/lib/classes/Layer/index.d.ts +10 -0
  8. package/dist/lib/classes/Layer/index.js +15 -1
  9. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  10. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  11. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  12. package/dist/lib/classes/LayerStack/index.js +26 -2
  13. package/dist/lib/classes/PaintCanvas/index.d.ts +36 -5
  14. package/dist/lib/classes/PaintCanvas/index.js +45 -14
  15. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  16. package/dist/lib/classes/PngEncoder/index.js +104 -0
  17. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  18. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  19. package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
  20. package/dist/lib/classes/SettingsAutosave/index.js +2 -1
  21. package/dist/lib/classes/UiSettings/index.d.ts +24 -2
  22. package/dist/lib/classes/UiSettings/index.js +32 -7
  23. package/dist/lib/messages/schema.d.ts +720 -0
  24. package/dist/lib/presets/schema.d.ts +360 -0
  25. package/dist/lib/presets/schema.js +8 -1
  26. package/dist/lib/profiles/index.d.ts +61 -0
  27. package/dist/lib/profiles/index.js +131 -0
  28. package/dist/lib/profiles/schema.d.ts +73 -0
  29. package/dist/lib/profiles/schema.js +53 -0
  30. package/dist/lib/rendering/sample/index.d.ts +11 -0
  31. package/dist/lib/rendering/sample/index.js +36 -5
  32. package/dist/lib/settings/index.d.ts +2 -1
  33. package/dist/lib/settings/index.js +4 -1
  34. package/dist/lib/settings/schema.d.ts +53 -1
  35. package/dist/lib/settings/schema.js +9 -2
  36. package/dist/lib/storage/index.d.ts +26 -0
  37. package/dist/lib/storage/index.js +38 -1
  38. package/dist/lib/validation/validators.generated.js +13061 -3675
  39. package/dist/lib/version/index.d.ts +25 -0
  40. package/dist/lib/version/index.js +35 -0
  41. package/dist/locales/en-GB.js +35 -0
  42. package/dist/locales/en-US.js +35 -0
  43. package/dist/locales/fr.js +35 -0
  44. package/dist/locales/ja.js +35 -0
  45. package/dist/locales/ko.js +35 -0
  46. package/dist/locales/zh-Hans.js +35 -0
  47. package/dist/locales/zh-Hant.js +35 -0
  48. package/dist/package.js +1 -1
  49. package/dist/ui/ask/index.d.ts +48 -0
  50. package/dist/ui/ask/index.js +67 -0
  51. package/dist/ui/base/index.d.ts +8 -1
  52. package/dist/ui/base/index.js +19 -4
  53. package/dist/ui/elements/PaintCanvas/index.d.ts +13 -3
  54. package/dist/ui/elements/PaintCanvas/index.js +25 -6
  55. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  56. package/dist/ui/elements/PcAppSettings/index.d.ts +15 -7
  57. package/dist/ui/elements/PcAppSettings/index.js +53 -62
  58. package/dist/ui/elements/PcBrushConfig/index.d.ts +26 -14
  59. package/dist/ui/elements/PcBrushConfig/index.js +214 -145
  60. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  61. package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
  62. package/dist/ui/elements/PcBrushTest/index.js +15 -9
  63. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  64. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  65. package/dist/ui/elements/PcButton/index.js +18 -10
  66. package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
  67. package/dist/ui/elements/PcButtonMenu/index.js +125 -0
  68. package/dist/ui/elements/PcCanvas/index.d.ts +15 -1
  69. package/dist/ui/elements/PcCanvas/index.js +42 -19
  70. package/dist/ui/elements/PcCanvas/style.js +1 -1
  71. package/dist/ui/elements/PcClipboardTools/index.d.ts +11 -7
  72. package/dist/ui/elements/PcClipboardTools/index.js +30 -29
  73. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  74. package/dist/ui/elements/PcDialog/index.js +102 -51
  75. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  76. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  77. package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
  78. package/dist/ui/elements/PcInputSettings/index.js +9 -15
  79. package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
  80. package/dist/ui/elements/PcLayerItem/index.js +53 -26
  81. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  82. package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
  83. package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
  84. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  85. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  86. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  87. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  88. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  89. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  90. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  91. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  92. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  93. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  94. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  95. package/dist/ui/elements/PcSliderRange/index.js +308 -0
  96. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  97. package/dist/ui/elements/PcStats/index.js +29 -13
  98. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
  99. package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
  100. package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
  101. package/dist/ui/elements/PcToolSettings/index.js +14 -14
  102. package/dist/ui/elements/PcVersion/index.d.ts +12 -0
  103. package/dist/ui/elements/PcVersion/index.js +43 -0
  104. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  105. package/dist/ui/icons/index.d.ts +1 -1
  106. package/dist/ui/icons/index.js +2 -0
  107. package/dist/ui/index.d.ts +9 -2
  108. package/dist/ui/index.js +9 -2
  109. package/dist/ui/layout/index.d.ts +2 -2
  110. package/dist/ui/layout/index.js +2 -2
  111. package/dist/ui/styles/index.d.ts +21 -1
  112. package/dist/ui/styles/index.js +73 -14
  113. package/locales/en-GB.json +35 -0
  114. package/locales/en-US.json +35 -0
  115. package/locales/fr.json +35 -0
  116. package/locales/ja.json +35 -0
  117. package/locales/ko.json +35 -0
  118. package/locales/zh-Hans.json +35 -0
  119. package/locales/zh-Hant.json +35 -0
  120. package/package.json +25 -23
@@ -11,7 +11,7 @@ import Selection from "../../selection/Selection/index.js";
11
11
  import Lasso from "../../selection/Lasso/index.js";
12
12
  import Transformer, { applyMask, contentBounds, copyArea, drawTransformed } from "../../transform/Transformer/index.js";
13
13
  import { boxBounds } from "../../transform/geometry/index.js";
14
- import Layer, { createOffscreenCanvas } from "../Layer/index.js";
14
+ import Layer, { createMemoryCanvas, createOffscreenCanvas } from "../Layer/index.js";
15
15
  import LayerAutosave from "../LayerAutosave/index.js";
16
16
  import { brushPresets, eraserPresets } from "../../brushes/index.js";
17
17
  import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
@@ -19,10 +19,11 @@ import "../../presets/index.js";
19
19
  import CustomPresets from "../CustomPresets/index.js";
20
20
  import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
21
21
  import InputSettings from "../InputSettings/index.js";
22
- import UiSettings from "../UiSettings/index.js";
22
+ import UiSettings, { DEFAULT_UI_SETTINGS } from "../UiSettings/index.js";
23
23
  import Palette from "../Palette/index.js";
24
- import ShortcutSettings from "../ShortcutSettings/index.js";
25
24
  import { PALETTE_FILE_FORMAT } from "../../palette/schema.js";
25
+ import ProfileGraphs from "../ProfileGraphs/index.js";
26
+ import ShortcutSettings from "../ShortcutSettings/index.js";
26
27
  import "../../palette/index.js";
27
28
  import { SETTINGS_FILE_FORMAT } from "../../settings/schema.js";
28
29
  import ExternalSheets from "../ExternalSheets/index.js";
@@ -126,10 +127,12 @@ var PaintCanvas = class extends EventTarget {
126
127
  background = new BackgroundWork(this.strokeLog);
127
128
  /** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
128
129
  input = new InputSettings();
129
- /** The user's interface choices (theme and language), kept with the other app settings */
130
- ui = new UiSettings();
130
+ /** The user's interface choices (theme, language and where canvases draw), kept with the other app settings */
131
+ ui;
131
132
  /** The user's saved colours, shown as swatches */
132
133
  palette = new Palette();
134
+ /** The user's own profile graphs (and the presets), for shaping how brush settings vary */
135
+ profiles = new ProfileGraphs();
133
136
  /** The user's keyboard shortcuts: each configurable action's key combination */
134
137
  shortcuts = new ShortcutSettings();
135
138
  /** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
@@ -209,6 +212,8 @@ var PaintCanvas = class extends EventTarget {
209
212
  /** The layer stack whose changes are being sent */
210
213
  sendingLayers;
211
214
  onLayersChangeToSend = () => this.sender?.schedule();
215
+ /** Where the canvases draw, chosen when the canvas started (the user's choice for next time is `ui.data.rendering`) */
216
+ rendering;
212
217
  /** Creates the canvases the app draws on (fake ones in tests) */
213
218
  createCanvas;
214
219
  /** Who the custom presets belong to (passed to the preset store) */
@@ -239,7 +244,9 @@ var PaintCanvas = class extends EventTarget {
239
244
  this.customPresets = new CustomPresets(this.ownerId, options.presetStore);
240
245
  this.customPresets.addEventListener("change", () => this.listCustomPresets());
241
246
  this.customPresets.load().catch(() => void 0);
242
- this.createCanvas = options.createCanvas ?? createOffscreenCanvas;
247
+ this.rendering = options.rendering ?? DEFAULT_UI_SETTINGS.rendering;
248
+ this.ui = new UiSettings({ rendering: this.rendering });
249
+ this.createCanvas = options.createCanvas ?? (this.rendering === "gpu" ? createOffscreenCanvas : createMemoryCanvas);
243
250
  this.systemClipboard = options.systemClipboard === null ? void 0 : options.systemClipboard ?? globalThis.navigator?.clipboard;
244
251
  this.canvasWidth = DEFAULT_CANVAS_SIZE;
245
252
  this.canvasHeight = DEFAULT_CANVAS_SIZE;
@@ -715,11 +722,31 @@ var PaintCanvas = class extends EventTarget {
715
722
  * nothing during a stroke or while a transform is pending. Returns the area copied, if any.
716
723
  */
717
724
  copy() {
718
- const area = this.maskedArea();
725
+ return this.copyMasked(this.layerStack.active.canvas);
726
+ }
727
+ /**
728
+ * Copy the masked image data of all the shown layers, mixed as they're shown (each layer's opacity and blend mode,
729
+ * without the white background), to the clipboard. Hidden layers are left out. Like `copy`, it needs a mask with image
730
+ * data inside it, and does nothing during a stroke or while a transform is pending. Returns the area copied, if any.
731
+ *
732
+ * @example
733
+ * app.selection.selectAll();
734
+ * app.copyMerged(); // { x: 10, y: 20, width: 30, height: 5 }: the part of the canvas with paint on any shown layer
735
+ */
736
+ copyMerged() {
737
+ if (!this.canCopy) return void 0;
738
+ const merged = this.createCanvas(this.width, this.height);
739
+ drawLayers(merged.getContext("2d"), this.layerStack.layers);
740
+ return this.copyMasked(merged);
741
+ }
742
+ /**
743
+ * Copy the masked part of a canvas-sized image to the clipboard (see `copy`), returning the area copied, if any.
744
+ */
745
+ copyMasked(source) {
746
+ const area = this.maskedArea(source);
719
747
  const mask = this.currentSelection.mask;
720
748
  if (!area || !mask) return void 0;
721
- const layer = this.layerStack.active;
722
- const image = copyArea(this.createCanvas, layer.canvas, area);
749
+ const image = copyArea(this.createCanvas, source, area);
723
750
  applyMask(image, copyArea(this.createCanvas, mask, area));
724
751
  this.clipboard = {
725
752
  image,
@@ -807,15 +834,19 @@ var PaintCanvas = class extends EventTarget {
807
834
  height
808
835
  };
809
836
  }
837
+ /** Whether image data can be copied now: not during a stroke or while a transform is pending */
838
+ get canCopy() {
839
+ return !this.strokeEngine.drawing && !this.transformer.pending;
840
+ }
810
841
  /**
811
- * The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
812
- * during a stroke or while a transform is pending.
842
+ * The bounds of a canvas-sized image's data (the selected layer's by default) inside the mask, if there is a mask
843
+ * with image data inside it. None during a stroke or while a transform is pending.
813
844
  */
814
- maskedArea() {
845
+ maskedArea(source = this.layerStack.active.canvas) {
815
846
  const mask = this.currentSelection.mask;
816
- if (!mask || this.strokeEngine.drawing || this.transformer.pending) return void 0;
847
+ if (!mask || !this.canCopy) return void 0;
817
848
  const { width, height } = this;
818
- return contentBounds(this.layerStack.active.context.getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
849
+ return contentBounds(source.getContext("2d").getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
819
850
  }
820
851
  /**
821
852
  * Erase the mask's part of an area from the selected layer, or paint it with the background colour when the layer's
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Encode a canvas as a PNG on the page's own thread (which blocks the page while it runs: seconds on phones for a large
3
+ * canvas).
4
+ *
5
+ * @example
6
+ * const png = await encodePngHere(layer.canvas);
7
+ */
8
+ export declare const encodePngHere: (canvas: OffscreenCanvas) => Promise<Blob>;
9
+ /** The worker's code: draws the image it's sent onto its own canvas and encodes that as a PNG */
10
+ export declare const PNG_WORKER_SOURCE = "self.onmessage = async (event) => {\n const { id, bitmap } = event.data;\n try {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n canvas.getContext(\"2d\").drawImage(bitmap, 0, 0);\n bitmap.close();\n self.postMessage({ id, blob: await canvas.convertToBlob({ type: \"image/png\" }) });\n } catch (error) {\n self.postMessage({ id, error: String(error) });\n }\n};";
11
+ /** Makes the worker, or gives null where workers can't be used */
12
+ export type WorkerFactory = () => Worker | null;
13
+ /**
14
+ * Make the PNG worker from its code (no separate file, so any bundler works), or null where there are no workers or
15
+ * `createImageBitmap`, or the page doesn't allow workers from `blob:` addresses.
16
+ */
17
+ export declare const createPngWorker: WorkerFactory;
18
+ /**
19
+ * Encodes canvases as PNGs in a background worker, so the page keeps responding while large layers are saved. The
20
+ * canvas is copied (`createImageBitmap`) and sent to the worker, which encodes the copy. Where a worker can't be used,
21
+ * or it fails, it encodes on the page's own thread instead (see `encodePngHere`).
22
+ *
23
+ * @example
24
+ * const encoder = new PngEncoder();
25
+ * const png = await encoder.encode(layer.canvas);
26
+ */
27
+ export default class PngEncoder {
28
+ private readonly createWorker;
29
+ /** The worker, once made (null when it can't be used) */
30
+ private worker?;
31
+ private nextId;
32
+ /** Encodings waiting for the worker, by ID */
33
+ private readonly waiting;
34
+ constructor(createWorker?: WorkerFactory);
35
+ /**
36
+ * Encode a canvas as a PNG, in the worker where it can be used.
37
+ */
38
+ encode: (canvas: OffscreenCanvas) => Promise<Blob>;
39
+ /** The worker, made on first use */
40
+ private getWorker;
41
+ }
@@ -0,0 +1,104 @@
1
+ // src/lib/classes/PngEncoder/index.ts
2
+ /**
3
+ * Encode a canvas as a PNG on the page's own thread (which blocks the page while it runs: seconds on phones for a large
4
+ * canvas).
5
+ *
6
+ * @example
7
+ * const png = await encodePngHere(layer.canvas);
8
+ */
9
+ var encodePngHere = (canvas) => {
10
+ canvas.getContext("2d");
11
+ return canvas.convertToBlob({ type: "image/png" });
12
+ };
13
+ /** The worker's code: draws the image it's sent onto its own canvas and encodes that as a PNG */
14
+ var PNG_WORKER_SOURCE = `self.onmessage = async (event) => {
15
+ const { id, bitmap } = event.data;
16
+ try {
17
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
18
+ canvas.getContext("2d").drawImage(bitmap, 0, 0);
19
+ bitmap.close();
20
+ self.postMessage({ id, blob: await canvas.convertToBlob({ type: "image/png" }) });
21
+ } catch (error) {
22
+ self.postMessage({ id, error: String(error) });
23
+ }
24
+ };`;
25
+ /**
26
+ * Make the PNG worker from its code (no separate file, so any bundler works), or null where there are no workers or
27
+ * `createImageBitmap`, or the page doesn't allow workers from `blob:` addresses.
28
+ */
29
+ var createPngWorker = () => {
30
+ if (typeof Worker === "undefined" || typeof createImageBitmap === "undefined") return null;
31
+ try {
32
+ const url = URL.createObjectURL(new Blob([PNG_WORKER_SOURCE], { type: "text/javascript" }));
33
+ return new Worker(url);
34
+ } catch {
35
+ return null;
36
+ }
37
+ };
38
+ /**
39
+ * Encodes canvases as PNGs in a background worker, so the page keeps responding while large layers are saved. The
40
+ * canvas is copied (`createImageBitmap`) and sent to the worker, which encodes the copy. Where a worker can't be used,
41
+ * or it fails, it encodes on the page's own thread instead (see `encodePngHere`).
42
+ *
43
+ * @example
44
+ * const encoder = new PngEncoder();
45
+ * const png = await encoder.encode(layer.canvas);
46
+ */
47
+ var PngEncoder = class {
48
+ createWorker;
49
+ /** The worker, once made (null when it can't be used) */
50
+ worker;
51
+ nextId = 0;
52
+ /** Encodings waiting for the worker, by ID */
53
+ waiting = /* @__PURE__ */ new Map();
54
+ constructor(createWorker = createPngWorker) {
55
+ this.createWorker = createWorker;
56
+ }
57
+ /**
58
+ * Encode a canvas as a PNG, in the worker where it can be used.
59
+ */
60
+ encode = async (canvas) => {
61
+ const worker = this.getWorker();
62
+ if (!worker) return encodePngHere(canvas);
63
+ try {
64
+ canvas.getContext("2d");
65
+ const bitmap = await createImageBitmap(canvas);
66
+ const id = this.nextId++;
67
+ return await new Promise((resolve, reject) => {
68
+ this.waiting.set(id, {
69
+ resolve,
70
+ reject
71
+ });
72
+ worker.postMessage({
73
+ id,
74
+ bitmap
75
+ }, [bitmap]);
76
+ });
77
+ } catch {
78
+ return encodePngHere(canvas);
79
+ }
80
+ };
81
+ /** The worker, made on first use */
82
+ getWorker() {
83
+ if (this.worker !== void 0) return this.worker;
84
+ const worker = this.createWorker();
85
+ this.worker = worker;
86
+ if (!worker) return null;
87
+ worker.addEventListener("message", (event) => {
88
+ const { id, blob, error } = event.data;
89
+ const waiting = this.waiting.get(id);
90
+ if (!waiting) return;
91
+ this.waiting.delete(id);
92
+ if (blob) waiting.resolve(blob);
93
+ else waiting.reject(new Error(error));
94
+ });
95
+ worker.addEventListener("error", () => {
96
+ worker.terminate();
97
+ this.worker = null;
98
+ for (const { reject } of this.waiting.values()) reject(/* @__PURE__ */ new Error("PNG worker failed"));
99
+ this.waiting.clear();
100
+ });
101
+ return worker;
102
+ }
103
+ };
104
+ export { PNG_WORKER_SOURCE, createPngWorker, PngEncoder as default, encodePngHere };
@@ -0,0 +1,87 @@
1
+ import { type ProfileGraph, type ProfilePoint } from "../../profiles";
2
+ /**
3
+ * Keep only valid points from an untrusted value, in order from left to right: each on the grid, with a known
4
+ * interpolation. Undefined if there aren't 2 to `MAX_PROFILE_POINTS` of them.
5
+ *
6
+ * @example
7
+ * sanitisePoints([{ x: 10, y: 0, i: "linear" }, { x: 0, y: -10, i: "linear" }]); // sorted by x
8
+ */
9
+ export declare const sanitisePoints: (input: unknown) => ProfilePoint[] | undefined;
10
+ /**
11
+ * Keep only valid profile graphs from an untrusted value (e.g. stored or imported): each with an ID and name and valid
12
+ * points (see `sanitisePoints`), each ID once and none of the presets', up to `MAX_SAVED_PROFILES`. Undefined if the
13
+ * value isn't an array.
14
+ *
15
+ * @example
16
+ * sanitiseProfiles([{ id: "a", name: "Soft", points: [...] }, { id: "a", name: "Again", points: [...] }]); // the first
17
+ */
18
+ export declare const sanitiseProfiles: (input: unknown) => ProfileGraph[] | undefined;
19
+ /** What a profile graph is saved from: its name and points, and its ID when replacing one */
20
+ export interface ProfileGraphDraft {
21
+ id?: string;
22
+ name: string;
23
+ points: readonly ProfilePoint[];
24
+ }
25
+ /**
26
+ * The user's own profile graphs, alongside the ones PaintCanvas comes with (`PROFILE_PRESETS`). Kept in memory; the app
27
+ * settings save and restore them (see `set` and the `change` event). Each gets a UUID when it's first saved.
28
+ *
29
+ * Dispatches a `change` event when the user's graphs change.
30
+ *
31
+ * @example
32
+ * const profiles = new ProfileGraphs();
33
+ * const soft = profiles.save({ name: "Soft", points: PROFILE_PRESETS[1].points });
34
+ * profiles.get(soft.id)?.name; // "Soft"
35
+ */
36
+ export default class ProfileGraphs extends EventTarget {
37
+ private values;
38
+ /** The user's own graphs, in the order they were first saved */
39
+ get graphs(): readonly ProfileGraph[];
40
+ /** Every graph: the presets, then the user's own */
41
+ get all(): readonly ProfileGraph[];
42
+ /**
43
+ * A graph by its ID: a preset or one of the user's own.
44
+ *
45
+ * @example
46
+ * profiles.get("overweight")?.name; // "Overweight"
47
+ */
48
+ get(id: string | undefined): ProfileGraph | undefined;
49
+ /** Whether a graph is one PaintCanvas comes with (so it can't be renamed, replaced or deleted) */
50
+ isPreset(id: string | undefined): boolean;
51
+ /**
52
+ * One of the user's own graphs with a name, ignoring case and spaces around it.
53
+ *
54
+ * @example
55
+ * profiles.findByName(" soft "); // the graph called "Soft"
56
+ */
57
+ findByName(name: string): ProfileGraph | undefined;
58
+ /**
59
+ * Save a graph as one of the user's own: as a new one (with a new UUID), or in place of the one with its ID. Returns
60
+ * the saved graph, or undefined if it can't be saved (no name, invalid points, a preset's ID, or no room).
61
+ *
62
+ * @example
63
+ * profiles.save({ name: "Soft", points }); // { id: "1b9d…", name: "Soft", points }
64
+ */
65
+ save(draft: ProfileGraphDraft): ProfileGraph | undefined;
66
+ /**
67
+ * Rename one of the user's own graphs. Returns whether it was renamed.
68
+ *
69
+ * @example
70
+ * profiles.rename(id, "Softer");
71
+ */
72
+ rename(id: string, name: string): boolean;
73
+ /**
74
+ * Delete one of the user's own graphs. Returns whether it was deleted.
75
+ *
76
+ * @example
77
+ * profiles.remove(id);
78
+ */
79
+ remove(id: string): boolean;
80
+ /**
81
+ * Replace all the user's graphs (e.g. when restoring saved ones), keeping only valid ones (see `sanitiseProfiles`).
82
+ *
83
+ * @example
84
+ * profiles.set(settings.profiles);
85
+ */
86
+ set(graphs: unknown): void;
87
+ }
@@ -0,0 +1,148 @@
1
+ import { Interpolation, PROFILE_PRESETS } from "../../profiles/index.js";
2
+ import "../../profiles/schema.js";
3
+ // src/lib/classes/ProfileGraphs/index.ts
4
+ /** The interpolations a point can have */
5
+ var INTERPOLATIONS = Object.values(Interpolation);
6
+ /** Whether a value is text of a usable length for an ID or name */
7
+ var isText = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 100;
8
+ /**
9
+ * Keep only valid points from an untrusted value, in order from left to right: each on the grid, with a known
10
+ * interpolation. Undefined if there aren't 2 to `MAX_PROFILE_POINTS` of them.
11
+ *
12
+ * @example
13
+ * sanitisePoints([{ x: 10, y: 0, i: "linear" }, { x: 0, y: -10, i: "linear" }]); // sorted by x
14
+ */
15
+ var sanitisePoints = (input) => {
16
+ if (!Array.isArray(input)) return void 0;
17
+ const points = input.filter((point) => typeof point?.x === "number" && typeof point.y === "number" && point.x >= 0 && point.x <= 10 && point.y >= -10 && point.y <= 0 && INTERPOLATIONS.includes(point.i)).map(({ x, y, i }) => ({
18
+ x,
19
+ y,
20
+ i
21
+ })).sort((a, b) => a.x - b.x);
22
+ return points.length >= 2 && points.length <= 32 ? points : void 0;
23
+ };
24
+ /**
25
+ * Keep only valid profile graphs from an untrusted value (e.g. stored or imported): each with an ID and name and valid
26
+ * points (see `sanitisePoints`), each ID once and none of the presets', up to `MAX_SAVED_PROFILES`. Undefined if the
27
+ * value isn't an array.
28
+ *
29
+ * @example
30
+ * sanitiseProfiles([{ id: "a", name: "Soft", points: [...] }, { id: "a", name: "Again", points: [...] }]); // the first
31
+ */
32
+ var sanitiseProfiles = (input) => {
33
+ if (!Array.isArray(input)) return void 0;
34
+ const graphs = /* @__PURE__ */ new Map();
35
+ for (const graph of input) {
36
+ if (graphs.size >= 100) break;
37
+ const points = sanitisePoints(graph?.points);
38
+ if (!points || !isText(graph.id) || !isText(graph.name) || graphs.has(graph.id) || PROFILE_PRESETS.some((preset) => preset.id === graph.id)) continue;
39
+ graphs.set(graph.id, {
40
+ id: graph.id,
41
+ name: graph.name.trim(),
42
+ points
43
+ });
44
+ }
45
+ return [...graphs.values()];
46
+ };
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.
50
+ *
51
+ * Dispatches a `change` event when the user's graphs change.
52
+ *
53
+ * @example
54
+ * const profiles = new ProfileGraphs();
55
+ * const soft = profiles.save({ name: "Soft", points: PROFILE_PRESETS[1].points });
56
+ * profiles.get(soft.id)?.name; // "Soft"
57
+ */
58
+ var ProfileGraphs = class extends EventTarget {
59
+ values = [];
60
+ /** The user's own graphs, in the order they were first saved */
61
+ get graphs() {
62
+ return this.values;
63
+ }
64
+ /** Every graph: the presets, then the user's own */
65
+ get all() {
66
+ return [...PROFILE_PRESETS, ...this.values];
67
+ }
68
+ /**
69
+ * A graph by its ID: a preset or one of the user's own.
70
+ *
71
+ * @example
72
+ * profiles.get("overweight")?.name; // "Overweight"
73
+ */
74
+ get(id) {
75
+ return this.all.find((graph) => graph.id === id);
76
+ }
77
+ /** Whether a graph is one PaintCanvas comes with (so it can't be renamed, replaced or deleted) */
78
+ isPreset(id) {
79
+ return PROFILE_PRESETS.some((graph) => graph.id === id);
80
+ }
81
+ /**
82
+ * One of the user's own graphs with a name, ignoring case and spaces around it.
83
+ *
84
+ * @example
85
+ * profiles.findByName(" soft "); // the graph called "Soft"
86
+ */
87
+ findByName(name) {
88
+ const wanted = name.trim().toLowerCase();
89
+ return this.values.find((graph) => graph.name.toLowerCase() === wanted);
90
+ }
91
+ /**
92
+ * Save a graph as one of the user's own: as a new one (with a new UUID), or in place of the one with its ID. Returns
93
+ * the saved graph, or undefined if it can't be saved (no name, invalid points, a preset's ID, or no room).
94
+ *
95
+ * @example
96
+ * profiles.save({ name: "Soft", points }); // { id: "1b9d…", name: "Soft", points }
97
+ */
98
+ save(draft) {
99
+ const index = this.values.findIndex((graph) => graph.id === draft.id);
100
+ const [graph] = sanitiseProfiles([{
101
+ ...draft,
102
+ id: index >= 0 ? draft.id : crypto.randomUUID()
103
+ }]);
104
+ if (!graph || index < 0 && this.values.length >= 100) return void 0;
105
+ this.values = index >= 0 ? this.values.map((saved, i) => i === index ? graph : saved) : [...this.values, graph];
106
+ this.dispatchEvent(new Event("change"));
107
+ return graph;
108
+ }
109
+ /**
110
+ * Rename one of the user's own graphs. Returns whether it was renamed.
111
+ *
112
+ * @example
113
+ * profiles.rename(id, "Softer");
114
+ */
115
+ rename(id, name) {
116
+ const graph = this.values.find((saved) => saved.id === id);
117
+ return graph !== void 0 && this.save({
118
+ ...graph,
119
+ name
120
+ }) !== void 0;
121
+ }
122
+ /**
123
+ * Delete one of the user's own graphs. Returns whether it was deleted.
124
+ *
125
+ * @example
126
+ * profiles.remove(id);
127
+ */
128
+ remove(id) {
129
+ const next = this.values.filter((graph) => graph.id !== id);
130
+ if (next.length === this.values.length) return false;
131
+ this.values = next;
132
+ this.dispatchEvent(new Event("change"));
133
+ return true;
134
+ }
135
+ /**
136
+ * Replace all the user's graphs (e.g. when restoring saved ones), keeping only valid ones (see `sanitiseProfiles`).
137
+ *
138
+ * @example
139
+ * profiles.set(settings.profiles);
140
+ */
141
+ set(graphs) {
142
+ const next = sanitiseProfiles(graphs);
143
+ if (!next) return;
144
+ this.values = next;
145
+ this.dispatchEvent(new Event("change"));
146
+ }
147
+ };
148
+ export { ProfileGraphs as default, sanitisePoints, sanitiseProfiles };
@@ -10,7 +10,7 @@ export interface SettingsAutosaveOptions {
10
10
  delayMs?: number;
11
11
  }
12
12
  /**
13
- * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, theme and language) in a
13
+ * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, profile graphs, theme and language) in a
14
14
  * store: the saved settings are restored when it starts (invalid ones are ignored), then changes are saved soon after
15
15
  * they happen.
16
16
  *
@@ -5,7 +5,7 @@ var SETTINGS_SAVE_DELAY_MS = 500;
5
5
  /** Event dispatched when loading or saving settings fails (detail: the error) */
6
6
  var SETTINGS_AUTOSAVE_ERROR_EVENT = "error";
7
7
  /**
8
- * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, theme and language) in a
8
+ * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, profile graphs, theme and language) in a
9
9
  * store: the saved settings are restored when it starts (invalid ones are ignored), then changes are saved soon after
10
10
  * they happen.
11
11
  *
@@ -43,6 +43,7 @@ var SettingsAutosave = class extends EventTarget {
43
43
  app.input,
44
44
  app.shortcuts,
45
45
  app.palette,
46
+ app.profiles,
46
47
  app.ui
47
48
  ];
48
49
  }
@@ -1,6 +1,12 @@
1
1
  /** The themes: light, dark, or following the device's (system) */
2
2
  export declare const THEMES: readonly ["light", "dark", "system"];
3
3
  export type Theme = (typeof THEMES)[number];
4
+ /**
5
+ * Where canvases draw: `cpu` keeps their pixels in regular memory (reading and saving them never waits for the GPU),
6
+ * `gpu` asks the browser to keep them on the GPU (drawing may be quicker; the browser can still use the CPU)
7
+ */
8
+ export declare const RENDERINGS: readonly ["cpu", "gpu"];
9
+ export type Rendering = (typeof RENDERINGS)[number];
4
10
  /** Longest locale tag kept in UI settings (characters) */
5
11
  export declare const MAX_LOCALE_LENGTH = 35;
6
12
  /** A locale tag's characters: letters, digits and hyphens (e.g. `zh-Hant`) */
@@ -13,6 +19,14 @@ export declare const LOCALE_PATTERN = "^[A-Za-z0-9-]+$";
13
19
  * isTheme(undefined); // false
14
20
  */
15
21
  export declare const isTheme: (value: unknown) => value is Theme;
22
+ /**
23
+ * Whether a value is one of the renderings.
24
+ *
25
+ * @example
26
+ * isRendering("gpu"); // true
27
+ * isRendering("webgl"); // false
28
+ */
29
+ export declare const isRendering: (value: unknown) => value is Rendering;
16
30
  /**
17
31
  * Whether a value can be a locale tag (letters, digits and hyphens, at most `MAX_LOCALE_LENGTH` characters).
18
32
  *
@@ -27,7 +41,14 @@ export interface UiSettingsData {
27
41
  theme: Theme;
28
42
  /** The language's locale tag, e.g. `fr`, or undefined to follow the device's languages */
29
43
  locale?: string;
44
+ /**
45
+ * Where canvases draw (`cpu` by default). Browsers fix this when a canvas is made, so a change applies the next time
46
+ * the canvas starts (e.g. after reloading the page; see `PaintCanvasOptions.rendering`).
47
+ */
48
+ rendering: Rendering;
30
49
  }
50
+ /** UI settings used for anything not given */
51
+ export declare const DEFAULT_UI_SETTINGS: Readonly<UiSettingsData>;
31
52
  /** UI settings the UI can show and change: PaintCanvas's `UiSettings`, or anything with the same data and `update` */
32
53
  export interface UiSettingsLike extends EventTarget {
33
54
  readonly data: Readonly<UiSettingsData>;
@@ -38,11 +59,12 @@ export interface UiSettingsLike extends EventTarget {
38
59
  * or invalid. Other properties are left out.
39
60
  *
40
61
  * @example
41
- * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system" }); // { theme: "dark" }
62
+ * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system", rendering: "cpu" });
63
+ * // { theme: "dark", rendering: "cpu" }
42
64
  */
43
65
  export declare const sanitiseUiSettings: (input: unknown, defaults: UiSettingsData) => UiSettingsData;
44
66
  /**
45
- * The user's interface choices: the theme and the language. Kept in memory; the implementer saves and restores them
67
+ * The user's interface choices: the theme, the language and where canvases draw. Kept in memory; the implementer saves and restores them
46
68
  * with the other app settings (see `SettingsAutosave`), and the UI shows and applies them (e.g. `<pc-theme-picker>` and
47
69
  * `<pc-input-settings>`).
48
70
  *
@@ -5,6 +5,11 @@ var THEMES = [
5
5
  "dark",
6
6
  "system"
7
7
  ];
8
+ /**
9
+ * Where canvases draw: `cpu` keeps their pixels in regular memory (reading and saving them never waits for the GPU),
10
+ * `gpu` asks the browser to keep them on the GPU (drawing may be quicker; the browser can still use the CPU)
11
+ */
12
+ var RENDERINGS = ["cpu", "gpu"];
8
13
  /** Longest locale tag kept in UI settings (characters) */
9
14
  var MAX_LOCALE_LENGTH = 35;
10
15
  /** A locale tag's characters: letters, digits and hyphens (e.g. `zh-Hant`) */
@@ -18,6 +23,14 @@ var LOCALE_PATTERN = "^[A-Za-z0-9-]+$";
18
23
  */
19
24
  var isTheme = (value) => THEMES.includes(value);
20
25
  /**
26
+ * Whether a value is one of the renderings.
27
+ *
28
+ * @example
29
+ * isRendering("gpu"); // true
30
+ * isRendering("webgl"); // false
31
+ */
32
+ var isRendering = (value) => RENDERINGS.includes(value);
33
+ /**
21
34
  * Whether a value can be a locale tag (letters, digits and hyphens, at most `MAX_LOCALE_LENGTH` characters).
22
35
  *
23
36
  * @example
@@ -25,24 +38,35 @@ var isTheme = (value) => THEMES.includes(value);
25
38
  * isLocaleTag("<script>"); // false
26
39
  */
27
40
  var isLocaleTag = (value) => typeof value === "string" && value.length <= 35 && (/* @__PURE__ */ new RegExp("^[A-Za-z0-9-]+$")).test(value);
41
+ /** UI settings used for anything not given */
42
+ var DEFAULT_UI_SETTINGS = {
43
+ theme: "system",
44
+ rendering: "cpu"
45
+ };
28
46
  /**
29
47
  * Keep only valid UI settings from an untrusted value (e.g. stored or imported), using `defaults` for anything missing
30
48
  * or invalid. Other properties are left out.
31
49
  *
32
50
  * @example
33
- * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system" }); // { theme: "dark" }
51
+ * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system", rendering: "cpu" });
52
+ * // { theme: "dark", rendering: "cpu" }
34
53
  */
35
54
  var sanitiseUiSettings = (input, defaults) => {
36
55
  const value = typeof input === "object" && input !== null ? input : {};
37
56
  const theme = isTheme(value.theme) ? value.theme : defaults.theme;
38
57
  const locale = isLocaleTag(value.locale) ? value.locale : defaults.locale;
39
- return locale === void 0 ? { theme } : {
58
+ const rendering = isRendering(value.rendering) ? value.rendering : defaults.rendering;
59
+ return locale === void 0 ? {
60
+ theme,
61
+ rendering
62
+ } : {
40
63
  theme,
41
- locale
64
+ locale,
65
+ rendering
42
66
  };
43
67
  };
44
68
  /**
45
- * The user's interface choices: the theme and the language. Kept in memory; the implementer saves and restores them
69
+ * The user's interface choices: the theme, the language and where canvases draw. Kept in memory; the implementer saves and restores them
46
70
  * with the other app settings (see `SettingsAutosave`), and the UI shows and applies them (e.g. `<pc-theme-picker>` and
47
71
  * `<pc-input-settings>`).
48
72
  *
@@ -57,7 +81,7 @@ var UiSettings = class extends EventTarget {
57
81
  values;
58
82
  constructor(initial) {
59
83
  super();
60
- this.values = sanitiseUiSettings(initial, { theme: "system" });
84
+ this.values = sanitiseUiSettings(initial, DEFAULT_UI_SETTINGS);
61
85
  }
62
86
  /** Current settings (read-only copy) */
63
87
  get data() {
@@ -74,9 +98,10 @@ var UiSettings = class extends EventTarget {
74
98
  ...this.values,
75
99
  ...changes
76
100
  }, this.values);
77
- if (next.theme === this.values.theme && next.locale === this.values.locale) return;
101
+ const keys = Object.keys(next);
102
+ if (keys.length === Object.keys(this.values).length && keys.every((key) => next[key] === this.values[key])) return;
78
103
  this.values = next;
79
104
  this.dispatchEvent(new Event("change"));
80
105
  }
81
106
  };
82
- export { LOCALE_PATTERN, MAX_LOCALE_LENGTH, THEMES, UiSettings as default, isLocaleTag, isTheme, sanitiseUiSettings };
107
+ export { DEFAULT_UI_SETTINGS, LOCALE_PATTERN, MAX_LOCALE_LENGTH, RENDERINGS, THEMES, UiSettings as default, isLocaleTag, isRendering, isTheme, sanitiseUiSettings };