@lvl99dotcom/paintcanvas 0.6.0 → 0.8.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 +3 -2
- package/dist/lib/assets/index.d.ts +92 -0
- package/dist/lib/assets/index.js +106 -0
- package/dist/lib/assets/schema.d.ts +151 -0
- package/dist/lib/assets/schema.js +76 -0
- package/dist/lib/brushes/presetFile/index.d.ts +6 -27
- package/dist/lib/brushes/presetFile/index.js +34 -47
- package/dist/lib/brushes/types/index.d.ts +2 -11
- package/dist/lib/classes/AssetSync/index.d.ts +45 -0
- package/dist/lib/classes/AssetSync/index.js +80 -0
- package/dist/lib/classes/ColourPalettes/index.d.ts +83 -0
- package/dist/lib/classes/ColourPalettes/index.js +165 -0
- package/dist/lib/classes/CustomPresets/index.d.ts +14 -15
- package/dist/lib/classes/CustomPresets/index.js +25 -10
- package/dist/lib/classes/LayerAutosave/index.d.ts +9 -2
- package/dist/lib/classes/LayerAutosave/index.js +16 -6
- package/dist/lib/classes/PaintCanvas/index.d.ts +124 -28
- package/dist/lib/classes/PaintCanvas/index.js +228 -63
- package/dist/lib/classes/Palette/index.d.ts +3 -2
- package/dist/lib/classes/Palette/index.js +3 -2
- package/dist/lib/classes/ProfileGraphs/index.d.ts +13 -2
- package/dist/lib/classes/ProfileGraphs/index.js +22 -2
- package/dist/lib/classes/RemoteDocumentAutosave/index.d.ts +73 -0
- package/dist/lib/classes/RemoteDocumentAutosave/index.js +119 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +7 -3
- package/dist/lib/classes/SettingsAutosave/index.js +28 -7
- package/dist/lib/classes/SettingsExtensions/index.d.ts +67 -0
- package/dist/lib/classes/SettingsExtensions/index.js +88 -0
- package/dist/lib/classes/UiSettings/index.d.ts +24 -2
- package/dist/lib/classes/UiSettings/index.js +32 -7
- package/dist/lib/document/index.d.ts +61 -4
- package/dist/lib/document/index.js +173 -4
- package/dist/lib/document/schema.d.ts +154 -39
- package/dist/lib/document/schema.js +91 -25
- package/dist/lib/messages/schema.d.ts +176 -74
- package/dist/lib/messages/schema.js +2 -1
- package/dist/lib/palette/index.d.ts +8 -42
- package/dist/lib/palette/index.js +26 -59
- package/dist/lib/palette/schema.d.ts +111 -22
- package/dist/lib/palette/schema.js +51 -26
- package/dist/lib/presets/index.d.ts +1 -9
- package/dist/lib/presets/index.js +4 -10
- package/dist/lib/presets/schema.d.ts +118 -73
- package/dist/lib/presets/schema.js +28 -60
- package/dist/lib/profiles/profileFile/index.d.ts +10 -0
- package/dist/lib/profiles/profileFile/index.js +19 -0
- package/dist/lib/profiles/schema.d.ts +96 -3
- package/dist/lib/profiles/schema.js +31 -7
- package/dist/lib/schemas/index.d.ts +42 -0
- package/dist/lib/schemas/index.js +60 -0
- package/dist/lib/settings/index.d.ts +37 -22
- package/dist/lib/settings/index.js +78 -37
- package/dist/lib/settings/schema.d.ts +202 -77
- package/dist/lib/settings/schema.js +93 -86
- package/dist/lib/storage/index.d.ts +108 -3
- package/dist/lib/storage/index.js +119 -5
- package/dist/lib/validation/validators.generated.d.ts +19 -8
- package/dist/lib/validation/validators.generated.js +44723 -18889
- package/dist/lib/version/index.d.ts +25 -0
- package/dist/lib/version/index.js +35 -0
- package/dist/locales/en-GB.js +28 -5
- package/dist/locales/en-US.js +28 -5
- package/dist/locales/fr.js +28 -5
- package/dist/locales/ja.js +28 -5
- package/dist/locales/ko.js +28 -5
- package/dist/locales/zh-Hans.js +28 -5
- package/dist/locales/zh-Hant.js +28 -5
- package/dist/package.js +1 -1
- package/dist/testing/app.d.ts +6 -2
- package/dist/testing/app.js +7 -2
- package/dist/testing/documentStore.d.ts +26 -0
- package/dist/testing/documentStore.js +31 -0
- package/dist/ui/elements/PaintCanvas/index.d.ts +19 -5
- package/dist/ui/elements/PaintCanvas/index.js +41 -10
- package/dist/ui/elements/PcAppSettings/index.d.ts +14 -7
- package/dist/ui/elements/PcAppSettings/index.js +58 -33
- package/dist/ui/elements/PcBrushConfig/index.d.ts +1 -0
- package/dist/ui/elements/PcBrushConfig/index.js +8 -9
- package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
- package/dist/ui/elements/PcBrushTest/index.js +8 -7
- package/dist/ui/elements/PcBrushTest/style.js +1 -1
- package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
- package/dist/ui/elements/PcButtonMenu/index.js +125 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +9 -0
- package/dist/ui/elements/PcCanvas/index.js +33 -17
- package/dist/ui/elements/PcClipboardTools/index.d.ts +7 -4
- package/dist/ui/elements/PcClipboardTools/index.js +20 -26
- package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
- package/dist/ui/elements/PcInputSettings/index.js +9 -15
- package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
- package/dist/ui/elements/PcLayerItem/index.js +53 -26
- package/dist/ui/elements/PcLayerItem/style.js +1 -1
- package/dist/ui/elements/PcPanelBrushes/index.d.ts +1 -1
- package/dist/ui/elements/PcPanelBrushes/index.js +22 -28
- package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
- package/dist/ui/elements/PcPanelColours/index.d.ts +56 -19
- package/dist/ui/elements/PcPanelColours/index.js +251 -104
- package/dist/ui/elements/PcPanelColours/style.js +1 -1
- package/dist/ui/elements/PcSliderRange/index.js +3 -2
- package/dist/ui/elements/PcStats/index.d.ts +8 -4
- package/dist/ui/elements/PcStats/index.js +39 -5
- package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
- package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
- package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
- package/dist/ui/elements/PcToolSettings/index.js +8 -8
- package/dist/ui/elements/PcVersion/index.d.ts +12 -0
- package/dist/ui/elements/PcVersion/index.js +43 -0
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.js +4 -2
- package/dist/ui/layout/index.d.ts +2 -2
- package/dist/ui/layout/index.js +2 -2
- package/locales/en-GB.json +28 -5
- package/locales/en-US.json +28 -5
- package/locales/fr.json +28 -5
- package/locales/ja.json +28 -5
- package/locales/ko.json +28 -5
- package/locales/zh-Hans.json +28 -5
- package/locales/zh-Hant.json +28 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -89,9 +89,9 @@ The demo is published at https://lvl99.github.io/paintcanvas/app/. To run it loc
|
|
|
89
89
|
pnpm dev
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
Starts a development server and opens the demo in your browser. It uses the source directly, so saving a change to `src/`, `demo/` or `locales/` reloads the page with the change straight away. (The whole page reloads because custom elements can't be redefined in a running page, so the drawing starts again.)
|
|
92
|
+
Starts a development server and opens the demo in your browser. It uses the source directly, so saving a change to `src/`, `demo/` or `locales/` reloads the page with the change straight away. (The whole page reloads because custom elements can't be redefined in a running page, so the drawing starts again.) It uses vite.demo.config.ts, which also gives the demo the commit it was built from (see scripts/build-info.ts).
|
|
93
93
|
|
|
94
|
-
- http://localhost:5190/: a full-page PaintCanvas with a layers panel, the colour palette (from the colours at the top of the toolbar), a menu with input settings, keyboard shortcuts, settings import and export, a theme picker (light, dark or system) and a "New drawing" button. The drawing and the settings, including the theme and
|
|
94
|
+
- http://localhost:5190/: a full-page PaintCanvas with a layers panel, the colour palette (from the colours at the top of the toolbar), a menu with PaintCanvas's version, input settings, a "Draw with the GPU" switch, keyboard shortcuts, settings import and export, a theme picker (light, dark or system) and a "New drawing" button. The drawing and the settings, including the theme, language and rendering (CPU or GPU), are kept in the browser, so they survive reloads. Below 600px wide, the toolbar and the layers rail sit over the canvas.
|
|
95
95
|
- http://localhost:5190/elements.html: the base elements.
|
|
96
96
|
|
|
97
97
|
To try it with a pen or touch on another device (e.g. a tablet) on the same network, run `pnpm dev --host` and open the network address it prints. This makes the server reachable by anyone on that network.
|
|
@@ -130,6 +130,7 @@ Tests sit in a `tests` folder beside the code. Unit tests (`*.test.ts`) run in j
|
|
|
130
130
|
| `pnpm coverage` | Runs both, with a coverage report in `coverage/` |
|
|
131
131
|
| `pnpm build` | Builds the package into `dist/`: ES modules matching `src/`, with types |
|
|
132
132
|
| `pnpm build:validators` | Regenerates the validators after changing the preset, message or document schemas |
|
|
133
|
+
| `pnpm build:schemas` | Writes new versions of the published JSON schemas into `schemas/` |
|
|
133
134
|
| `pnpm icons:build` | Optimises the icons' SVGs and makes their PNGs (`pnpm build` and `pnpm dev` run it) |
|
|
134
135
|
| `pnpm dev` | Starts the demo, reloading it when files change (`pnpm demo` does the same) |
|
|
135
136
|
| `pnpm docs` | Starts the documentation site (`pnpm docs:build` builds it into `docs/build/`) |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Validator } from "../validation/validators.generated.js";
|
|
2
|
+
import type { AssetCollectionFile } from "./schema";
|
|
3
|
+
export * from "./schema";
|
|
4
|
+
/** Why reading or writing an asset file failed */
|
|
5
|
+
export type AssetFileErrorReason =
|
|
6
|
+
/** The file couldn't be read, or isn't JSON */
|
|
7
|
+
"read"
|
|
8
|
+
/** The file isn't this kind of file, or doesn't match its schema */
|
|
9
|
+
| "invalid"
|
|
10
|
+
/** The file is a version the app can't read */
|
|
11
|
+
| "unsupported"
|
|
12
|
+
/** There's nothing to export */
|
|
13
|
+
| "empty";
|
|
14
|
+
/** Reading or writing an asset file failed, for a reason the user can be told */
|
|
15
|
+
export declare class AssetFileError extends Error {
|
|
16
|
+
readonly reason: AssetFileErrorReason;
|
|
17
|
+
constructor(reason: AssetFileErrorReason);
|
|
18
|
+
}
|
|
19
|
+
/** Turns a file of an older version into the next version */
|
|
20
|
+
export interface AssetFileMigration {
|
|
21
|
+
/** Version it migrates from: a schema version, or one named by `legacyVersion` */
|
|
22
|
+
from: string;
|
|
23
|
+
/** Version it migrates to */
|
|
24
|
+
to: string;
|
|
25
|
+
/** The file's data in the newer version (its `$schema` and `@type` are set for you) */
|
|
26
|
+
migrate(data: Record<string, unknown>): Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/** What an asset file is */
|
|
29
|
+
export interface AssetFileConfig<T> {
|
|
30
|
+
/** Name of the collection's schema, e.g. `brush-preset-collection` */
|
|
31
|
+
schema: string;
|
|
32
|
+
/** Version of the collection's schema the app writes */
|
|
33
|
+
version: string;
|
|
34
|
+
/** The collection's `@type`, e.g. `PaintCanvas/BrushConfigCollection` */
|
|
35
|
+
type: string;
|
|
36
|
+
/** Checks a file of the current version */
|
|
37
|
+
validate: Validator<AssetCollectionFile<T>>;
|
|
38
|
+
/** Names exported files, e.g. `brush-presets` makes `paintcanvas-brush-presets-2026-11-11.json` */
|
|
39
|
+
fileName: string;
|
|
40
|
+
/** Migrations from older versions, applied one after another */
|
|
41
|
+
migrations?: readonly AssetFileMigration[];
|
|
42
|
+
/** The version of a file from before versioned schemas (e.g. `legacy-1`), or undefined if it isn't one */
|
|
43
|
+
legacyVersion?: (data: Record<string, unknown>) => string | undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads and writes the collection files that users export and import assets of one kind in (e.g. brush presets),
|
|
47
|
+
* migrating files of older versions to the current one.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const presets = brushPresetFile.parse(await file.text()); // throws AssetFileError
|
|
51
|
+
* const json = brushPresetFile.write(presets);
|
|
52
|
+
*/
|
|
53
|
+
export default class AssetFile<T> {
|
|
54
|
+
readonly config: AssetFileConfig<T>;
|
|
55
|
+
constructor(config: AssetFileConfig<T>);
|
|
56
|
+
/** URL of the schema of the files this writes */
|
|
57
|
+
get schemaUrl(): string;
|
|
58
|
+
/**
|
|
59
|
+
* The version of a file of this kind, or undefined if it isn't one.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* brushPresetFile.versionOf({ $schema: ".../brush-preset-collection-1.0.0.json" }); // "1.0.0"
|
|
63
|
+
*/
|
|
64
|
+
versionOf(data: Record<string, unknown>): string | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The assets in a file's data, migrated from an older version if needed. Throws an `AssetFileError` if it isn't
|
|
67
|
+
* this kind of file, isn't valid, or is a version that can't be read.
|
|
68
|
+
*/
|
|
69
|
+
read(data: unknown): T[];
|
|
70
|
+
/**
|
|
71
|
+
* The assets in a file's text. Throws an `AssetFileError` if it isn't JSON, or (see `read`) can't be read.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* brushPresetFile.parse('{ "$schema": "...", "@type": "PaintCanvas/BrushConfigCollection", "items": [] }'); // []
|
|
75
|
+
*/
|
|
76
|
+
parse(text: string): T[];
|
|
77
|
+
/**
|
|
78
|
+
* Write assets to a file (JSON) of the current version. Throws an `AssetFileError` if there are none, or they don't
|
|
79
|
+
* match the schema.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* brushPresetFile.write([myPreset]); // '{ "$schema": "...", "@type": "PaintCanvas/BrushConfigCollection", "items": [...] }'
|
|
83
|
+
*/
|
|
84
|
+
write(items: readonly T[]): string;
|
|
85
|
+
/**
|
|
86
|
+
* The name of a file exported on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* brushPresetFile.fileName(new Date(2026, 10, 11)); // "paintcanvas-brush-presets-2026-11-11.json"
|
|
90
|
+
*/
|
|
91
|
+
fileName(date?: Date, prefix?: string): string;
|
|
92
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { compareVersions, majorVersion, parseSchemaUrl, schemaUrl } from "../schemas/index.js";
|
|
2
|
+
import { DID_PATTERN, MAX_AUTHOR_TEXT_LENGTH, MAX_URL_LENGTH, UUID_PATTERN, authorDetailsSchema, authorSchema, collectionSchema, fileSchemaUrlSchema } from "./schema.js";
|
|
3
|
+
// src/lib/assets/index.ts
|
|
4
|
+
/** Reading or writing an asset file failed, for a reason the user can be told */
|
|
5
|
+
var AssetFileError = class extends Error {
|
|
6
|
+
reason;
|
|
7
|
+
constructor(reason) {
|
|
8
|
+
super(`Asset file error: ${reason}`);
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/** Whether a value is a plain object */
|
|
13
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
/**
|
|
15
|
+
* Reads and writes the collection files that users export and import assets of one kind in (e.g. brush presets),
|
|
16
|
+
* migrating files of older versions to the current one.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const presets = brushPresetFile.parse(await file.text()); // throws AssetFileError
|
|
20
|
+
* const json = brushPresetFile.write(presets);
|
|
21
|
+
*/
|
|
22
|
+
var AssetFile = class {
|
|
23
|
+
config;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
/** URL of the schema of the files this writes */
|
|
28
|
+
get schemaUrl() {
|
|
29
|
+
return schemaUrl(this.config.schema, this.config.version);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The version of a file of this kind, or undefined if it isn't one.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* brushPresetFile.versionOf({ $schema: ".../brush-preset-collection-1.0.0.json" }); // "1.0.0"
|
|
36
|
+
*/
|
|
37
|
+
versionOf(data) {
|
|
38
|
+
const named = parseSchemaUrl(data.$schema);
|
|
39
|
+
if (named) return named.name === this.config.schema ? named.version : void 0;
|
|
40
|
+
return this.config.legacyVersion?.(data);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The assets in a file's data, migrated from an older version if needed. Throws an `AssetFileError` if it isn't
|
|
44
|
+
* this kind of file, isn't valid, or is a version that can't be read.
|
|
45
|
+
*/
|
|
46
|
+
read(data) {
|
|
47
|
+
if (!isRecord(data)) throw new AssetFileError("invalid");
|
|
48
|
+
let version = this.versionOf(data);
|
|
49
|
+
if (version === void 0) throw new AssetFileError("invalid");
|
|
50
|
+
const { migrations = [], type, validate } = this.config;
|
|
51
|
+
for (let migration = migrations.find((m) => m.from === version); migration; migration = migrations.find((m) => m.from === version)) {
|
|
52
|
+
data = {
|
|
53
|
+
...migration.migrate(data),
|
|
54
|
+
$schema: schemaUrl(this.config.schema, migration.to),
|
|
55
|
+
"@type": type
|
|
56
|
+
};
|
|
57
|
+
version = migration.to;
|
|
58
|
+
}
|
|
59
|
+
if (majorVersion(version) !== majorVersion(this.config.version) || compareVersions(version, this.config.version) > 0) throw new AssetFileError("unsupported");
|
|
60
|
+
if (!validate(data)) throw new AssetFileError("invalid");
|
|
61
|
+
return data.items;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The assets in a file's text. Throws an `AssetFileError` if it isn't JSON, or (see `read`) can't be read.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* brushPresetFile.parse('{ "$schema": "...", "@type": "PaintCanvas/BrushConfigCollection", "items": [] }'); // []
|
|
68
|
+
*/
|
|
69
|
+
parse(text) {
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(text);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new AssetFileError("read");
|
|
75
|
+
}
|
|
76
|
+
return this.read(data);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Write assets to a file (JSON) of the current version. Throws an `AssetFileError` if there are none, or they don't
|
|
80
|
+
* match the schema.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* brushPresetFile.write([myPreset]); // '{ "$schema": "...", "@type": "PaintCanvas/BrushConfigCollection", "items": [...] }'
|
|
84
|
+
*/
|
|
85
|
+
write(items) {
|
|
86
|
+
if (items.length === 0) throw new AssetFileError("empty");
|
|
87
|
+
const file = {
|
|
88
|
+
$schema: this.schemaUrl,
|
|
89
|
+
"@type": this.config.type,
|
|
90
|
+
items
|
|
91
|
+
};
|
|
92
|
+
if (!this.config.validate(file)) throw new AssetFileError("invalid");
|
|
93
|
+
return JSON.stringify(file, null, 2);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The name of a file exported on a day, starting with a prefix (see `PaintCanvasNames.filePrefix`).
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* brushPresetFile.fileName(new Date(2026, 10, 11)); // "paintcanvas-brush-presets-2026-11-11.json"
|
|
100
|
+
*/
|
|
101
|
+
fileName(date = /* @__PURE__ */ new Date(), prefix = "paintcanvas") {
|
|
102
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
103
|
+
return `${prefix}-${this.config.fileName}-${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}.json`;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
export { AssetFileError, DID_PATTERN, MAX_AUTHOR_TEXT_LENGTH, MAX_URL_LENGTH, UUID_PATTERN, authorDetailsSchema, authorSchema, collectionSchema, AssetFile as default, fileSchemaUrlSchema };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Matches a lowercase UUID */
|
|
2
|
+
export declare const UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
3
|
+
/** Matches a decentralised identifier, e.g. `did:plc:abc123` */
|
|
4
|
+
export declare const DID_PATTERN = "^did:[a-z]+:[a-zA-Z0-9._:%-]+$";
|
|
5
|
+
/** Longest author name or handle (characters) */
|
|
6
|
+
export declare const MAX_AUTHOR_TEXT_LENGTH = 100;
|
|
7
|
+
/** Longest URL in an asset (characters) */
|
|
8
|
+
export declare const MAX_URL_LENGTH = 2048;
|
|
9
|
+
/** Who made an asset or document */
|
|
10
|
+
export interface AssetAuthorDetails {
|
|
11
|
+
/** Their decentralised identifier, e.g. an ATProto account's `did:plc:…` */
|
|
12
|
+
did?: string;
|
|
13
|
+
/** Their name */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Their handle or username, which tells their assets apart from others with the same name */
|
|
16
|
+
handle?: string;
|
|
17
|
+
/** Their email */
|
|
18
|
+
email?: string;
|
|
19
|
+
/** Their website */
|
|
20
|
+
homepage?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Who made an asset: their name, or their details */
|
|
23
|
+
export type AssetAuthor = string | AssetAuthorDetails;
|
|
24
|
+
/** Schema of an author's details */
|
|
25
|
+
export declare const authorDetailsSchema: {
|
|
26
|
+
readonly type: "object";
|
|
27
|
+
readonly additionalProperties: false;
|
|
28
|
+
readonly required: readonly ["name"];
|
|
29
|
+
readonly properties: {
|
|
30
|
+
readonly did: {
|
|
31
|
+
readonly type: "string";
|
|
32
|
+
readonly maxLength: 2048;
|
|
33
|
+
readonly pattern: "^did:[a-z]+:[a-zA-Z0-9._:%-]+$";
|
|
34
|
+
};
|
|
35
|
+
readonly name: {
|
|
36
|
+
readonly type: "string";
|
|
37
|
+
readonly minLength: 1;
|
|
38
|
+
readonly maxLength: 100;
|
|
39
|
+
};
|
|
40
|
+
readonly handle: {
|
|
41
|
+
readonly type: "string";
|
|
42
|
+
readonly minLength: 1;
|
|
43
|
+
readonly maxLength: 100;
|
|
44
|
+
};
|
|
45
|
+
readonly email: {
|
|
46
|
+
readonly type: "string";
|
|
47
|
+
readonly maxLength: 254;
|
|
48
|
+
};
|
|
49
|
+
readonly homepage: {
|
|
50
|
+
readonly type: "string";
|
|
51
|
+
readonly maxLength: 2048;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** Schema of an asset's author: a name, or their details */
|
|
56
|
+
export declare const authorSchema: {
|
|
57
|
+
readonly anyOf: readonly [{
|
|
58
|
+
readonly type: "string";
|
|
59
|
+
readonly minLength: 1;
|
|
60
|
+
readonly maxLength: 100;
|
|
61
|
+
}, {
|
|
62
|
+
readonly type: "object";
|
|
63
|
+
readonly additionalProperties: false;
|
|
64
|
+
readonly required: readonly ["name"];
|
|
65
|
+
readonly properties: {
|
|
66
|
+
readonly did: {
|
|
67
|
+
readonly type: "string";
|
|
68
|
+
readonly maxLength: 2048;
|
|
69
|
+
readonly pattern: "^did:[a-z]+:[a-zA-Z0-9._:%-]+$";
|
|
70
|
+
};
|
|
71
|
+
readonly name: {
|
|
72
|
+
readonly type: "string";
|
|
73
|
+
readonly minLength: 1;
|
|
74
|
+
readonly maxLength: 100;
|
|
75
|
+
};
|
|
76
|
+
readonly handle: {
|
|
77
|
+
readonly type: "string";
|
|
78
|
+
readonly minLength: 1;
|
|
79
|
+
readonly maxLength: 100;
|
|
80
|
+
};
|
|
81
|
+
readonly email: {
|
|
82
|
+
readonly type: "string";
|
|
83
|
+
readonly maxLength: 254;
|
|
84
|
+
};
|
|
85
|
+
readonly homepage: {
|
|
86
|
+
readonly type: "string";
|
|
87
|
+
readonly maxLength: 2048;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
}];
|
|
91
|
+
};
|
|
92
|
+
/** Schema of the `$schema` a file names (any version: the app checks which ones it reads) */
|
|
93
|
+
export declare const fileSchemaUrlSchema: {
|
|
94
|
+
readonly type: "string";
|
|
95
|
+
readonly minLength: 1;
|
|
96
|
+
readonly maxLength: 2048;
|
|
97
|
+
};
|
|
98
|
+
/** A collection file: what users export and import one or more assets in */
|
|
99
|
+
export interface AssetCollectionFile<T> {
|
|
100
|
+
/** URL of the collection's schema, which names its version */
|
|
101
|
+
$schema: string;
|
|
102
|
+
/** The kind of collection, e.g. `PaintCanvas/BrushConfigCollection` */
|
|
103
|
+
"@type": string;
|
|
104
|
+
/** The assets */
|
|
105
|
+
items: T[];
|
|
106
|
+
}
|
|
107
|
+
/** What a collection schema is made from */
|
|
108
|
+
export interface CollectionSchemaOptions {
|
|
109
|
+
/** The collection schema's name, e.g. `brush-preset-collection` */
|
|
110
|
+
name: string;
|
|
111
|
+
/** The collection schema's version */
|
|
112
|
+
version: string;
|
|
113
|
+
/** The collection's `@type` */
|
|
114
|
+
type: string;
|
|
115
|
+
/** `$id` of each item's schema */
|
|
116
|
+
item: string;
|
|
117
|
+
/** Most items in one file */
|
|
118
|
+
maxItems: number;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The schema of a collection file holding assets of one kind.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* collectionSchema({ name: "brush-preset-collection", version: "1.0.0", type: "PaintCanvas/BrushConfigCollection",
|
|
125
|
+
* item: brushPresetSchema.$id, maxItems: 1000 });
|
|
126
|
+
*/
|
|
127
|
+
export declare const collectionSchema: ({ name, version, type, item, maxItems, }: CollectionSchemaOptions) => {
|
|
128
|
+
$schema: string;
|
|
129
|
+
$id: string;
|
|
130
|
+
"@type": string;
|
|
131
|
+
type: string;
|
|
132
|
+
additionalProperties: boolean;
|
|
133
|
+
required: string[];
|
|
134
|
+
properties: {
|
|
135
|
+
$schema: {
|
|
136
|
+
readonly type: "string";
|
|
137
|
+
readonly minLength: 1;
|
|
138
|
+
readonly maxLength: 2048;
|
|
139
|
+
};
|
|
140
|
+
"@type": {
|
|
141
|
+
const: string;
|
|
142
|
+
};
|
|
143
|
+
items: {
|
|
144
|
+
type: string;
|
|
145
|
+
maxItems: number;
|
|
146
|
+
items: {
|
|
147
|
+
$ref: string;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SCHEMA_DIALECT, schemaUrl } from "../schemas/index.js";
|
|
2
|
+
// src/lib/assets/schema.ts
|
|
3
|
+
/** Matches a lowercase UUID */
|
|
4
|
+
var UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
5
|
+
/** Matches a decentralised identifier, e.g. `did:plc:abc123` */
|
|
6
|
+
var DID_PATTERN = "^did:[a-z]+:[a-zA-Z0-9._:%-]+$";
|
|
7
|
+
/** Longest author name or handle (characters) */
|
|
8
|
+
var MAX_AUTHOR_TEXT_LENGTH = 100;
|
|
9
|
+
/** Longest URL in an asset (characters) */
|
|
10
|
+
var MAX_URL_LENGTH = 2048;
|
|
11
|
+
/** Short text, such as an author's name */
|
|
12
|
+
var authorText = {
|
|
13
|
+
type: "string",
|
|
14
|
+
minLength: 1,
|
|
15
|
+
maxLength: 100
|
|
16
|
+
};
|
|
17
|
+
/** Schema of an author's details */
|
|
18
|
+
var authorDetailsSchema = {
|
|
19
|
+
type: "object",
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
required: ["name"],
|
|
22
|
+
properties: {
|
|
23
|
+
did: {
|
|
24
|
+
type: "string",
|
|
25
|
+
maxLength: MAX_URL_LENGTH,
|
|
26
|
+
pattern: DID_PATTERN
|
|
27
|
+
},
|
|
28
|
+
name: authorText,
|
|
29
|
+
handle: authorText,
|
|
30
|
+
email: {
|
|
31
|
+
type: "string",
|
|
32
|
+
maxLength: 254
|
|
33
|
+
},
|
|
34
|
+
homepage: {
|
|
35
|
+
type: "string",
|
|
36
|
+
maxLength: MAX_URL_LENGTH
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** Schema of an asset's author: a name, or their details */
|
|
41
|
+
var authorSchema = { anyOf: [authorText, authorDetailsSchema] };
|
|
42
|
+
/** Schema of the `$schema` a file names (any version: the app checks which ones it reads) */
|
|
43
|
+
var fileSchemaUrlSchema = {
|
|
44
|
+
type: "string",
|
|
45
|
+
minLength: 1,
|
|
46
|
+
maxLength: MAX_URL_LENGTH
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The schema of a collection file holding assets of one kind.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* collectionSchema({ name: "brush-preset-collection", version: "1.0.0", type: "PaintCanvas/BrushConfigCollection",
|
|
53
|
+
* item: brushPresetSchema.$id, maxItems: 1000 });
|
|
54
|
+
*/
|
|
55
|
+
var collectionSchema = ({ name, version, type, item, maxItems }) => ({
|
|
56
|
+
$schema: SCHEMA_DIALECT,
|
|
57
|
+
$id: schemaUrl(name, version),
|
|
58
|
+
"@type": type,
|
|
59
|
+
type: "object",
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
required: [
|
|
62
|
+
"$schema",
|
|
63
|
+
"@type",
|
|
64
|
+
"items"
|
|
65
|
+
],
|
|
66
|
+
properties: {
|
|
67
|
+
$schema: fileSchemaUrlSchema,
|
|
68
|
+
"@type": { const: type },
|
|
69
|
+
items: {
|
|
70
|
+
type: "array",
|
|
71
|
+
maxItems,
|
|
72
|
+
items: { $ref: item }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
export { DID_PATTERN, MAX_AUTHOR_TEXT_LENGTH, MAX_URL_LENGTH, UUID_PATTERN, authorDetailsSchema, authorSchema, collectionSchema, fileSchemaUrlSchema };
|
|
@@ -1,32 +1,11 @@
|
|
|
1
|
+
import AssetFile from "../../assets";
|
|
1
2
|
import type { BrushConfig } from "../types";
|
|
2
|
-
/** Messages shown when importing or exporting presets fails */
|
|
3
|
-
export declare const PRESET_FILE_ERRORS: {
|
|
4
|
-
readonly incompatible: "Brush preset JSON file not compatible";
|
|
5
|
-
readonly read: "Failed to read brush preset JSON file";
|
|
6
|
-
readonly export: "Error exporting brush preset JSON file";
|
|
7
|
-
readonly empty: "No brush presets to export";
|
|
8
|
-
};
|
|
9
|
-
/** Importing or exporting presets failed, with a message for the user */
|
|
10
|
-
export declare class PresetFileError extends Error {
|
|
11
|
-
}
|
|
12
3
|
/**
|
|
13
|
-
*
|
|
4
|
+
* Reads and writes brush preset collection files. Files from before versioned schemas (`format`, `version: 1` and
|
|
5
|
+
* `presets`) are migrated.
|
|
14
6
|
*
|
|
15
7
|
* @example
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* const presets = brushPresetFile.parse(await file.text()); // throws AssetFileError
|
|
9
|
+
* const json = brushPresetFile.write(app.customPresets.presets);
|
|
18
10
|
*/
|
|
19
|
-
export declare const
|
|
20
|
-
/**
|
|
21
|
-
* Write presets to a brush preset file (JSON) of a format (see `PaintCanvasNames.presetFileFormat`). Throws a
|
|
22
|
-
* `PresetFileError` if there are none, or one can't be exported (e.g. its shape isn't a PNG data URL).
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* exportPresetFile([myPreset]); // '{ "format": "paintcanvas-brush-presets", "version": 1, "presets": [...] }'
|
|
26
|
-
*/
|
|
27
|
-
export declare const exportPresetFile: (presets: BrushConfig[], format?: string) => string;
|
|
28
|
-
/**
|
|
29
|
-
* Read the presets in a brush preset file of one of the given formats (PaintCanvas's by default).
|
|
30
|
-
* Throws a `PresetFileError` if it isn't valid JSON or a compatible file.
|
|
31
|
-
*/
|
|
32
|
-
export declare const parsePresetFile: (text: string, formats?: readonly string[]) => BrushConfig[];
|
|
11
|
+
export declare const brushPresetFile: AssetFile<BrushConfig>;
|
|
@@ -1,55 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import AssetFile from "../../assets/index.js";
|
|
2
|
+
import { BRUSH_PRESET_COLLECTION_SCHEMA_VERSION, BRUSH_PRESET_COLLECTION_TYPE } from "../../presets/schema.js";
|
|
3
|
+
import "../../presets/index.js";
|
|
4
|
+
import { validateBrushPresetCollection } from "../../validation/validators.generated.js";
|
|
3
5
|
// src/lib/brushes/presetFile/index.ts
|
|
4
|
-
/** Messages shown when importing or exporting presets fails */
|
|
5
|
-
var PRESET_FILE_ERRORS = {
|
|
6
|
-
incompatible: "Brush preset JSON file not compatible",
|
|
7
|
-
read: "Failed to read brush preset JSON file",
|
|
8
|
-
export: "Error exporting brush preset JSON file",
|
|
9
|
-
empty: "No brush presets to export"
|
|
10
|
-
};
|
|
11
|
-
/** Importing or exporting presets failed, with a message for the user */
|
|
12
|
-
var PresetFileError = class extends Error {};
|
|
13
6
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* presetFileName(new Date(2026, 9, 24)); // "paintcanvas-brush-presets-2026-10-24.json"
|
|
18
|
-
* presetFileName(new Date(2026, 9, 24), "myapp"); // "myapp-brush-presets-2026-10-24.json"
|
|
7
|
+
* A preset from a brush preset file from before versioned schemas, with its author's `website` renamed `homepage`.
|
|
19
8
|
*/
|
|
20
|
-
var
|
|
21
|
-
const
|
|
22
|
-
|
|
9
|
+
var migrateLegacyPreset = (preset) => {
|
|
10
|
+
const author = preset?.author;
|
|
11
|
+
if (typeof author !== "object" || author === null || !("website" in author)) return preset;
|
|
12
|
+
const { website, ...rest } = author;
|
|
13
|
+
return {
|
|
14
|
+
...preset,
|
|
15
|
+
author: {
|
|
16
|
+
...rest,
|
|
17
|
+
homepage: website
|
|
18
|
+
}
|
|
19
|
+
};
|
|
23
20
|
};
|
|
24
21
|
/**
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
22
|
+
* Reads and writes brush preset collection files. Files from before versioned schemas (`format`, `version: 1` and
|
|
23
|
+
* `presets`) are migrated.
|
|
27
24
|
*
|
|
28
25
|
* @example
|
|
29
|
-
*
|
|
26
|
+
* const presets = brushPresetFile.parse(await file.text()); // throws AssetFileError
|
|
27
|
+
* const json = brushPresetFile.write(app.customPresets.presets);
|
|
30
28
|
*/
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let data;
|
|
46
|
-
try {
|
|
47
|
-
data = JSON.parse(text);
|
|
48
|
-
} catch {
|
|
49
|
-
throw new PresetFileError(PRESET_FILE_ERRORS.incompatible);
|
|
50
|
-
}
|
|
51
|
-
const result = isValidBrushPresetFile(data);
|
|
52
|
-
if (!result.valid || !formats.includes(result.value.format)) throw new PresetFileError(PRESET_FILE_ERRORS.incompatible);
|
|
53
|
-
return result.value.presets;
|
|
54
|
-
};
|
|
55
|
-
export { PRESET_FILE_ERRORS, PresetFileError, exportPresetFile, parsePresetFile, presetFileName };
|
|
29
|
+
var brushPresetFile = new AssetFile({
|
|
30
|
+
schema: "brush-preset-collection",
|
|
31
|
+
version: BRUSH_PRESET_COLLECTION_SCHEMA_VERSION,
|
|
32
|
+
type: BRUSH_PRESET_COLLECTION_TYPE,
|
|
33
|
+
validate: validateBrushPresetCollection,
|
|
34
|
+
fileName: "brush-presets",
|
|
35
|
+
legacyVersion: (data) => typeof data.format === "string" && data.version === 1 ? "legacy-1" : void 0,
|
|
36
|
+
migrations: [{
|
|
37
|
+
from: "legacy-1",
|
|
38
|
+
to: "1.0.0",
|
|
39
|
+
migrate: (data) => ({ items: Array.isArray(data.presets) ? data.presets.map(migrateLegacyPreset) : data.presets })
|
|
40
|
+
}]
|
|
41
|
+
});
|
|
42
|
+
export { brushPresetFile };
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
import type { ProfilePoint } from "../../profiles";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
name: string;
|
|
5
|
-
/** Author's handle or username, which tells their presets apart from others with the same name */
|
|
6
|
-
handle?: string;
|
|
7
|
-
/** Author's email */
|
|
8
|
-
email?: string;
|
|
9
|
-
/** Author's website */
|
|
10
|
-
website?: string;
|
|
11
|
-
}
|
|
12
|
-
export type AssetAuthor = string | AssetAuthorDetails;
|
|
2
|
+
import type { AssetAuthor } from "../../assets/schema";
|
|
3
|
+
export type { AssetAuthor, AssetAuthorDetails } from "../../assets/schema";
|
|
13
4
|
/** Which colour settings a brush varies */
|
|
14
5
|
export declare enum ColourMode {
|
|
15
6
|
HSL = 0,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Asset, AssetStoreLike } from "../../storage";
|
|
2
|
+
/** Event dispatched when loading or saving assets fails (detail: the error) */
|
|
3
|
+
export declare const ASSET_SYNC_ERROR_EVENT = "error";
|
|
4
|
+
/** Assets kept in memory that `AssetSync` keeps in a store */
|
|
5
|
+
export interface SyncedAssets<T extends Asset> extends EventTarget {
|
|
6
|
+
/** The assets, in order (dispatching `change` when they change) */
|
|
7
|
+
readonly items: readonly T[];
|
|
8
|
+
/** Add assets loaded from the store (untrusted: leave out invalid ones), replacing ones with the same IDs */
|
|
9
|
+
merge(items: unknown[]): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Keeps assets held in memory (e.g. profile graphs) in a store: the saved assets are added when it starts, assets only
|
|
13
|
+
* in memory are saved (e.g. ones restored from older app settings), then each change saves the assets that changed and
|
|
14
|
+
* deletes the ones that are gone.
|
|
15
|
+
*
|
|
16
|
+
* Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const sync = new AssetSync(profiles, new IndexedDbAssetStore("profiles"));
|
|
20
|
+
* await sync.ready;
|
|
21
|
+
* sync.stop();
|
|
22
|
+
*/
|
|
23
|
+
export default class AssetSync<T extends Asset> extends EventTarget {
|
|
24
|
+
readonly assets: SyncedAssets<T>;
|
|
25
|
+
readonly store: AssetStoreLike<T>;
|
|
26
|
+
/** Resolves once the saved assets are loaded and any only in memory are saved */
|
|
27
|
+
readonly ready: Promise<void>;
|
|
28
|
+
/** Each asset as last saved, as JSON, by ID */
|
|
29
|
+
private saved;
|
|
30
|
+
private stopped;
|
|
31
|
+
/** Saving, one change after another */
|
|
32
|
+
private saving;
|
|
33
|
+
private readonly onChange;
|
|
34
|
+
constructor(assets: SyncedAssets<T>, store: AssetStoreLike<T>);
|
|
35
|
+
/**
|
|
36
|
+
* Stop keeping changes (ones already being saved finish).
|
|
37
|
+
*/
|
|
38
|
+
stop(): void;
|
|
39
|
+
/** Load the saved assets, then save the ones only in memory and listen for changes */
|
|
40
|
+
private start;
|
|
41
|
+
/** Save the assets that changed since they were last saved, and delete the ones that are gone */
|
|
42
|
+
private save;
|
|
43
|
+
/** Tell listeners loading or saving failed */
|
|
44
|
+
private fail;
|
|
45
|
+
}
|