@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,150 @@
|
|
|
1
|
+
import type { InputSettingsData } from "../classes/InputSettings";
|
|
2
|
+
import type { KeyCombo } from "../utils/shortcuts";
|
|
3
|
+
/** The `format` of a settings file, unless the implementer names their own (see `PaintCanvasNames`) */
|
|
4
|
+
export declare const SETTINGS_FILE_FORMAT = "paintcanvas-settings";
|
|
5
|
+
/** Formats of settings files that are imported by default */
|
|
6
|
+
export declare const SETTINGS_FILE_FORMATS: readonly string[];
|
|
7
|
+
/** The version of the settings file format */
|
|
8
|
+
export declare const SETTINGS_FILE_VERSION = 1;
|
|
9
|
+
/** Longest settings file format name (characters) */
|
|
10
|
+
export declare const MAX_SETTINGS_FILE_FORMAT_LENGTH = 100;
|
|
11
|
+
/** Most keyboard shortcuts in saved settings */
|
|
12
|
+
export declare const MAX_SAVED_SHORTCUTS = 200;
|
|
13
|
+
/** The user's app settings. Each part is optional, so a file can hold only some of them. */
|
|
14
|
+
export interface AppSettings {
|
|
15
|
+
/** How input devices control the canvas */
|
|
16
|
+
input?: Partial<InputSettingsData>;
|
|
17
|
+
/** Each configurable keyboard shortcut action's key combination, by ID (null when it has none) */
|
|
18
|
+
shortcuts?: Record<string, KeyCombo | null>;
|
|
19
|
+
/** The colour palette's colours, as `#rrggbb` */
|
|
20
|
+
palette?: string[];
|
|
21
|
+
/** The foreground and background colours, as `#rrggbb` */
|
|
22
|
+
colours?: {
|
|
23
|
+
foreground: string;
|
|
24
|
+
background: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A settings file: what users export and import */
|
|
28
|
+
export interface SettingsFile {
|
|
29
|
+
/** Names the app or kind of file, e.g. `SETTINGS_FILE_FORMAT` */
|
|
30
|
+
format: string;
|
|
31
|
+
version: typeof SETTINGS_FILE_VERSION;
|
|
32
|
+
settings: AppSettings;
|
|
33
|
+
}
|
|
34
|
+
/** Schema of a key combination */
|
|
35
|
+
export declare const keyComboSchema: {
|
|
36
|
+
readonly $id: "paintcanvas/key-combo";
|
|
37
|
+
readonly type: "object";
|
|
38
|
+
readonly additionalProperties: false;
|
|
39
|
+
readonly required: readonly ["key"];
|
|
40
|
+
readonly properties: {
|
|
41
|
+
readonly key: {
|
|
42
|
+
readonly type: "string";
|
|
43
|
+
readonly minLength: 1;
|
|
44
|
+
readonly maxLength: 32;
|
|
45
|
+
};
|
|
46
|
+
readonly cmd: {
|
|
47
|
+
readonly type: "boolean";
|
|
48
|
+
};
|
|
49
|
+
readonly ctrl: {
|
|
50
|
+
readonly type: "boolean";
|
|
51
|
+
};
|
|
52
|
+
readonly shift: {
|
|
53
|
+
readonly type: "boolean";
|
|
54
|
+
};
|
|
55
|
+
readonly alt: {
|
|
56
|
+
readonly type: "boolean";
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** Schema of the user's app settings */
|
|
61
|
+
export declare const settingsSchema: {
|
|
62
|
+
readonly $id: "paintcanvas/settings";
|
|
63
|
+
readonly type: "object";
|
|
64
|
+
readonly additionalProperties: false;
|
|
65
|
+
readonly properties: {
|
|
66
|
+
readonly input: {
|
|
67
|
+
readonly type: "object";
|
|
68
|
+
readonly additionalProperties: false;
|
|
69
|
+
readonly properties: {
|
|
70
|
+
readonly scrollPan: {
|
|
71
|
+
readonly type: "boolean";
|
|
72
|
+
};
|
|
73
|
+
readonly pinchZoom: {
|
|
74
|
+
readonly type: "boolean";
|
|
75
|
+
};
|
|
76
|
+
readonly modifierScrollZoom: {
|
|
77
|
+
readonly type: "boolean";
|
|
78
|
+
};
|
|
79
|
+
readonly focusPen: {
|
|
80
|
+
readonly type: "boolean";
|
|
81
|
+
};
|
|
82
|
+
readonly gestures: {
|
|
83
|
+
readonly type: "object";
|
|
84
|
+
readonly maxProperties: 20;
|
|
85
|
+
readonly additionalProperties: {
|
|
86
|
+
readonly type: "string";
|
|
87
|
+
readonly maxLength: 50;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
readonly shortcuts: {
|
|
93
|
+
readonly type: "object";
|
|
94
|
+
readonly maxProperties: 200;
|
|
95
|
+
readonly propertyNames: {
|
|
96
|
+
readonly maxLength: 64;
|
|
97
|
+
};
|
|
98
|
+
readonly additionalProperties: {
|
|
99
|
+
readonly anyOf: readonly [{
|
|
100
|
+
readonly type: "null";
|
|
101
|
+
}, {
|
|
102
|
+
readonly $ref: "key-combo";
|
|
103
|
+
}];
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
readonly palette: {
|
|
107
|
+
readonly type: "array";
|
|
108
|
+
readonly maxItems: 1000;
|
|
109
|
+
readonly items: {
|
|
110
|
+
readonly type: "string";
|
|
111
|
+
readonly pattern: "^#[0-9a-fA-F]{6}$";
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
readonly colours: {
|
|
115
|
+
readonly type: "object";
|
|
116
|
+
readonly additionalProperties: false;
|
|
117
|
+
readonly required: readonly ["foreground", "background"];
|
|
118
|
+
readonly properties: {
|
|
119
|
+
readonly foreground: {
|
|
120
|
+
readonly type: "string";
|
|
121
|
+
readonly pattern: "^#[0-9a-fA-F]{6}$";
|
|
122
|
+
};
|
|
123
|
+
readonly background: {
|
|
124
|
+
readonly type: "string";
|
|
125
|
+
readonly pattern: "^#[0-9a-fA-F]{6}$";
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
/** Schema of a settings file */
|
|
132
|
+
export declare const settingsFileSchema: {
|
|
133
|
+
readonly $id: "paintcanvas/settings-file";
|
|
134
|
+
readonly type: "object";
|
|
135
|
+
readonly additionalProperties: false;
|
|
136
|
+
readonly required: readonly ["format", "version", "settings"];
|
|
137
|
+
readonly properties: {
|
|
138
|
+
readonly format: {
|
|
139
|
+
readonly type: "string";
|
|
140
|
+
readonly minLength: 1;
|
|
141
|
+
readonly maxLength: 100;
|
|
142
|
+
};
|
|
143
|
+
readonly version: {
|
|
144
|
+
readonly const: 1;
|
|
145
|
+
};
|
|
146
|
+
readonly settings: {
|
|
147
|
+
readonly $ref: "settings";
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { HEX_COLOUR_PATTERN, MAX_PALETTE_COLOURS, paletteFileSchema } from "../palette/schema.js";
|
|
2
|
+
// src/lib/settings/schema.ts
|
|
3
|
+
/** The `format` of a settings file, unless the implementer names their own (see `PaintCanvasNames`) */
|
|
4
|
+
var SETTINGS_FILE_FORMAT = "paintcanvas-settings";
|
|
5
|
+
/** Formats of settings files that are imported by default */
|
|
6
|
+
var SETTINGS_FILE_FORMATS = [SETTINGS_FILE_FORMAT];
|
|
7
|
+
/** The version of the settings file format */
|
|
8
|
+
var SETTINGS_FILE_VERSION = 1;
|
|
9
|
+
/** Longest settings file format name (characters) */
|
|
10
|
+
var MAX_SETTINGS_FILE_FORMAT_LENGTH = 100;
|
|
11
|
+
/** Most keyboard shortcuts in saved settings */
|
|
12
|
+
var MAX_SAVED_SHORTCUTS = 200;
|
|
13
|
+
/** A `#rrggbb` colour */
|
|
14
|
+
var hexColour = {
|
|
15
|
+
type: "string",
|
|
16
|
+
pattern: HEX_COLOUR_PATTERN
|
|
17
|
+
};
|
|
18
|
+
/** Schema of a key combination */
|
|
19
|
+
var keyComboSchema = {
|
|
20
|
+
$id: "paintcanvas/key-combo",
|
|
21
|
+
type: "object",
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
required: ["key"],
|
|
24
|
+
properties: {
|
|
25
|
+
key: {
|
|
26
|
+
type: "string",
|
|
27
|
+
minLength: 1,
|
|
28
|
+
maxLength: 32
|
|
29
|
+
},
|
|
30
|
+
cmd: { type: "boolean" },
|
|
31
|
+
ctrl: { type: "boolean" },
|
|
32
|
+
shift: { type: "boolean" },
|
|
33
|
+
alt: { type: "boolean" }
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/** Schema of the user's app settings */
|
|
37
|
+
var settingsSchema = {
|
|
38
|
+
$id: "paintcanvas/settings",
|
|
39
|
+
type: "object",
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
properties: {
|
|
42
|
+
input: {
|
|
43
|
+
type: "object",
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
properties: {
|
|
46
|
+
scrollPan: { type: "boolean" },
|
|
47
|
+
pinchZoom: { type: "boolean" },
|
|
48
|
+
modifierScrollZoom: { type: "boolean" },
|
|
49
|
+
focusPen: { type: "boolean" },
|
|
50
|
+
gestures: {
|
|
51
|
+
type: "object",
|
|
52
|
+
maxProperties: 20,
|
|
53
|
+
additionalProperties: {
|
|
54
|
+
type: "string",
|
|
55
|
+
maxLength: 50
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
shortcuts: {
|
|
61
|
+
type: "object",
|
|
62
|
+
maxProperties: 200,
|
|
63
|
+
propertyNames: { maxLength: 64 },
|
|
64
|
+
additionalProperties: { anyOf: [{ type: "null" }, { $ref: "key-combo" }] }
|
|
65
|
+
},
|
|
66
|
+
palette: {
|
|
67
|
+
type: "array",
|
|
68
|
+
maxItems: MAX_PALETTE_COLOURS,
|
|
69
|
+
items: paletteFileSchema.properties.colours.items
|
|
70
|
+
},
|
|
71
|
+
colours: {
|
|
72
|
+
type: "object",
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
required: ["foreground", "background"],
|
|
75
|
+
properties: {
|
|
76
|
+
foreground: hexColour,
|
|
77
|
+
background: hexColour
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/** Schema of a settings file */
|
|
83
|
+
var settingsFileSchema = {
|
|
84
|
+
$id: "paintcanvas/settings-file",
|
|
85
|
+
type: "object",
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
required: [
|
|
88
|
+
"format",
|
|
89
|
+
"version",
|
|
90
|
+
"settings"
|
|
91
|
+
],
|
|
92
|
+
properties: {
|
|
93
|
+
format: {
|
|
94
|
+
type: "string",
|
|
95
|
+
minLength: 1,
|
|
96
|
+
maxLength: 100
|
|
97
|
+
},
|
|
98
|
+
version: { const: 1 },
|
|
99
|
+
settings: { $ref: "settings" }
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export { MAX_SAVED_SHORTCUTS, MAX_SETTINGS_FILE_FORMAT_LENGTH, SETTINGS_FILE_FORMAT, SETTINGS_FILE_FORMATS, SETTINGS_FILE_VERSION, keyComboSchema, settingsFileSchema, settingsSchema };
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { LayerConfig } from "../classes/Layer";
|
|
2
|
+
import type { AppSettings } from "../settings/schema";
|
|
2
3
|
/** Version of a saved drawing's shape, so drawings saved by other versions can be recognised */
|
|
3
4
|
export declare const SAVED_DRAWING_VERSION = 1;
|
|
4
|
-
/** Name of the IndexedDB database `IndexedDbLayerStore` keeps drawings in */
|
|
5
|
+
/** Name of the IndexedDB database `IndexedDbLayerStore` keeps drawings in, and `IndexedDbSettingsStore` settings */
|
|
5
6
|
export declare const DATABASE_NAME = "paintcanvas";
|
|
7
|
+
/** Version of the database's object stores (2 added settings) */
|
|
8
|
+
export declare const DATABASE_VERSION = 2;
|
|
6
9
|
/** A saved layer's settings (its pixels are kept separately, as a PNG) */
|
|
7
10
|
export type SavedLayer = Required<LayerConfig>;
|
|
8
11
|
/** A saved drawing: its size, its layers (top first) without their pixels, and the selected layer */
|
|
@@ -43,8 +46,7 @@ export interface LayerStoreLike {
|
|
|
43
46
|
*/
|
|
44
47
|
export default class IndexedDbLayerStore implements LayerStoreLike {
|
|
45
48
|
readonly name: string;
|
|
46
|
-
private readonly
|
|
47
|
-
private database?;
|
|
49
|
+
private readonly connection;
|
|
48
50
|
/**
|
|
49
51
|
* @param name - the drawing's name
|
|
50
52
|
* @param factory - the IndexedDB to use (the browser's by default)
|
|
@@ -54,10 +56,39 @@ export default class IndexedDbLayerStore implements LayerStoreLike {
|
|
|
54
56
|
save(drawing: SavedDrawing, images: ReadonlyMap<string, Blob>): Promise<void>;
|
|
55
57
|
clear(): Promise<void>;
|
|
56
58
|
/**
|
|
57
|
-
* Open the database (once)
|
|
58
|
-
* again.
|
|
59
|
+
* Open the database (once). If it can't be opened, the next call tries again.
|
|
59
60
|
*/
|
|
60
61
|
private open;
|
|
61
62
|
/** The keys of this drawing's layer images: `[name, layerId]` (arrays sort after strings) */
|
|
62
63
|
private imageKeys;
|
|
63
64
|
}
|
|
65
|
+
/** Where the user's app settings are kept (see `IndexedDbSettingsStore`) */
|
|
66
|
+
export interface SettingsStoreLike {
|
|
67
|
+
/** The settings' name, e.g. to keep settings for different apps apart */
|
|
68
|
+
readonly name: string;
|
|
69
|
+
/** The saved settings (untrusted: check them before using them), or undefined if none were saved */
|
|
70
|
+
load(): Promise<unknown>;
|
|
71
|
+
/** Save the settings */
|
|
72
|
+
save(settings: AppSettings): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
/** Name the app settings are kept under when none is given */
|
|
75
|
+
export declare const DEFAULT_SETTINGS_NAME = "default";
|
|
76
|
+
/**
|
|
77
|
+
* Keeps the user's app settings (see `AppSettings`) in the browser's IndexedDB database (`DATABASE_NAME`), under a
|
|
78
|
+
* name.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* const store = new IndexedDbSettingsStore();
|
|
82
|
+
* await store.save(collectSettings(canvas));
|
|
83
|
+
*/
|
|
84
|
+
export declare class IndexedDbSettingsStore implements SettingsStoreLike {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
private readonly connection;
|
|
87
|
+
/**
|
|
88
|
+
* @param name - the settings' name (`DEFAULT_SETTINGS_NAME` by default)
|
|
89
|
+
* @param factory - the IndexedDB to use (the browser's by default)
|
|
90
|
+
*/
|
|
91
|
+
constructor(name?: string, factory?: IDBFactory);
|
|
92
|
+
load(): Promise<unknown>;
|
|
93
|
+
save(settings: AppSettings): Promise<void>;
|
|
94
|
+
}
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
// src/lib/storage/index.ts
|
|
2
2
|
/** Version of a saved drawing's shape, so drawings saved by other versions can be recognised */
|
|
3
3
|
var SAVED_DRAWING_VERSION = 1;
|
|
4
|
-
/** Name of the IndexedDB database `IndexedDbLayerStore` keeps drawings in */
|
|
4
|
+
/** Name of the IndexedDB database `IndexedDbLayerStore` keeps drawings in, and `IndexedDbSettingsStore` settings */
|
|
5
5
|
var DATABASE_NAME = "paintcanvas";
|
|
6
|
-
/** Version of the database's object stores */
|
|
7
|
-
var DATABASE_VERSION =
|
|
6
|
+
/** Version of the database's object stores (2 added settings) */
|
|
7
|
+
var DATABASE_VERSION = 2;
|
|
8
8
|
/** Object store of saved drawings, by name */
|
|
9
9
|
var DRAWINGS = "drawings";
|
|
10
10
|
/** Object store of layer images (PNG blobs), by drawing name and layer ID */
|
|
11
11
|
var IMAGES = "images";
|
|
12
|
+
/** Object store of app settings, by name */
|
|
13
|
+
var SETTINGS = "settings";
|
|
14
|
+
/** Every object store, created when the database is first opened or upgraded */
|
|
15
|
+
var OBJECT_STORES = [
|
|
16
|
+
DRAWINGS,
|
|
17
|
+
IMAGES,
|
|
18
|
+
SETTINGS
|
|
19
|
+
];
|
|
12
20
|
/**
|
|
13
21
|
* Wait for an IndexedDB request's result.
|
|
14
22
|
*/
|
|
@@ -24,6 +32,46 @@ var completed = (transaction) => new Promise((resolve, reject) => {
|
|
|
24
32
|
transaction.onabort = () => reject(transaction.error ?? /* @__PURE__ */ new Error("Transaction aborted"));
|
|
25
33
|
});
|
|
26
34
|
/**
|
|
35
|
+
* Open PaintCanvas's database, creating the object stores it doesn't have yet. When another window upgrades the
|
|
36
|
+
* database, this connection closes and `onClose` is called, so the next use opens it again.
|
|
37
|
+
*/
|
|
38
|
+
var openDatabase = (factory, onClose) => new Promise((resolve, reject) => {
|
|
39
|
+
const request = factory.open(DATABASE_NAME, 2);
|
|
40
|
+
request.onupgradeneeded = () => {
|
|
41
|
+
const database = request.result;
|
|
42
|
+
for (const store of OBJECT_STORES) if (!database.objectStoreNames.contains(store)) database.createObjectStore(store);
|
|
43
|
+
};
|
|
44
|
+
request.onsuccess = () => {
|
|
45
|
+
const database = request.result;
|
|
46
|
+
database.onversionchange = () => {
|
|
47
|
+
database.close();
|
|
48
|
+
onClose();
|
|
49
|
+
};
|
|
50
|
+
resolve(database);
|
|
51
|
+
};
|
|
52
|
+
request.onerror = () => reject(request.error);
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Opens PaintCanvas's database once and keeps the connection, opening it again if it closes or fails to open.
|
|
56
|
+
*/
|
|
57
|
+
var DatabaseConnection = class {
|
|
58
|
+
factory;
|
|
59
|
+
database;
|
|
60
|
+
constructor(factory) {
|
|
61
|
+
this.factory = factory;
|
|
62
|
+
}
|
|
63
|
+
/** The open database */
|
|
64
|
+
open() {
|
|
65
|
+
this.database ??= openDatabase(this.factory, () => {
|
|
66
|
+
this.database = void 0;
|
|
67
|
+
}).catch((error) => {
|
|
68
|
+
this.database = void 0;
|
|
69
|
+
throw error;
|
|
70
|
+
});
|
|
71
|
+
return this.database;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
27
75
|
* The layer ID in a layer image's key (`[name, layerId]`).
|
|
28
76
|
*/
|
|
29
77
|
var layerIdOf = (key) => key[1];
|
|
@@ -36,15 +84,14 @@ var layerIdOf = (key) => key[1];
|
|
|
36
84
|
*/
|
|
37
85
|
var IndexedDbLayerStore = class {
|
|
38
86
|
name;
|
|
39
|
-
|
|
40
|
-
database;
|
|
87
|
+
connection;
|
|
41
88
|
/**
|
|
42
89
|
* @param name - the drawing's name
|
|
43
90
|
* @param factory - the IndexedDB to use (the browser's by default)
|
|
44
91
|
*/
|
|
45
92
|
constructor(name, factory = indexedDB) {
|
|
46
93
|
this.name = name;
|
|
47
|
-
this.
|
|
94
|
+
this.connection = new DatabaseConnection(factory);
|
|
48
95
|
}
|
|
49
96
|
async load() {
|
|
50
97
|
const transaction = (await this.open()).transaction([DRAWINGS, IMAGES], "readonly");
|
|
@@ -81,34 +128,45 @@ var IndexedDbLayerStore = class {
|
|
|
81
128
|
await done;
|
|
82
129
|
}
|
|
83
130
|
/**
|
|
84
|
-
* Open the database (once)
|
|
85
|
-
* again.
|
|
131
|
+
* Open the database (once). If it can't be opened, the next call tries again.
|
|
86
132
|
*/
|
|
87
133
|
open() {
|
|
88
|
-
this.
|
|
89
|
-
const request = this.factory.open(DATABASE_NAME, DATABASE_VERSION);
|
|
90
|
-
request.onupgradeneeded = () => {
|
|
91
|
-
request.result.createObjectStore(DRAWINGS);
|
|
92
|
-
request.result.createObjectStore(IMAGES);
|
|
93
|
-
};
|
|
94
|
-
request.onsuccess = () => {
|
|
95
|
-
const database = request.result;
|
|
96
|
-
database.onversionchange = () => {
|
|
97
|
-
database.close();
|
|
98
|
-
this.database = void 0;
|
|
99
|
-
};
|
|
100
|
-
resolve(database);
|
|
101
|
-
};
|
|
102
|
-
request.onerror = () => {
|
|
103
|
-
this.database = void 0;
|
|
104
|
-
reject(request.error);
|
|
105
|
-
};
|
|
106
|
-
});
|
|
107
|
-
return this.database;
|
|
134
|
+
return this.connection.open();
|
|
108
135
|
}
|
|
109
136
|
/** The keys of this drawing's layer images: `[name, layerId]` (arrays sort after strings) */
|
|
110
137
|
imageKeys() {
|
|
111
138
|
return IDBKeyRange.bound([this.name], [this.name, []]);
|
|
112
139
|
}
|
|
113
140
|
};
|
|
114
|
-
|
|
141
|
+
/** Name the app settings are kept under when none is given */
|
|
142
|
+
var DEFAULT_SETTINGS_NAME = "default";
|
|
143
|
+
/**
|
|
144
|
+
* Keeps the user's app settings (see `AppSettings`) in the browser's IndexedDB database (`DATABASE_NAME`), under a
|
|
145
|
+
* name.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* const store = new IndexedDbSettingsStore();
|
|
149
|
+
* await store.save(collectSettings(canvas));
|
|
150
|
+
*/
|
|
151
|
+
var IndexedDbSettingsStore = class {
|
|
152
|
+
name;
|
|
153
|
+
connection;
|
|
154
|
+
/**
|
|
155
|
+
* @param name - the settings' name (`DEFAULT_SETTINGS_NAME` by default)
|
|
156
|
+
* @param factory - the IndexedDB to use (the browser's by default)
|
|
157
|
+
*/
|
|
158
|
+
constructor(name = DEFAULT_SETTINGS_NAME, factory = indexedDB) {
|
|
159
|
+
this.name = name;
|
|
160
|
+
this.connection = new DatabaseConnection(factory);
|
|
161
|
+
}
|
|
162
|
+
async load() {
|
|
163
|
+
return result((await this.connection.open()).transaction(SETTINGS, "readonly").objectStore(SETTINGS).get(this.name));
|
|
164
|
+
}
|
|
165
|
+
async save(settings) {
|
|
166
|
+
const transaction = (await this.connection.open()).transaction(SETTINGS, "readwrite");
|
|
167
|
+
const done = completed(transaction);
|
|
168
|
+
transaction.objectStore(SETTINGS).put(settings, this.name);
|
|
169
|
+
await done;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
export { DATABASE_NAME, DATABASE_VERSION, DEFAULT_SETTINGS_NAME, IndexedDbSettingsStore, SAVED_DRAWING_VERSION, IndexedDbLayerStore as default };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** A colour's red, green and blue channels, each from 0 to 255 */
|
|
2
|
+
export interface Rgb {
|
|
3
|
+
r: number;
|
|
4
|
+
g: number;
|
|
5
|
+
b: number;
|
|
6
|
+
}
|
|
7
|
+
/** A colour's hue (0–360 degrees), saturation (0–100%) and lightness (0–100%) */
|
|
8
|
+
export interface Hsl {
|
|
9
|
+
h: number;
|
|
10
|
+
s: number;
|
|
11
|
+
l: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Check that a value is a 6-digit hex colour, e.g. `#ff0000`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* isHexColour("#FF00aa"); // true
|
|
18
|
+
* isHexColour("red"); // false
|
|
19
|
+
*/
|
|
20
|
+
export declare const isHexColour: (value: unknown) => value is string;
|
|
21
|
+
/**
|
|
22
|
+
* Turn a hex colour as typed (3 or 6 digits, with or without `#`, any case) into `#rrggbb`. Undefined if it isn't one.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* normaliseHex("F0a"); // "#ff00aa"
|
|
26
|
+
* normaliseHex(" #123456 "); // "#123456"
|
|
27
|
+
* normaliseHex("#12345"); // undefined
|
|
28
|
+
*/
|
|
29
|
+
export declare const normaliseHex: (input: string) => string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The red, green and blue channels of a `#rrggbb` colour.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* hexToRgb("#ff8000"); // { r: 255, g: 128, b: 0 }
|
|
35
|
+
*/
|
|
36
|
+
export declare const hexToRgb: (hex: string) => Rgb;
|
|
37
|
+
/**
|
|
38
|
+
* Format red, green and blue channels as `#rrggbb`, rounding and keeping each from 0 to 255.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* rgbToHex({ r: 255, g: 127.6, b: -3 }); // "#ff8000"
|
|
42
|
+
*/
|
|
43
|
+
export declare const rgbToHex: ({ r, g, b }: Rgb) => string;
|
|
44
|
+
/**
|
|
45
|
+
* Convert red, green and blue channels to hue, saturation and lightness, rounded to whole numbers. Greys have hue 0.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* rgbToHsl({ r: 255, g: 0, b: 0 }); // { h: 0, s: 100, l: 50 }
|
|
49
|
+
*/
|
|
50
|
+
export declare const rgbToHsl: ({ r, g, b }: Rgb) => Hsl;
|
|
51
|
+
/**
|
|
52
|
+
* Convert hue (degrees, wrapped to 0–360), saturation and lightness (percentages, kept from 0 to 100) to red, green and
|
|
53
|
+
* blue channels, rounded to whole numbers.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* hslToRgb({ h: 120, s: 100, l: 25 }); // { r: 0, g: 128, b: 0 }
|
|
57
|
+
*/
|
|
58
|
+
export declare const hslToRgb: ({ h, s, l }: Hsl) => Rgb;
|
|
59
|
+
/**
|
|
60
|
+
* Convert a `#rrggbb` colour to hue, saturation and lightness.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* hexToHsl("#ff0000"); // { h: 0, s: 100, l: 50 }
|
|
64
|
+
*/
|
|
65
|
+
export declare const hexToHsl: (hex: string) => Hsl;
|
|
66
|
+
/**
|
|
67
|
+
* Convert hue, saturation and lightness to a `#rrggbb` colour.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* hslToHex({ h: 240, s: 100, l: 50 }); // "#0000ff"
|
|
71
|
+
*/
|
|
72
|
+
export declare const hslToHex: (hsl: Hsl) => string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/lib/utils/colour/index.ts
|
|
2
|
+
/** Pattern for `#rrggbb` colours */
|
|
3
|
+
var HEX_PATTERN = /^#[0-9a-f]{6}$/i;
|
|
4
|
+
/** Pattern for hex colours as people type them: 3 or 6 digits, with or without `#` */
|
|
5
|
+
var HEX_INPUT_PATTERN = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
6
|
+
/**
|
|
7
|
+
* Keep a number within a range.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* clamp(300, 0, 255); // 255
|
|
11
|
+
*/
|
|
12
|
+
var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
13
|
+
/**
|
|
14
|
+
* Check that a value is a 6-digit hex colour, e.g. `#ff0000`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* isHexColour("#FF00aa"); // true
|
|
18
|
+
* isHexColour("red"); // false
|
|
19
|
+
*/
|
|
20
|
+
var isHexColour = (value) => typeof value === "string" && HEX_PATTERN.test(value);
|
|
21
|
+
/**
|
|
22
|
+
* Turn a hex colour as typed (3 or 6 digits, with or without `#`, any case) into `#rrggbb`. Undefined if it isn't one.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* normaliseHex("F0a"); // "#ff00aa"
|
|
26
|
+
* normaliseHex(" #123456 "); // "#123456"
|
|
27
|
+
* normaliseHex("#12345"); // undefined
|
|
28
|
+
*/
|
|
29
|
+
var normaliseHex = (input) => {
|
|
30
|
+
const digits = HEX_INPUT_PATTERN.exec(input.trim())?.[1];
|
|
31
|
+
if (!digits) return void 0;
|
|
32
|
+
return `#${(digits.length === 3 ? [...digits].map((digit) => digit + digit).join("") : digits).toLowerCase()}`;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The red, green and blue channels of a `#rrggbb` colour.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* hexToRgb("#ff8000"); // { r: 255, g: 128, b: 0 }
|
|
39
|
+
*/
|
|
40
|
+
var hexToRgb = (hex) => {
|
|
41
|
+
const value = Number.parseInt(hex.slice(1), 16);
|
|
42
|
+
return {
|
|
43
|
+
r: value >> 16 & 255,
|
|
44
|
+
g: value >> 8 & 255,
|
|
45
|
+
b: value & 255
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Format red, green and blue channels as `#rrggbb`, rounding and keeping each from 0 to 255.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* rgbToHex({ r: 255, g: 127.6, b: -3 }); // "#ff8000"
|
|
53
|
+
*/
|
|
54
|
+
var rgbToHex = ({ r, g, b }) => `#${[
|
|
55
|
+
r,
|
|
56
|
+
g,
|
|
57
|
+
b
|
|
58
|
+
].map((channel) => Math.round(clamp(channel, 0, 255)).toString(16).padStart(2, "0")).join("")}`;
|
|
59
|
+
/**
|
|
60
|
+
* Convert red, green and blue channels to hue, saturation and lightness, rounded to whole numbers. Greys have hue 0.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* rgbToHsl({ r: 255, g: 0, b: 0 }); // { h: 0, s: 100, l: 50 }
|
|
64
|
+
*/
|
|
65
|
+
var rgbToHsl = ({ r, g, b }) => {
|
|
66
|
+
const [red, green, blue] = [
|
|
67
|
+
r,
|
|
68
|
+
g,
|
|
69
|
+
b
|
|
70
|
+
].map((channel) => channel / 255);
|
|
71
|
+
const max = Math.max(red, green, blue);
|
|
72
|
+
const min = Math.min(red, green, blue);
|
|
73
|
+
const lightness = (max + min) / 2;
|
|
74
|
+
const range = max - min;
|
|
75
|
+
if (range === 0) return {
|
|
76
|
+
h: 0,
|
|
77
|
+
s: 0,
|
|
78
|
+
l: Math.round(lightness * 100)
|
|
79
|
+
};
|
|
80
|
+
const saturation = range / (1 - Math.abs(2 * lightness - 1));
|
|
81
|
+
const hue = max === red ? ((green - blue) / range + 6) % 6 : max === green ? (blue - red) / range + 2 : (red - green) / range + 4;
|
|
82
|
+
return {
|
|
83
|
+
h: Math.round(hue * 60) % 360,
|
|
84
|
+
s: Math.round(saturation * 100),
|
|
85
|
+
l: Math.round(lightness * 100)
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Convert hue (degrees, wrapped to 0–360), saturation and lightness (percentages, kept from 0 to 100) to red, green and
|
|
90
|
+
* blue channels, rounded to whole numbers.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* hslToRgb({ h: 120, s: 100, l: 25 }); // { r: 0, g: 128, b: 0 }
|
|
94
|
+
*/
|
|
95
|
+
var hslToRgb = ({ h, s, l }) => {
|
|
96
|
+
const hue = (h % 360 + 360) % 360 / 60;
|
|
97
|
+
const saturation = clamp(s, 0, 100) / 100;
|
|
98
|
+
const lightness = clamp(l, 0, 100) / 100;
|
|
99
|
+
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
|
100
|
+
const second = chroma * (1 - Math.abs(hue % 2 - 1));
|
|
101
|
+
const [red, green, blue] = [
|
|
102
|
+
[
|
|
103
|
+
chroma,
|
|
104
|
+
second,
|
|
105
|
+
0
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
second,
|
|
109
|
+
chroma,
|
|
110
|
+
0
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
0,
|
|
114
|
+
chroma,
|
|
115
|
+
second
|
|
116
|
+
],
|
|
117
|
+
[
|
|
118
|
+
0,
|
|
119
|
+
second,
|
|
120
|
+
chroma
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
second,
|
|
124
|
+
0,
|
|
125
|
+
chroma
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
chroma,
|
|
129
|
+
0,
|
|
130
|
+
second
|
|
131
|
+
]
|
|
132
|
+
][Math.floor(hue)];
|
|
133
|
+
const match = lightness - chroma / 2;
|
|
134
|
+
return {
|
|
135
|
+
r: Math.round((red + match) * 255),
|
|
136
|
+
g: Math.round((green + match) * 255),
|
|
137
|
+
b: Math.round((blue + match) * 255)
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Convert a `#rrggbb` colour to hue, saturation and lightness.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* hexToHsl("#ff0000"); // { h: 0, s: 100, l: 50 }
|
|
145
|
+
*/
|
|
146
|
+
var hexToHsl = (hex) => rgbToHsl(hexToRgb(hex));
|
|
147
|
+
/**
|
|
148
|
+
* Convert hue, saturation and lightness to a `#rrggbb` colour.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* hslToHex({ h: 240, s: 100, l: 50 }); // "#0000ff"
|
|
152
|
+
*/
|
|
153
|
+
var hslToHex = (hsl) => rgbToHex(hslToRgb(hsl));
|
|
154
|
+
export { hexToHsl, hexToRgb, hslToHex, hslToRgb, isHexColour, normaliseHex, rgbToHex, rgbToHsl };
|