@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.
- package/dist/lib/brushes/configRanges/index.d.ts +17 -0
- package/dist/lib/brushes/configRanges/index.js +31 -1
- package/dist/lib/brushes/params/index.d.ts +2 -1
- package/dist/lib/brushes/params/index.js +7 -4
- package/dist/lib/brushes/types/index.d.ts +8 -0
- package/dist/lib/classes/Layer/index.d.ts +10 -0
- package/dist/lib/classes/Layer/index.js +15 -1
- package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
- package/dist/lib/classes/LayerAutosave/index.js +30 -43
- package/dist/lib/classes/LayerStack/index.d.ts +15 -0
- package/dist/lib/classes/LayerStack/index.js +26 -2
- package/dist/lib/classes/PaintCanvas/index.d.ts +26 -3
- package/dist/lib/classes/PaintCanvas/index.js +39 -12
- package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
- package/dist/lib/classes/PngEncoder/index.js +104 -0
- package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
- package/dist/lib/classes/ProfileGraphs/index.js +148 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
- package/dist/lib/classes/SettingsAutosave/index.js +2 -1
- package/dist/lib/messages/schema.d.ts +720 -0
- package/dist/lib/presets/schema.d.ts +360 -0
- package/dist/lib/presets/schema.js +8 -1
- package/dist/lib/profiles/index.d.ts +61 -0
- package/dist/lib/profiles/index.js +131 -0
- package/dist/lib/profiles/schema.d.ts +73 -0
- package/dist/lib/profiles/schema.js +53 -0
- package/dist/lib/rendering/sample/index.d.ts +11 -0
- package/dist/lib/rendering/sample/index.js +36 -5
- package/dist/lib/settings/index.d.ts +2 -1
- package/dist/lib/settings/index.js +4 -1
- package/dist/lib/settings/schema.d.ts +49 -0
- package/dist/lib/settings/schema.js +6 -0
- package/dist/lib/validation/validators.generated.js +13064 -3696
- package/dist/locales/en-GB.js +30 -0
- package/dist/locales/en-US.js +30 -0
- package/dist/locales/fr.js +30 -0
- package/dist/locales/ja.js +30 -0
- package/dist/locales/ko.js +30 -0
- package/dist/locales/zh-Hans.js +30 -0
- package/dist/locales/zh-Hant.js +30 -0
- package/dist/package.js +1 -1
- package/dist/ui/ask/index.d.ts +48 -0
- package/dist/ui/ask/index.js +67 -0
- package/dist/ui/base/index.d.ts +8 -1
- package/dist/ui/base/index.js +19 -4
- package/dist/ui/elements/PaintCanvas/index.js +3 -1
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
- package/dist/ui/elements/PcAppSettings/index.js +21 -65
- package/dist/ui/elements/PcBrushConfig/index.d.ts +25 -14
- package/dist/ui/elements/PcBrushConfig/index.js +216 -146
- package/dist/ui/elements/PcBrushConfig/style.js +1 -1
- package/dist/ui/elements/PcBrushTest/index.js +7 -2
- package/dist/ui/elements/PcBrushTest/style.js +1 -1
- package/dist/ui/elements/PcButton/index.d.ts +12 -3
- package/dist/ui/elements/PcButton/index.js +18 -10
- package/dist/ui/elements/PcCanvas/index.d.ts +6 -1
- package/dist/ui/elements/PcCanvas/index.js +10 -3
- package/dist/ui/elements/PcCanvas/style.js +1 -1
- package/dist/ui/elements/PcClipboardTools/index.d.ts +5 -4
- package/dist/ui/elements/PcClipboardTools/index.js +12 -5
- package/dist/ui/elements/PcDialog/index.d.ts +46 -10
- package/dist/ui/elements/PcDialog/index.js +102 -51
- package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
- package/dist/ui/elements/PcDialogWindow/index.js +385 -0
- package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
- package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
- package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
- package/dist/ui/elements/PcPanelColours/index.js +2 -2
- package/dist/ui/elements/PcPanelColours/style.js +1 -1
- package/dist/ui/elements/PcPanelLayers/index.js +1 -1
- package/dist/ui/elements/PcPanelLayers/style.js +1 -1
- package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
- package/dist/ui/elements/PcProfileEditor/index.js +259 -0
- package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
- package/dist/ui/elements/PcProfileGraph/index.js +317 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
- package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
- package/dist/ui/elements/PcSliderRange/index.js +307 -0
- package/dist/ui/elements/PcStats/index.d.ts +19 -4
- package/dist/ui/elements/PcStats/index.js +29 -13
- package/dist/ui/elements/PcToolSettings/index.js +6 -6
- package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
- package/dist/ui/icons/index.d.ts +1 -1
- package/dist/ui/icons/index.js +2 -0
- package/dist/ui/index.d.ts +6 -1
- package/dist/ui/index.js +6 -1
- package/dist/ui/styles/index.d.ts +21 -1
- package/dist/ui/styles/index.js +73 -14
- package/locales/en-GB.json +30 -0
- package/locales/en-US.json +30 -0
- package/locales/fr.json +30 -0
- package/locales/ja.json +30 -0
- package/locales/ko.json +30 -0
- package/locales/zh-Hans.json +30 -0
- package/locales/zh-Hant.json +30 -0
- package/package.json +25 -23
|
@@ -11,18 +11,19 @@ import Selection from "../../selection/Selection/index.js";
|
|
|
11
11
|
import Lasso from "../../selection/Lasso/index.js";
|
|
12
12
|
import Transformer, { applyMask, contentBounds, copyArea, drawTransformed } from "../../transform/Transformer/index.js";
|
|
13
13
|
import { boxBounds } from "../../transform/geometry/index.js";
|
|
14
|
-
import Layer, {
|
|
14
|
+
import Layer, { createMemoryCanvas } from "../Layer/index.js";
|
|
15
15
|
import LayerAutosave from "../LayerAutosave/index.js";
|
|
16
16
|
import { brushPresets, eraserPresets } from "../../brushes/index.js";
|
|
17
17
|
import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
|
|
18
18
|
import "../../presets/index.js";
|
|
19
|
-
import CustomPresets from "../CustomPresets/index.js";
|
|
20
19
|
import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
|
|
20
|
+
import CustomPresets from "../CustomPresets/index.js";
|
|
21
21
|
import InputSettings from "../InputSettings/index.js";
|
|
22
22
|
import UiSettings 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";
|
|
@@ -130,6 +131,8 @@ var PaintCanvas = class extends EventTarget {
|
|
|
130
131
|
ui = new UiSettings();
|
|
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) */
|
|
@@ -239,7 +242,7 @@ var PaintCanvas = class extends EventTarget {
|
|
|
239
242
|
this.customPresets = new CustomPresets(this.ownerId, options.presetStore);
|
|
240
243
|
this.customPresets.addEventListener("change", () => this.listCustomPresets());
|
|
241
244
|
this.customPresets.load().catch(() => void 0);
|
|
242
|
-
this.createCanvas = options.createCanvas ??
|
|
245
|
+
this.createCanvas = options.createCanvas ?? createMemoryCanvas;
|
|
243
246
|
this.systemClipboard = options.systemClipboard === null ? void 0 : options.systemClipboard ?? globalThis.navigator?.clipboard;
|
|
244
247
|
this.canvasWidth = DEFAULT_CANVAS_SIZE;
|
|
245
248
|
this.canvasHeight = DEFAULT_CANVAS_SIZE;
|
|
@@ -715,11 +718,31 @@ var PaintCanvas = class extends EventTarget {
|
|
|
715
718
|
* nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
716
719
|
*/
|
|
717
720
|
copy() {
|
|
718
|
-
|
|
721
|
+
return this.copyMasked(this.layerStack.active.canvas);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Copy the masked image data of all the shown layers, mixed as they're shown (each layer's opacity and blend mode,
|
|
725
|
+
* without the white background), to the clipboard. Hidden layers are left out. Like `copy`, it needs a mask with image
|
|
726
|
+
* data inside it, and does nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
727
|
+
*
|
|
728
|
+
* @example
|
|
729
|
+
* app.selection.selectAll();
|
|
730
|
+
* app.copyMerged(); // { x: 10, y: 20, width: 30, height: 5 }: the part of the canvas with paint on any shown layer
|
|
731
|
+
*/
|
|
732
|
+
copyMerged() {
|
|
733
|
+
if (!this.canCopy) return void 0;
|
|
734
|
+
const merged = this.createCanvas(this.width, this.height);
|
|
735
|
+
drawLayers(merged.getContext("2d"), this.layerStack.layers);
|
|
736
|
+
return this.copyMasked(merged);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Copy the masked part of a canvas-sized image to the clipboard (see `copy`), returning the area copied, if any.
|
|
740
|
+
*/
|
|
741
|
+
copyMasked(source) {
|
|
742
|
+
const area = this.maskedArea(source);
|
|
719
743
|
const mask = this.currentSelection.mask;
|
|
720
744
|
if (!area || !mask) return void 0;
|
|
721
|
-
const
|
|
722
|
-
const image = copyArea(this.createCanvas, layer.canvas, area);
|
|
745
|
+
const image = copyArea(this.createCanvas, source, area);
|
|
723
746
|
applyMask(image, copyArea(this.createCanvas, mask, area));
|
|
724
747
|
this.clipboard = {
|
|
725
748
|
image,
|
|
@@ -807,15 +830,19 @@ var PaintCanvas = class extends EventTarget {
|
|
|
807
830
|
height
|
|
808
831
|
};
|
|
809
832
|
}
|
|
833
|
+
/** Whether image data can be copied now: not during a stroke or while a transform is pending */
|
|
834
|
+
get canCopy() {
|
|
835
|
+
return !this.strokeEngine.drawing && !this.transformer.pending;
|
|
836
|
+
}
|
|
810
837
|
/**
|
|
811
|
-
* The bounds of the selected layer's
|
|
812
|
-
* during a stroke or while a transform is pending.
|
|
838
|
+
* The bounds of a canvas-sized image's data (the selected layer's by default) inside the mask, if there is a mask
|
|
839
|
+
* with image data inside it. None during a stroke or while a transform is pending.
|
|
813
840
|
*/
|
|
814
|
-
maskedArea() {
|
|
841
|
+
maskedArea(source = this.layerStack.active.canvas) {
|
|
815
842
|
const mask = this.currentSelection.mask;
|
|
816
|
-
if (!mask || this.
|
|
843
|
+
if (!mask || !this.canCopy) return void 0;
|
|
817
844
|
const { width, height } = this;
|
|
818
|
-
return contentBounds(
|
|
845
|
+
return contentBounds(source.getContext("2d").getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
|
|
819
846
|
}
|
|
820
847
|
/**
|
|
821
848
|
* 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
|
}
|