@lvl99dotcom/paintcanvas 0.3.1 → 0.4.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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
  3. package/dist/lib/brushes/StrokeEngine/index.js +26 -28
  4. package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
  5. package/dist/lib/classes/BrushSettings/index.js +35 -1
  6. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
  7. package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
  8. package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
  9. package/dist/lib/classes/PaintCanvas/index.js +141 -41
  10. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  11. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  12. package/dist/lib/classes/Palette/index.d.ts +58 -0
  13. package/dist/lib/classes/Palette/index.js +92 -0
  14. package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
  15. package/dist/lib/classes/SettingsAutosave/index.js +96 -0
  16. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  17. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  18. package/dist/lib/palette/index.d.ts +49 -0
  19. package/dist/lib/palette/index.js +82 -0
  20. package/dist/lib/palette/schema.d.ts +45 -0
  21. package/dist/lib/palette/schema.js +41 -0
  22. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  23. package/dist/lib/rendering/cursors/index.js +3 -1
  24. package/dist/lib/rendering/sample/index.js +3 -2
  25. package/dist/lib/settings/index.d.ts +63 -0
  26. package/dist/lib/settings/index.js +91 -0
  27. package/dist/lib/settings/schema.d.ts +150 -0
  28. package/dist/lib/settings/schema.js +102 -0
  29. package/dist/lib/storage/index.d.ts +36 -5
  30. package/dist/lib/storage/index.js +87 -29
  31. package/dist/lib/utils/colour/index.d.ts +72 -0
  32. package/dist/lib/utils/colour/index.js +154 -0
  33. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  34. package/dist/lib/utils/shortcuts/index.js +391 -145
  35. package/dist/lib/validation/validators.generated.d.ts +5 -0
  36. package/dist/lib/validation/validators.generated.js +872 -1
  37. package/dist/locales/en-GB.js +87 -1
  38. package/dist/locales/en-US.js +87 -1
  39. package/dist/locales/fr.js +87 -1
  40. package/dist/locales/ja.js +87 -1
  41. package/dist/locales/ko.js +87 -1
  42. package/dist/locales/zh-Hans.js +87 -1
  43. package/dist/locales/zh-Hant.js +87 -1
  44. package/dist/package.js +1 -1
  45. package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
  46. package/dist/ui/elements/PaintCanvas/index.js +121 -171
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  49. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  50. package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
  51. package/dist/ui/elements/PcCanvas/index.js +7 -5
  52. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  53. package/dist/ui/elements/PcColourPair/index.js +125 -0
  54. package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
  55. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  56. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  57. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  58. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  59. package/dist/ui/elements/PcMenu/index.d.ts +93 -0
  60. package/dist/ui/elements/PcMenu/index.js +204 -0
  61. package/dist/ui/elements/PcPane/index.d.ts +118 -0
  62. package/dist/ui/elements/PcPane/index.js +357 -0
  63. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  64. package/dist/ui/elements/PcPanel/index.js +76 -8
  65. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
  66. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +18 -14
  67. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  68. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  69. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  70. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  71. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  72. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  73. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  74. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  75. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  76. package/dist/ui/elements/PcToolSettings/index.js +2 -1
  77. package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
  78. package/dist/ui/elements/PcToolbar/index.js +77 -56
  79. package/dist/ui/elements/PcToolbar/style.js +1 -1
  80. package/dist/ui/index.d.ts +7 -1
  81. package/dist/ui/index.js +8 -2
  82. package/dist/ui/tokens/index.js +3 -1
  83. package/locales/en-GB.json +87 -1
  84. package/locales/en-US.json +87 -1
  85. package/locales/fr.json +87 -1
  86. package/locales/ja.json +87 -1
  87. package/locales/ko.json +87 -1
  88. package/locales/zh-Hans.json +87 -1
  89. package/locales/zh-Hant.json +87 -1
  90. package/package.json +1 -1
  91. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  92. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
