@lvl99dotcom/paintcanvas 0.3.0 → 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.
- package/README.md +1 -1
- package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
- package/dist/lib/brushes/StrokeEngine/index.js +26 -28
- package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
- package/dist/lib/classes/BrushSettings/index.js +35 -1
- package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
- package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
- package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
- package/dist/lib/classes/PaintCanvas/index.js +143 -42
- package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
- package/dist/lib/classes/PaintCanvas/tool.js +18 -0
- package/dist/lib/classes/Palette/index.d.ts +58 -0
- package/dist/lib/classes/Palette/index.js +92 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
- package/dist/lib/classes/SettingsAutosave/index.js +96 -0
- package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
- package/dist/lib/classes/ShortcutSettings/index.js +110 -0
- package/dist/lib/palette/index.d.ts +49 -0
- package/dist/lib/palette/index.js +82 -0
- package/dist/lib/palette/schema.d.ts +45 -0
- package/dist/lib/palette/schema.js +41 -0
- package/dist/lib/rendering/cursors/index.d.ts +2 -0
- package/dist/lib/rendering/cursors/index.js +3 -1
- package/dist/lib/rendering/sample/index.js +3 -2
- package/dist/lib/settings/index.d.ts +63 -0
- package/dist/lib/settings/index.js +91 -0
- package/dist/lib/settings/schema.d.ts +150 -0
- package/dist/lib/settings/schema.js +102 -0
- package/dist/lib/storage/index.d.ts +36 -5
- package/dist/lib/storage/index.js +87 -29
- package/dist/lib/utils/colour/index.d.ts +72 -0
- package/dist/lib/utils/colour/index.js +154 -0
- package/dist/lib/utils/shortcuts/index.d.ts +174 -11
- package/dist/lib/utils/shortcuts/index.js +391 -145
- package/dist/lib/validation/validators.generated.d.ts +5 -0
- package/dist/lib/validation/validators.generated.js +872 -1
- package/dist/locales/en-GB.js +87 -1
- package/dist/locales/en-US.js +87 -1
- package/dist/locales/fr.js +87 -1
- package/dist/locales/ja.js +87 -1
- package/dist/locales/ko.js +87 -1
- package/dist/locales/zh-Hans.js +87 -1
- package/dist/locales/zh-Hant.js +87 -1
- package/dist/package.js +1 -1
- package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
- package/dist/ui/elements/PaintCanvas/index.js +128 -169
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
- package/dist/ui/elements/PcAppSettings/index.js +222 -0
- package/dist/ui/elements/PcBrushConfig/index.js +1 -0
- package/dist/ui/elements/PcButton/index.js +1 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
- package/dist/ui/elements/PcCanvas/index.js +7 -5
- package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
- package/dist/ui/elements/PcColourPair/index.js +125 -0
- package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
- package/dist/ui/elements/PcContextMenu/index.js +128 -97
- package/dist/ui/elements/PcDialog/index.js +1 -0
- package/dist/ui/elements/PcIconToggle/index.js +1 -0
- package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
- package/dist/ui/elements/PcLayerItem/index.js +39 -91
- package/dist/ui/elements/PcLayerItem/style.js +1 -1
- package/dist/ui/elements/PcMenu/index.d.ts +93 -0
- package/dist/ui/elements/PcMenu/index.js +204 -0
- package/dist/ui/elements/PcPane/index.d.ts +118 -0
- package/dist/ui/elements/PcPane/index.js +357 -0
- package/dist/ui/elements/PcPanel/index.d.ts +26 -8
- package/dist/ui/elements/PcPanel/index.js +76 -8
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +20 -15
- package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
- package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
- package/dist/ui/elements/PcPanelColours/index.js +650 -0
- package/dist/ui/elements/PcPanelColours/style.js +3 -0
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
- package/dist/ui/elements/PcPanelLayers/style.js +3 -0
- package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
- package/dist/ui/elements/PcThemePicker/index.js +1 -0
- package/dist/ui/elements/PcToast/index.js +1 -0
- package/dist/ui/elements/PcToolSettings/index.js +2 -1
- package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
- package/dist/ui/elements/PcToolbar/index.js +77 -56
- package/dist/ui/elements/PcToolbar/style.js +1 -1
- package/dist/ui/index.d.ts +7 -1
- package/dist/ui/index.js +8 -2
- package/dist/ui/tokens/index.js +3 -1
- package/locales/en-GB.json +87 -1
- package/locales/en-US.json +87 -1
- package/locales/fr.json +87 -1
- package/locales/ja.json +87 -1
- package/locales/ko.json +87 -1
- package/locales/zh-Hans.json +87 -1
- package/locales/zh-Hant.json +87 -1
- package/package.json +1 -1
- package/dist/ui/elements/PcBrushMenu/style.js +0 -3
- 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
|
-
|
|
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 };
|