@@ -0,0 +1,63 @@
1
+ import { type KeyCombo, type Shortcut, type ShortcutBindings } from "../../utils/shortcuts";
2
+ /** What happened when setting an action's key combination */
3
+ export interface ShortcutChangeResult {
4
+ /** Whether the key combination was set */
5
+ changed: boolean;
6
+ /** The action that had the key combination and lost it, if any */
7
+ movedFrom?: string;
8
+ /** The fixed action that uses the key combination, so it wasn't set */
9
+ fixed?: string;
10
+ }
11
+ /**
12
+ * The user's keyboard shortcuts: the key combination of each configurable action (see `CONFIGURABLE_SHORTCUTS`), each
13
+ * used by one action at most. Kept in memory; the implementer saves and restores them (see `load` and the `change`
14
+ * event).
15
+ *
16
+ * Dispatches a `change` event when a key combination changes.
17
+ *
18
+ * @example
19
+ * const shortcuts = new ShortcutSettings();
20
+ * shortcuts.set("tool-brush", { key: "p" });
21
+ * shortcuts.combo("tool-brush"); // { key: "p" }
22
+ */
23
+ export default class ShortcutSettings extends EventTarget {
24
+ private values;
25
+ private list;
26
+ constructor(initial?: unknown);
27
+ /** Each configurable action's key combination, by ID (read-only copy) */
28
+ get bindings(): Readonly<ShortcutBindings>;
29
+ /** The shortcuts key presses are matched against (except the hold-to-pan key) */
30
+ get shortcuts(): readonly Shortcut[];
31
+ /** The key held to pan, returning to the last tool when released (null when it has none) */
32
+ get holdToPan(): KeyCombo | null;
33
+ /**
34
+ * A configurable action's key combination (null when it has none or there's no such action).
35
+ *
36
+ * @example
37
+ * shortcuts.combo("deselect"); // { key: "d", cmd: true }
38
+ */
39
+ combo(id: string): KeyCombo | null;
40
+ /**
41
+ * Set or remove (null) a configurable action's key combination. If another configurable action has it, that action
42
+ * loses it (`movedFrom`). Nothing changes for a key combination a fixed action uses (`fixed`), or an unknown action.
43
+ *
44
+ * @example
45
+ * shortcuts.set("tool-brush", { key: "e" }); // { changed: true, movedFrom: "tool-eraser" }
46
+ * shortcuts.set("deselect", { key: "z", cmd: true }); // { changed: false, fixed: "undo" }
47
+ */
48
+ set(id: string, combo: KeyCombo | null): ShortcutChangeResult;
49
+ /**
50
+ * Go back to the default key combinations.
51
+ */
52
+ reset(): void;
53
+ /**
54
+ * Replace the key combinations, e.g. with saved or imported ones (see `sanitiseShortcutBindings`; actions left out
55
+ * get their defaults), and dispatch `change` if anything changed.
56
+ *
57
+ * @example
58
+ * shortcuts.load({ "tool-brush": { key: "p" } });
59
+ */
60
+ load(input: unknown): void;
61
+ /** Use new key combinations and tell listeners */
62
+ private apply;
63
+ }
@@ -0,0 +1,110 @@
1
+ import { CONFIGURABLE_SHORTCUTS, defaultShortcutBindings, fixedShortcutFor, normaliseCombo, sameCombo, sanitiseShortcutBindings, shortcutsFor } from "../../utils/shortcuts/index.js";
2
+ // src/lib/classes/ShortcutSettings/index.ts
3
+ /**
4
+ * The user's keyboard shortcuts: the key combination of each configurable action (see `CONFIGURABLE_SHORTCUTS`), each
5
+ * used by one action at most. Kept in memory; the implementer saves and restores them (see `load` and the `change`
6
+ * event).
7
+ *
8
+ * Dispatches a `change` event when a key combination changes.
9
+ *
10
+ * @example
11
+ * const shortcuts = new ShortcutSettings();
12
+ * shortcuts.set("tool-brush", { key: "p" });
13
+ * shortcuts.combo("tool-brush"); // { key: "p" }
14
+ */
15
+ var ShortcutSettings = class extends EventTarget {
16
+ values;
17
+ list;
18
+ constructor(initial) {
19
+ super();
20
+ this.values = sanitiseShortcutBindings(initial, defaultShortcutBindings());
21
+ this.list = shortcutsFor(this.values);
22
+ }
23
+ /** Each configurable action's key combination, by ID (read-only copy) */
24
+ get bindings() {
25
+ return this.values;
26
+ }
27
+ /** The shortcuts key presses are matched against (except the hold-to-pan key) */
28
+ get shortcuts() {
29
+ return this.list;
30
+ }
31
+ /** The key held to pan, returning to the last tool when released (null when it has none) */
32
+ get holdToPan() {
33
+ return this.values["pan"] ?? null;
34
+ }
35
+ /**
36
+ * A configurable action's key combination (null when it has none or there's no such action).
37
+ *
38
+ * @example
39
+ * shortcuts.combo("deselect"); // { key: "d", cmd: true }
40
+ */
41
+ combo(id) {
42
+ return this.values[id] ?? null;
43
+ }
44
+ /**
45
+ * Set or remove (null) a configurable action's key combination. If another configurable action has it, that action
46
+ * loses it (`movedFrom`). Nothing changes for a key combination a fixed action uses (`fixed`), or an unknown action.
47
+ *
48
+ * @example
49
+ * shortcuts.set("tool-brush", { key: "e" }); // { changed: true, movedFrom: "tool-eraser" }
50
+ * shortcuts.set("deselect", { key: "z", cmd: true }); // { changed: false, fixed: "undo" }
51
+ */
52
+ set(id, combo) {
53
+ if (!CONFIGURABLE_SHORTCUTS.some((definition) => definition.id === id)) return { changed: false };
54
+ const next = combo && normaliseCombo(combo);
55
+ const current = this.values[id] ?? null;
56
+ if (next ? current && sameCombo(current, next) : !current) return { changed: false };
57
+ if (!next) {
58
+ this.apply({
59
+ ...this.values,
60
+ [id]: null
61
+ });
62
+ return { changed: true };
63
+ }
64
+ const fixed = fixedShortcutFor(next);
65
+ if (fixed) return {
66
+ changed: false,
67
+ fixed: fixed.id
68
+ };
69
+ const movedFrom = Object.keys(this.values).find((other) => {
70
+ const otherCombo = this.values[other];
71
+ return other !== id && otherCombo && sameCombo(otherCombo, next);
72
+ });
73
+ this.apply({
74
+ ...this.values,
75
+ ...movedFrom && { [movedFrom]: null },
76
+ [id]: next
77
+ });
78
+ return movedFrom ? {
79
+ changed: true,
80
+ movedFrom
81
+ } : { changed: true };
82
+ }
83
+ /**
84
+ * Go back to the default key combinations.
85
+ */
86
+ reset() {
87
+ this.load(defaultShortcutBindings());
88
+ }
89
+ /**
90
+ * Replace the key combinations, e.g. with saved or imported ones (see `sanitiseShortcutBindings`; actions left out
91
+ * get their defaults), and dispatch `change` if anything changed.
92
+ *
93
+ * @example
94
+ * shortcuts.load({ "tool-brush": { key: "p" } });
95
+ */
96
+ load(input) {
97
+ const next = sanitiseShortcutBindings(input, defaultShortcutBindings());
98
+ if (Object.keys(next).some((id) => {
99
+ const [a, b] = [next[id], this.values[id]];
100
+ return a && b ? !sameCombo(a, b) : a !== b;
101
+ })) this.apply(next);
102
+ }
103
+ /** Use new key combinations and tell listeners */
104
+ apply(bindings) {
105
+ this.values = bindings;
106
+ this.list = shortcutsFor(bindings);
107
+ this.dispatchEvent(new Event("change"));
108
+ }
109
+ };
110
+ export { ShortcutSettings as default };
@@ -0,0 +1,49 @@
1
+ import { type PaletteFile } from "./schema";
2
+ export * from "./schema";
3
+ /** The colours a palette starts with */
4
+ export declare const DEFAULT_PALETTE: readonly string[];
5
+ /** Messages shown when importing or exporting a palette fails */
6
+ export declare const PALETTE_FILE_ERRORS: {
7
+ readonly incompatible: "Colour palette JSON file not compatible";
8
+ readonly read: "Failed to read colour palette JSON file";
9
+ };
10
+ /** Importing a palette failed, with a message for the user */
11
+ export declare class PaletteFileError extends Error {
12
+ }
13
+ /** Result of checking a palette file */
14
+ export type PaletteValidationResult = {
15
+ valid: true;
16
+ value: PaletteFile;
17
+ } | {
18
+ valid: false;
19
+ errors: string[];
20
+ };
21
+ /**
22
+ * Check a colour palette file against `paletteFileSchema`.
23
+ *
24
+ * @example
25
+ * isValidPaletteFile({ format: "paintcanvas-palette", version: 1, colours: ["#ff0000"] }).valid; // true
26
+ */
27
+ export declare const isValidPaletteFile: (input: unknown) => PaletteValidationResult;
28
+ /**
29
+ * The file name a palette is exported to on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
30
+ *
31
+ * @example
32
+ * paletteFileName(new Date(2026, 10, 4)); // "paintcanvas-palette-2026-11-04.json"
33
+ */
34
+ export declare const paletteFileName: (date?: Date, prefix?: string) => string;
35
+ /**
36
+ * Write colours to a colour palette file (JSON) of a format (see `PaintCanvasNames.paletteFileFormat`).
37
+ *
38
+ * @example
39
+ * exportPaletteFile(["#ff0000"]); // '{ "format": "paintcanvas-palette", "version": 1, "colours": ["#ff0000"] }'
40
+ */
41
+ export declare const exportPaletteFile: (colours: readonly string[], format?: string) => string;
42
+ /**
43
+ * Read the colours (as lowercase `#rrggbb`) in a colour palette file of one of the given formats (PaintCanvas's by
44
+ * default). Throws a `PaletteFileError` if it isn't valid JSON or a compatible file.
45
+ *
46
+ * @example
47
+ * parsePaletteFile('{ "format": "paintcanvas-palette", "version": 1, "colours": ["#FF0000"] }'); // ["#ff0000"]
48
+ */
49
+ export declare const parsePaletteFile: (text: string, formats?: readonly string[]) => string[];
@@ -0,0 +1,82 @@
1
+ import { formatErrors } from "../presets/index.js";
2
+ import { validatePaletteFile } from "../validation/validators.generated.js";
3
+ import { HEX_COLOUR_PATTERN, MAX_PALETTE_COLOURS, MAX_PALETTE_FILE_FORMAT_LENGTH, PALETTE_FILE_FORMAT, PALETTE_FILE_FORMATS, PALETTE_FILE_VERSION, paletteFileSchema } from "./schema.js";
4
+ // src/lib/palette/index.ts
5
+ /** The colours a palette starts with */
6
+ var DEFAULT_PALETTE = [
7
+ "#2c6bff",
8
+ "#8f3de8",
9
+ "#f40f95",
10
+ "#fb234a",
11
+ "#fe7e07",
12
+ "#ffd52c",
13
+ "#6dc905",
14
+ "#21e887",
15
+ "#13c3f4",
16
+ "#000000",
17
+ "#333333",
18
+ "#666666",
19
+ "#999999",
20
+ "#cccccc",
21
+ "#ffffff"
22
+ ];
23
+ /** Messages shown when importing or exporting a palette fails */
24
+ var PALETTE_FILE_ERRORS = {
25
+ incompatible: "Colour palette JSON file not compatible",
26
+ read: "Failed to read colour palette JSON file"
27
+ };
28
+ /** Importing a palette failed, with a message for the user */
29
+ var PaletteFileError = class extends Error {};
30
+ /**
31
+ * Check a colour palette file against `paletteFileSchema`.
32
+ *
33
+ * @example
34
+ * isValidPaletteFile({ format: "paintcanvas-palette", version: 1, colours: ["#ff0000"] }).valid; // true
35
+ */
36
+ var isValidPaletteFile = (input) => validatePaletteFile(input) ? {
37
+ valid: true,
38
+ value: input
39
+ } : {
40
+ valid: false,
41
+ errors: formatErrors(validatePaletteFile.errors)
42
+ };
43
+ /**
44
+ * The file name a palette is exported to on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
45
+ *
46
+ * @example
47
+ * paletteFileName(new Date(2026, 10, 4)); // "paintcanvas-palette-2026-11-04.json"
48
+ */
49
+ var paletteFileName = (date = /* @__PURE__ */ new Date(), prefix = "paintcanvas") => {
50
+ const pad = (n) => String(n).padStart(2, "0");
51
+ return `${prefix}-palette-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}.json`;
52
+ };
53
+ /**
54
+ * Write colours to a colour palette file (JSON) of a format (see `PaintCanvasNames.paletteFileFormat`).
55
+ *
56
+ * @example
57
+ * exportPaletteFile(["#ff0000"]); // '{ "format": "paintcanvas-palette", "version": 1, "colours": ["#ff0000"] }'
58
+ */
59
+ var exportPaletteFile = (colours, format = PALETTE_FILE_FORMAT) => JSON.stringify({
60
+ format,
61
+ version: 1,
62
+ colours
63
+ }, null, 2);
64
+ /**
65
+ * Read the colours (as lowercase `#rrggbb`) in a colour palette file of one of the given formats (PaintCanvas's by
66
+ * default). Throws a `PaletteFileError` if it isn't valid JSON or a compatible file.
67
+ *
68
+ * @example
69
+ * parsePaletteFile('{ "format": "paintcanvas-palette", "version": 1, "colours": ["#FF0000"] }'); // ["#ff0000"]
70
+ */
71
+ var parsePaletteFile = (text, formats = PALETTE_FILE_FORMATS) => {
72
+ let data;
73
+ try {
74
+ data = JSON.parse(text);
75
+ } catch {
76
+ throw new PaletteFileError(PALETTE_FILE_ERRORS.incompatible);
77
+ }
78
+ const result = isValidPaletteFile(data);
79
+ if (!result.valid || !formats.includes(result.value.format)) throw new PaletteFileError(PALETTE_FILE_ERRORS.incompatible);
80
+ return result.value.colours.map((colour) => colour.toLowerCase());
81
+ };
82
+ export { DEFAULT_PALETTE, HEX_COLOUR_PATTERN, MAX_PALETTE_COLOURS, MAX_PALETTE_FILE_FORMAT_LENGTH, PALETTE_FILE_ERRORS, PALETTE_FILE_FORMAT, PALETTE_FILE_FORMATS, PALETTE_FILE_VERSION, PaletteFileError, exportPaletteFile, isValidPaletteFile, paletteFileName, paletteFileSchema, parsePaletteFile };
@@ -0,0 +1,45 @@
1
+ /** The `format` of a colour palette file, unless the implementer names their own (see `PaintCanvasNames`) */
2
+ export declare const PALETTE_FILE_FORMAT = "paintcanvas-palette";
3
+ /** Formats of colour palette files that are imported by default */
4
+ export declare const PALETTE_FILE_FORMATS: readonly string[];
5
+ /** The version of the colour palette file format */
6
+ export declare const PALETTE_FILE_VERSION = 1;
7
+ /** Most colours in a palette (and in one file) */
8
+ export declare const MAX_PALETTE_COLOURS = 1000;
9
+ /** Longest palette file format name (characters) */
10
+ export declare const MAX_PALETTE_FILE_FORMAT_LENGTH = 100;
11
+ /** Matches a `#rrggbb` colour in any case */
12
+ export declare const HEX_COLOUR_PATTERN = "^#[0-9a-fA-F]{6}$";
13
+ /** A colour palette file: what users export and import */
14
+ export interface PaletteFile {
15
+ /** Names the app or kind of file, e.g. `PALETTE_FILE_FORMAT` */
16
+ format: string;
17
+ version: typeof PALETTE_FILE_VERSION;
18
+ /** Colours as `#rrggbb`, in palette order */
19
+ colours: string[];
20
+ }
21
+ /** Schema of a colour palette file */
22
+ export declare const paletteFileSchema: {
23
+ readonly $id: "paintcanvas/palette-file";
24
+ readonly type: "object";
25
+ readonly additionalProperties: false;
26
+ readonly required: readonly ["format", "version", "colours"];
27
+ readonly properties: {
28
+ readonly format: {
29
+ readonly type: "string";
30
+ readonly minLength: 1;
31
+ readonly maxLength: 100;
32
+ };
33
+ readonly version: {
34
+ readonly const: 1;
35
+ };
36
+ readonly colours: {
37
+ readonly type: "array";
38
+ readonly maxItems: 1000;
39
+ readonly items: {
40
+ readonly type: "string";
41
+ readonly pattern: "^#[0-9a-fA-F]{6}$";
42
+ };
43
+ };
44
+ };
45
+ };
@@ -0,0 +1,41 @@
1
+ // src/lib/palette/schema.ts
2
+ /** The `format` of a colour palette file, unless the implementer names their own (see `PaintCanvasNames`) */
3
+ var PALETTE_FILE_FORMAT = "paintcanvas-palette";
4
+ /** Formats of colour palette files that are imported by default */
5
+ var PALETTE_FILE_FORMATS = [PALETTE_FILE_FORMAT];
6
+ /** The version of the colour palette file format */
7
+ var PALETTE_FILE_VERSION = 1;
8
+ /** Most colours in a palette (and in one file) */
9
+ var MAX_PALETTE_COLOURS = 1e3;
10
+ /** Longest palette file format name (characters) */
11
+ var MAX_PALETTE_FILE_FORMAT_LENGTH = 100;
12
+ /** Matches a `#rrggbb` colour in any case */
13
+ var HEX_COLOUR_PATTERN = "^#[0-9a-fA-F]{6}$";
14
+ /** Schema of a colour palette file */
15
+ var paletteFileSchema = {
16
+ $id: "paintcanvas/palette-file",
17
+ type: "object",
18
+ additionalProperties: false,
19
+ required: [
20
+ "format",
21
+ "version",
22
+ "colours"
23
+ ],
24
+ properties: {
25
+ format: {
26
+ type: "string",
27
+ minLength: 1,
28
+ maxLength: 100
29
+ },
30
+ version: { const: 1 },
31
+ colours: {
32
+ type: "array",
33
+ maxItems: MAX_PALETTE_COLOURS,
34
+ items: {
35
+ type: "string",
36
+ pattern: HEX_COLOUR_PATTERN
37
+ }
38
+ }
39
+ }
40
+ };
41
+ export { HEX_COLOUR_PATTERN, MAX_PALETTE_COLOURS, MAX_PALETTE_FILE_FORMAT_LENGTH, PALETTE_FILE_FORMAT, PALETTE_FILE_FORMATS, PALETTE_FILE_VERSION, paletteFileSchema };
@@ -8,6 +8,8 @@
8
8
  * svgCursor("M4 4 L20 20", [4, 4], "crosshair"); // 'url("data:image/svg+xml,...") 4 4, crosshair'
9
9
  */
10
10
  export declare const svgCursor: (path: string, hotspot: [number, number], fallback: string) => string;
11
+ /** Deleting: an "X", e.g. over a brush preset or saved colour that a click deletes (Alt/Option held); the point is its middle */
12
+ export declare const DELETE_CURSOR: string;
11
13
  /** Colour sample (eyedropper): a pipette pointing down to the left; the point is at its tip */
12
14
  export declare const EYEDROPPER_CURSOR: string;
13
15
  /** Polygon lasso: a straight-edged loop with a tail; the point is at the tail's end */
@@ -12,6 +12,8 @@ var svgCursor = (path, hotspot, fallback) => {
12
12
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="${path}" stroke="#ffffff" stroke-width="3.5"/><path d="${path}" stroke="#000000" stroke-width="1.5"/></svg>`;
13
13
  return `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${hotspot[0]} ${hotspot[1]}, ${fallback}`;
14
14
  };
15
+ /** Deleting: an "X", e.g. over a brush preset or saved colour that a click deletes (Alt/Option held); the point is its middle */
16
+ var DELETE_CURSOR = svgCursor("M6 6 L18 18 M18 6 L6 18", [12, 12], "not-allowed");
15
17
  /** Colour sample (eyedropper): a pipette pointing down to the left; the point is at its tip */
16
18
  var EYEDROPPER_CURSOR = svgCursor("M2 22 L3 18 L12 9 L15 12 L6 21 Z M10 7 L17 14 M14 10 L18 6 C19 5 20 4 21 5 C22 6 21 7 20 8 L16 12", [2, 22], "crosshair");
17
19
  /** Polygon lasso: a straight-edged loop with a tail; the point is at the tail's end */
@@ -20,4 +22,4 @@ var POLYGON_LASSO_CURSOR = svgCursor("M3 21 L7 15 M7 15 L5 6 L13 2 L21 7 L17 15
20
22
  var FREEFORM_LASSO_CURSOR = svgCursor("M3 21 C3 19 5 18 7 17 C7 17 5 16 4 14 C2 10 4 6 8 5 C11 4 12 6 15 4 C18 3 22 6 20 10 C19 13 21 15 18 16 C15 17 11 15 9 16 C8 16.5 7.5 17 7 17", [3, 21], "crosshair");
21
23
  /** Rotate: the browser's grab cursor, as browsers have no standard rotate cursor */
22
24
  var ROTATE_CURSOR = "grab";
23
- export { EYEDROPPER_CURSOR, FREEFORM_LASSO_CURSOR, POLYGON_LASSO_CURSOR, ROTATE_CURSOR, svgCursor };
25
+ export { DELETE_CURSOR, EYEDROPPER_CURSOR, FREEFORM_LASSO_CURSOR, POLYGON_LASSO_CURSOR, ROTATE_CURSOR, svgCursor };
@@ -1,3 +1,4 @@
1
+ import { rgbToHex } from "../../utils/colour/index.js";
1
2
  import { CANVAS_BACKGROUND, drawLayers } from "../compositing/index.js";
2
3
  // src/lib/rendering/sample/index.ts
3
4
  /** How many canvas pixels across the sampling window enlarges */
@@ -12,11 +13,11 @@ var PREVIEW_MAX_PIXELS = 100;
12
13
  * @example
13
14
  * toHex(255, 0, 128); // "#ff0080"
14
15
  */
15
- var toHex = (r, g, b) => `#${[
16
+ var toHex = (r, g, b) => rgbToHex({
16
17
  r,
17
18
  g,
18
19
  b
19
- ].map((c) => Math.round(c).toString(16).padStart(2, "0")).join("")}`;
20
+ });
20
21
  /**
21
22
  * The average colour of RGBA pixels, counting every pixel that isn't fully transparent (transparent pixels are outside
22
23
  * the canvas). Undefined if all are transparent.
@@ -0,0 +1,63 @@
1
+ import type PaintCanvas from "../classes/PaintCanvas";
2
+ import { type AppSettings } from "./schema";
3
+ export * from "./schema";
4
+ /** Messages shown when importing settings fails */
5
+ export declare const SETTINGS_FILE_ERRORS: {
6
+ readonly incompatible: "Settings JSON file not compatible";
7
+ readonly read: "Failed to read settings JSON file";
8
+ };
9
+ /** Importing settings failed, with a message for the user */
10
+ export declare class SettingsFileError extends Error {
11
+ }
12
+ /** Result of checking settings */
13
+ export type SettingsValidationResult = {
14
+ valid: true;
15
+ value: AppSettings;
16
+ } | {
17
+ valid: false;
18
+ errors: string[];
19
+ };
20
+ /**
21
+ * Check app settings (e.g. kept in the browser) against `settingsSchema`.
22
+ *
23
+ * @example
24
+ * isValidSettings({ palette: ["#ff0000"] }).valid; // true
25
+ */
26
+ export declare const isValidSettings: (input: unknown) => SettingsValidationResult;
27
+ /**
28
+ * The app settings of a canvas: its input settings, keyboard shortcuts, colour palette and colours.
29
+ *
30
+ * @example
31
+ * collectSettings(canvas).palette; // ["#2c6bff", ...]
32
+ */
33
+ export declare const collectSettings: (app: PaintCanvas) => AppSettings;
34
+ /**
35
+ * Use app settings on a canvas, e.g. ones kept in the browser or imported. Parts left out stay as they are; invalid
36
+ * values within a part are left out (see each part's sanitising).
37
+ *
38
+ * @example
39
+ * applySettings(canvas, { colours: { foreground: "#ff0000", background: "#ffffff" } });
40
+ */
41
+ export declare const applySettings: (app: PaintCanvas, settings: AppSettings) => void;
42
+ /**
43
+ * The file name settings are exported to on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
44
+ *
45
+ * @example
46
+ * settingsFileName(new Date(2026, 10, 4)); // "paintcanvas-settings-2026-11-04.json"
47
+ */
48
+ export declare const settingsFileName: (date?: Date, prefix?: string) => string;
49
+ /**
50
+ * Write app settings to a settings file (JSON) of a format (see `PaintCanvasNames.settingsFileFormat`).
51
+ *
52
+ * @example
53
+ * exportSettingsFile(collectSettings(canvas)); // '{ "format": "paintcanvas-settings", "version": 1, "settings": {...} }'
54
+ */
55
+ export declare const exportSettingsFile: (settings: AppSettings, format?: string) => string;
56
+ /**
57
+ * Read the app settings in a settings file of one of the given formats (PaintCanvas's by default). Throws a
58
+ * `SettingsFileError` if it isn't valid JSON or a compatible file.
59
+ *
60
+ * @example
61
+ * applySettings(canvas, parseSettingsFile(text));
62
+ */
63
+ export declare const parseSettingsFile: (text: string, formats?: readonly string[]) => AppSettings;
@@ -0,0 +1,91 @@
1
+ import { formatErrors } from "../presets/index.js";
2
+ import { validateSettings, validateSettingsFile } from "../validation/validators.generated.js";
3
+ import { MAX_SAVED_SHORTCUTS, MAX_SETTINGS_FILE_FORMAT_LENGTH, SETTINGS_FILE_FORMAT, SETTINGS_FILE_FORMATS, SETTINGS_FILE_VERSION, keyComboSchema, settingsFileSchema, settingsSchema } from "./schema.js";
4
+ // src/lib/settings/index.ts
5
+ /** Messages shown when importing settings fails */
6
+ var SETTINGS_FILE_ERRORS = {
7
+ incompatible: "Settings JSON file not compatible",
8
+ read: "Failed to read settings JSON file"
9
+ };
10
+ /** Importing settings failed, with a message for the user */
11
+ var SettingsFileError = class extends Error {};
12
+ /**
13
+ * Check app settings (e.g. kept in the browser) against `settingsSchema`.
14
+ *
15
+ * @example
16
+ * isValidSettings({ palette: ["#ff0000"] }).valid; // true
17
+ */
18
+ var isValidSettings = (input) => validateSettings(input) ? {
19
+ valid: true,
20
+ value: input
21
+ } : {
22
+ valid: false,
23
+ errors: formatErrors(validateSettings.errors)
24
+ };
25
+ /**
26
+ * The app settings of a canvas: its input settings, keyboard shortcuts, colour palette and colours.
27
+ *
28
+ * @example
29
+ * collectSettings(canvas).palette; // ["#2c6bff", ...]
30
+ */
31
+ var collectSettings = (app) => ({
32
+ input: structuredClone(app.input.data),
33
+ shortcuts: structuredClone(app.shortcuts.bindings),
34
+ palette: [...app.palette.colours],
35
+ colours: { ...app.colours }
36
+ });
37
+ /**
38
+ * Use app settings on a canvas, e.g. ones kept in the browser or imported. Parts left out stay as they are; invalid
39
+ * values within a part are left out (see each part's sanitising).
40
+ *
41
+ * @example
42
+ * applySettings(canvas, { colours: { foreground: "#ff0000", background: "#ffffff" } });
43
+ */
44
+ var applySettings = (app, settings) => {
45
+ if (settings.input) app.input.update(settings.input);
46
+ if (settings.shortcuts) app.shortcuts.load(settings.shortcuts);
47
+ if (settings.palette) app.palette.set(settings.palette);
48
+ if (settings.colours) {
49
+ app.setColour(settings.colours.foreground, "foreground");
50
+ app.setColour(settings.colours.background, "background");
51
+ }
52
+ };
53
+ /**
54
+ * The file name settings are exported to on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
55
+ *
56
+ * @example
57
+ * settingsFileName(new Date(2026, 10, 4)); // "paintcanvas-settings-2026-11-04.json"
58
+ */
59
+ var settingsFileName = (date = /* @__PURE__ */ new Date(), prefix = "paintcanvas") => {
60
+ const pad = (n) => String(n).padStart(2, "0");
61
+ return `${prefix}-settings-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}.json`;
62
+ };
63
+ /**
64
+ * Write app settings to a settings file (JSON) of a format (see `PaintCanvasNames.settingsFileFormat`).
65
+ *
66
+ * @example
67
+ * exportSettingsFile(collectSettings(canvas)); // '{ "format": "paintcanvas-settings", "version": 1, "settings": {...} }'
68
+ */
69
+ var exportSettingsFile = (settings, format = SETTINGS_FILE_FORMAT) => JSON.stringify({
70
+ format,
71
+ version: 1,
72
+ settings
73
+ }, null, 2);
74
+ /**
75
+ * Read the app settings in a settings file of one of the given formats (PaintCanvas's by default). Throws a
76
+ * `SettingsFileError` if it isn't valid JSON or a compatible file.
77
+ *
78
+ * @example
79
+ * applySettings(canvas, parseSettingsFile(text));
80
+ */
81
+ var parseSettingsFile = (text, formats = SETTINGS_FILE_FORMATS) => {
82
+ let data;
83
+ try {
84
+ data = JSON.parse(text);
85
+ } catch {
86
+ throw new SettingsFileError(SETTINGS_FILE_ERRORS.incompatible);
87
+ }
88
+ if (!validateSettingsFile(data) || !formats.includes(data.format)) throw new SettingsFileError(SETTINGS_FILE_ERRORS.incompatible);
89
+ return data.settings;
90
+ };
91
+ export { MAX_SAVED_SHORTCUTS, MAX_SETTINGS_FILE_FORMAT_LENGTH, SETTINGS_FILE_ERRORS, SETTINGS_FILE_FORMAT, SETTINGS_FILE_FORMATS, SETTINGS_FILE_VERSION, SettingsFileError, applySettings, collectSettings, exportSettingsFile, isValidSettings, keyComboSchema, parseSettingsFile, settingsFileName, settingsFileSchema, settingsSchema };