@lvl99dotcom/paintcanvas 0.5.0 → 0.7.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 +2 -2
- package/dist/lib/brushes/configRanges/index.d.ts +17 -0
- package/dist/lib/brushes/configRanges/index.js +31 -1
- package/dist/lib/brushes/params/index.d.ts +2 -1
- package/dist/lib/brushes/params/index.js +7 -4
- package/dist/lib/brushes/types/index.d.ts +8 -0
- package/dist/lib/classes/Layer/index.d.ts +10 -0
- package/dist/lib/classes/Layer/index.js +15 -1
- package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
- package/dist/lib/classes/LayerAutosave/index.js +30 -43
- package/dist/lib/classes/LayerStack/index.d.ts +15 -0
- package/dist/lib/classes/LayerStack/index.js +26 -2
- package/dist/lib/classes/PaintCanvas/index.d.ts +36 -5
- package/dist/lib/classes/PaintCanvas/index.js +45 -14
- package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
- package/dist/lib/classes/PngEncoder/index.js +104 -0
- package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
- package/dist/lib/classes/ProfileGraphs/index.js +148 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
- package/dist/lib/classes/SettingsAutosave/index.js +2 -1
- package/dist/lib/classes/UiSettings/index.d.ts +24 -2
- package/dist/lib/classes/UiSettings/index.js +32 -7
- package/dist/lib/messages/schema.d.ts +720 -0
- package/dist/lib/presets/schema.d.ts +360 -0
- package/dist/lib/presets/schema.js +8 -1
- package/dist/lib/profiles/index.d.ts +61 -0
- package/dist/lib/profiles/index.js +131 -0
- package/dist/lib/profiles/schema.d.ts +73 -0
- package/dist/lib/profiles/schema.js +53 -0
- package/dist/lib/rendering/sample/index.d.ts +11 -0
- package/dist/lib/rendering/sample/index.js +36 -5
- package/dist/lib/settings/index.d.ts +2 -1
- package/dist/lib/settings/index.js +4 -1
- package/dist/lib/settings/schema.d.ts +53 -1
- package/dist/lib/settings/schema.js +9 -2
- package/dist/lib/storage/index.d.ts +26 -0
- package/dist/lib/storage/index.js +38 -1
- package/dist/lib/validation/validators.generated.js +13061 -3675
- package/dist/lib/version/index.d.ts +25 -0
- package/dist/lib/version/index.js +35 -0
- package/dist/locales/en-GB.js +35 -0
- package/dist/locales/en-US.js +35 -0
- package/dist/locales/fr.js +35 -0
- package/dist/locales/ja.js +35 -0
- package/dist/locales/ko.js +35 -0
- package/dist/locales/zh-Hans.js +35 -0
- package/dist/locales/zh-Hant.js +35 -0
- package/dist/package.js +1 -1
- package/dist/ui/ask/index.d.ts +48 -0
- package/dist/ui/ask/index.js +67 -0
- package/dist/ui/base/index.d.ts +8 -1
- package/dist/ui/base/index.js +19 -4
- package/dist/ui/elements/PaintCanvas/index.d.ts +13 -3
- package/dist/ui/elements/PaintCanvas/index.js +25 -6
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +15 -7
- package/dist/ui/elements/PcAppSettings/index.js +53 -62
- package/dist/ui/elements/PcBrushConfig/index.d.ts +26 -14
- package/dist/ui/elements/PcBrushConfig/index.js +214 -145
- package/dist/ui/elements/PcBrushConfig/style.js +1 -1
- package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
- package/dist/ui/elements/PcBrushTest/index.js +15 -9
- package/dist/ui/elements/PcBrushTest/style.js +1 -1
- package/dist/ui/elements/PcButton/index.d.ts +12 -3
- package/dist/ui/elements/PcButton/index.js +18 -10
- package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
- package/dist/ui/elements/PcButtonMenu/index.js +125 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +15 -1
- package/dist/ui/elements/PcCanvas/index.js +42 -19
- package/dist/ui/elements/PcCanvas/style.js +1 -1
- package/dist/ui/elements/PcClipboardTools/index.d.ts +11 -7
- package/dist/ui/elements/PcClipboardTools/index.js +30 -29
- package/dist/ui/elements/PcDialog/index.d.ts +46 -10
- package/dist/ui/elements/PcDialog/index.js +102 -51
- package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
- package/dist/ui/elements/PcDialogWindow/index.js +385 -0
- package/dist/ui/elements/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 +3 -2
- package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
- package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
- package/dist/ui/elements/PcPanelColours/index.js +2 -2
- package/dist/ui/elements/PcPanelColours/style.js +1 -1
- package/dist/ui/elements/PcPanelLayers/index.js +1 -1
- package/dist/ui/elements/PcPanelLayers/style.js +1 -1
- package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
- package/dist/ui/elements/PcProfileEditor/index.js +259 -0
- package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
- package/dist/ui/elements/PcProfileGraph/index.js +317 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
- package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
- package/dist/ui/elements/PcSliderRange/index.js +308 -0
- package/dist/ui/elements/PcStats/index.d.ts +19 -4
- package/dist/ui/elements/PcStats/index.js +29 -13
- 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 +14 -14
- package/dist/ui/elements/PcVersion/index.d.ts +12 -0
- package/dist/ui/elements/PcVersion/index.js +43 -0
- package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
- package/dist/ui/icons/index.d.ts +1 -1
- package/dist/ui/icons/index.js +2 -0
- package/dist/ui/index.d.ts +9 -2
- package/dist/ui/index.js +9 -2
- package/dist/ui/layout/index.d.ts +2 -2
- package/dist/ui/layout/index.js +2 -2
- package/dist/ui/styles/index.d.ts +21 -1
- package/dist/ui/styles/index.js +73 -14
- package/locales/en-GB.json +35 -0
- package/locales/en-US.json +35 -0
- package/locales/fr.json +35 -0
- package/locales/ja.json +35 -0
- package/locales/ko.json +35 -0
- package/locales/zh-Hans.json +35 -0
- package/locales/zh-Hant.json +35 -0
- package/package.json +25 -23
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.
|
|
@@ -23,6 +23,13 @@ export declare const PARAM_LIMITS: Record<VaryingParam, {
|
|
|
23
23
|
min: number;
|
|
24
24
|
max: number;
|
|
25
25
|
}>;
|
|
26
|
+
/**
|
|
27
|
+
* The config key of a setting's variation.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* configKey("width"); // "widthConfig"
|
|
31
|
+
*/
|
|
32
|
+
export declare const configKey: (param: VaryingParam) => `${VaryingParam}Config`;
|
|
26
33
|
/**
|
|
27
34
|
* Read a setting as the configuration window shows it. When it varies, its range comes from the first source turned
|
|
28
35
|
* on: scale settings (width, height, opacity) go from `value × min factor` to the value; rotation goes from the angle
|
|
@@ -50,3 +57,13 @@ export declare const writeRange: (config: BrushConfig, param: VaryingParam, rang
|
|
|
50
57
|
* when it is the first, a starting range (half to full size, the angle itself, or no scatter).
|
|
51
58
|
*/
|
|
52
59
|
export declare const toggleSource: (config: BrushConfig, param: VaryingParam, source: VariationSource, on: boolean) => Partial<BrushConfig>;
|
|
60
|
+
/**
|
|
61
|
+
* Make a setting vary with one source only (or none, with `undefined`), as config changes. Switching from another
|
|
62
|
+
* source keeps the setting's range; the first source takes a starting range (see `toggleSource`). The setting's profile
|
|
63
|
+
* graph is kept.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* selectSource({ id: "a", widthConfig: { usePressure: true, minPressure: 0.5 } }, "width", "tilt");
|
|
67
|
+
* // { widthConfig: { usePressure: false, useTilt: true, minTilt: 0.5, maxTilt: 1, useRandomise: false, … } }
|
|
68
|
+
*/
|
|
69
|
+
export declare const selectSource: (config: BrushConfig, param: VaryingParam, source: VariationSource | undefined) => Partial<BrushConfig>;
|
|
@@ -65,6 +65,12 @@ var SOURCE_DEFAULTS = {
|
|
|
65
65
|
tilt: [0, 1],
|
|
66
66
|
randomise: [1, 1]
|
|
67
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* The config key of a setting's variation.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* configKey("width"); // "widthConfig"
|
|
73
|
+
*/
|
|
68
74
|
var configKey = (param) => `${param}Config`;
|
|
69
75
|
var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
70
76
|
/** A setting's value as stored (alpha as 0–1) */
|
|
@@ -203,4 +209,28 @@ var toggleSource = (config, param, source, on) => {
|
|
|
203
209
|
});
|
|
204
210
|
return { [key]: variation };
|
|
205
211
|
};
|
|
206
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Make a setting vary with one source only (or none, with `undefined`), as config changes. Switching from another
|
|
214
|
+
* source keeps the setting's range; the first source takes a starting range (see `toggleSource`). The setting's profile
|
|
215
|
+
* graph is kept.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* selectSource({ id: "a", widthConfig: { usePressure: true, minPressure: 0.5 } }, "width", "tilt");
|
|
219
|
+
* // { widthConfig: { usePressure: false, useTilt: true, minTilt: 0.5, maxTilt: 1, useRandomise: false, … } }
|
|
220
|
+
*/
|
|
221
|
+
var selectSource = (config, param, source) => {
|
|
222
|
+
const key = configKey(param);
|
|
223
|
+
const previous = VARIATION_SOURCES.find((each) => readRange(config, param).sources[each]);
|
|
224
|
+
if (source && !previous) return toggleSource(config, param, source, true);
|
|
225
|
+
const variation = { ...config[key] };
|
|
226
|
+
for (const each of VARIATION_SOURCES) variation[SOURCE_KEYS[each][0]] = each === source;
|
|
227
|
+
if (source && previous && source !== previous) {
|
|
228
|
+
const [, fromMin, fromMax] = SOURCE_KEYS[previous];
|
|
229
|
+
const [, toMin, toMax] = SOURCE_KEYS[source];
|
|
230
|
+
const [minDefault, maxDefault] = SOURCE_DEFAULTS[previous];
|
|
231
|
+
variation[toMin] = variation[fromMin] ?? minDefault;
|
|
232
|
+
variation[toMax] = variation[fromMax] ?? maxDefault;
|
|
233
|
+
}
|
|
234
|
+
return { [key]: variation };
|
|
235
|
+
};
|
|
236
|
+
export { PARAM_LIMITS, VARIATION_SOURCES, configKey, readRange, selectSource, toggleSource, writeRange };
|
|
@@ -49,7 +49,8 @@ export declare const tiltAmount: (tiltX?: number, tiltY?: number) => number;
|
|
|
49
49
|
export declare const tiltDirection: (tiltX?: number, tiltY?: number) => number;
|
|
50
50
|
/**
|
|
51
51
|
* Combine a parameter's randomise, pressure and tilt factors into one factor (they multiply). Returns 1 when nothing is enabled.
|
|
52
|
-
* Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`).
|
|
52
|
+
* Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`). Each input goes
|
|
53
|
+
* through the parameter's profile graph (see `applyProfile`) before it sets the factor between its min and max.
|
|
53
54
|
*
|
|
54
55
|
* @example
|
|
55
56
|
* variationFactor({ useRandomise: true, minRandomise: 0.5, maxRandomise: 1 }, { isPen: false }, () => 0); // 0.5
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyProfile } from "../../profiles/index.js";
|
|
1
2
|
// src/lib/brushes/params/index.ts
|
|
2
3
|
/**
|
|
3
4
|
* Linear interpolation between two values.
|
|
@@ -27,22 +28,24 @@ var tiltAmount = (tiltX = 0, tiltY = 0) => clamp01(Math.hypot(tiltX, tiltY) / 90
|
|
|
27
28
|
var tiltDirection = (tiltX = 0, tiltY = 0) => Math.atan2(tiltY, tiltX) * 180 / Math.PI;
|
|
28
29
|
/**
|
|
29
30
|
* Combine a parameter's randomise, pressure and tilt factors into one factor (they multiply). Returns 1 when nothing is enabled.
|
|
30
|
-
* Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`).
|
|
31
|
+
* Pressure and tilt only apply to pens; for other devices they count as full (`maxPressure`/`maxTilt`). Each input goes
|
|
32
|
+
* through the parameter's profile graph (see `applyProfile`) before it sets the factor between its min and max.
|
|
31
33
|
*
|
|
32
34
|
* @example
|
|
33
35
|
* variationFactor({ useRandomise: true, minRandomise: 0.5, maxRandomise: 1 }, { isPen: false }, () => 0); // 0.5
|
|
34
36
|
*/
|
|
35
37
|
var variationFactor = (config, input, random = Math.random) => {
|
|
36
38
|
if (!config) return 1;
|
|
39
|
+
const { profile } = config;
|
|
37
40
|
let factor = 1;
|
|
38
|
-
if (config.useRandomise) factor *= lerp(config.minRandomise ?? 1, config.maxRandomise ?? 1, random());
|
|
41
|
+
if (config.useRandomise) factor *= lerp(config.minRandomise ?? 1, config.maxRandomise ?? 1, applyProfile(profile, random()));
|
|
39
42
|
if (config.usePressure) {
|
|
40
43
|
const pressure = input.isPen ? clamp01(input.pressure ?? 1) : 1;
|
|
41
|
-
factor *= lerp(config.minPressure ?? 0, config.maxPressure ?? 1, pressure);
|
|
44
|
+
factor *= lerp(config.minPressure ?? 0, config.maxPressure ?? 1, applyProfile(profile, pressure));
|
|
42
45
|
}
|
|
43
46
|
if (config.useTilt) {
|
|
44
47
|
const tilt = input.isPen ? tiltAmount(input.tiltX, input.tiltY) : 1;
|
|
45
|
-
factor *= lerp(config.minTilt ?? 0, config.maxTilt ?? 1, tilt);
|
|
48
|
+
factor *= lerp(config.minTilt ?? 0, config.maxTilt ?? 1, applyProfile(profile, tilt));
|
|
46
49
|
}
|
|
47
50
|
return factor;
|
|
48
51
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProfilePoint } from "../../profiles";
|
|
1
2
|
export interface AssetAuthorDetails {
|
|
2
3
|
/** Author's name */
|
|
3
4
|
name: string;
|
|
@@ -94,6 +95,13 @@ export interface BrushParamConfig {
|
|
|
94
95
|
* @default 1
|
|
95
96
|
*/
|
|
96
97
|
maxTilt?: number;
|
|
98
|
+
/**
|
|
99
|
+
* The profile graph the input (pressure, tilt or the random number) goes through before setting the factor between
|
|
100
|
+
* its min and max, e.g. so a light touch goes most of the way (see `applyProfile`). A straight line by default.
|
|
101
|
+
*/
|
|
102
|
+
profile?: ProfilePoint[];
|
|
103
|
+
/** The ID of the profile graph `profile` came from (a preset's or one of the user's), to show its name */
|
|
104
|
+
profileId?: string;
|
|
97
105
|
}
|
|
98
106
|
export interface BrushConfig {
|
|
99
107
|
/** Unique ID */
|
|
@@ -29,6 +29,16 @@ export type CanvasFactory = (width: number, height: number) => OffscreenCanvas;
|
|
|
29
29
|
* Create an `OffscreenCanvas` of the given size.
|
|
30
30
|
*/
|
|
31
31
|
export declare const createOffscreenCanvas: CanvasFactory;
|
|
32
|
+
/**
|
|
33
|
+
* Create an `OffscreenCanvas` whose pixels are kept in regular memory rather than on the GPU (its 2D context is made
|
|
34
|
+
* with `willReadFrequently`). Reading its pixels, encoding it and copying it are then plain memory copies, which on
|
|
35
|
+
* phones avoids long stalls waiting for the GPU (up to 15 seconds were seen on an iPhone), at the cost of drawing on
|
|
36
|
+
* the CPU. PaintCanvas makes its canvases this way by default.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const canvas = createMemoryCanvas(2000, 2000);
|
|
40
|
+
*/
|
|
41
|
+
export declare const createMemoryCanvas: CanvasFactory;
|
|
32
42
|
/**
|
|
33
43
|
* Clamp an opacity value to 0–1.
|
|
34
44
|
*
|
|
@@ -16,6 +16,20 @@ var MAX_LAYER_NAME_LENGTH = 64;
|
|
|
16
16
|
*/
|
|
17
17
|
var createOffscreenCanvas = (width, height) => new OffscreenCanvas(width, height);
|
|
18
18
|
/**
|
|
19
|
+
* Create an `OffscreenCanvas` whose pixels are kept in regular memory rather than on the GPU (its 2D context is made
|
|
20
|
+
* with `willReadFrequently`). Reading its pixels, encoding it and copying it are then plain memory copies, which on
|
|
21
|
+
* phones avoids long stalls waiting for the GPU (up to 15 seconds were seen on an iPhone), at the cost of drawing on
|
|
22
|
+
* the CPU. PaintCanvas makes its canvases this way by default.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const canvas = createMemoryCanvas(2000, 2000);
|
|
26
|
+
*/
|
|
27
|
+
var createMemoryCanvas = (width, height) => {
|
|
28
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
29
|
+
canvas.getContext("2d", { willReadFrequently: true });
|
|
30
|
+
return canvas;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
19
33
|
* Clamp an opacity value to 0–1.
|
|
20
34
|
*
|
|
21
35
|
* @example
|
|
@@ -108,4 +122,4 @@ var Layer = class {
|
|
|
108
122
|
};
|
|
109
123
|
}
|
|
110
124
|
};
|
|
111
|
-
export { BlendMode, MAX_LAYER_NAME_LENGTH, clampOpacity, createOffscreenCanvas, Layer as default };
|
|
125
|
+
export { BlendMode, MAX_LAYER_NAME_LENGTH, clampOpacity, createMemoryCanvas, createOffscreenCanvas, Layer as default };
|
|
@@ -34,7 +34,7 @@ export interface AutosaveOptions {
|
|
|
34
34
|
locks?: LockManager | null;
|
|
35
35
|
/** Tells when the page is hidden or shown again (the page's document by default) */
|
|
36
36
|
document?: Pick<Document, "visibilityState" | "addEventListener" | "removeEventListener"> | null;
|
|
37
|
-
/** Turns a layer's canvas into a PNG (replaceable for tests) */
|
|
37
|
+
/** Turns a layer's canvas into a PNG (in a background worker by default; replaceable for tests) */
|
|
38
38
|
encode?: (canvas: OffscreenCanvas) => Promise<Blob>;
|
|
39
39
|
/** Turns a saved PNG into an image to draw (replaceable for tests) */
|
|
40
40
|
decode?: (image: Blob) => Promise<CanvasImageSource>;
|
|
@@ -44,8 +44,8 @@ export interface AutosaveOptions {
|
|
|
44
44
|
/**
|
|
45
45
|
* Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
|
|
46
46
|
* starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
|
|
47
|
-
* Only layers whose pixels changed are encoded and saved again
|
|
48
|
-
* which.
|
|
47
|
+
* Only layers whose pixels changed are encoded and saved again: the layers' change counts tell which (see
|
|
48
|
+
* `LayerStack.pixelVersion`), without reading their pixels, which is slow on phones.
|
|
49
49
|
*
|
|
50
50
|
* Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
|
|
51
51
|
* (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
|
|
@@ -74,14 +74,10 @@ export default class LayerAutosave extends EventTarget {
|
|
|
74
74
|
private readonly delayMs;
|
|
75
75
|
/** The layers being watched for changes */
|
|
76
76
|
private watched?;
|
|
77
|
-
/**
|
|
78
|
-
private
|
|
77
|
+
/** Each layer as last saved, by layer ID: the layer itself and its change count (see `LayerStack.pixelVersion`) */
|
|
78
|
+
private saved;
|
|
79
79
|
/** Size in bytes of each layer's image as last saved or restored, by layer ID */
|
|
80
80
|
private sizes;
|
|
81
|
-
/** Layers whose pixels changed since the last save */
|
|
82
|
-
private changed;
|
|
83
|
-
/** Whether any layer may have changed since the last save (not only those in `changed`) */
|
|
84
|
-
private checkAll;
|
|
85
81
|
private timer?;
|
|
86
82
|
/** The save being written, if any */
|
|
87
83
|
private saving?;
|
|
@@ -133,11 +129,13 @@ export default class LayerAutosave extends EventTarget {
|
|
|
133
129
|
*/
|
|
134
130
|
private restore;
|
|
135
131
|
/**
|
|
136
|
-
* Remember each layer
|
|
132
|
+
* Remember each layer as saved (just restored).
|
|
137
133
|
*/
|
|
138
134
|
private remember;
|
|
135
|
+
/** A layer and its change count, to tell later whether it changed */
|
|
136
|
+
private record;
|
|
139
137
|
/**
|
|
140
|
-
* Save the drawing, encoding only the layers whose pixels changed since they were last saved.
|
|
138
|
+
* Save the drawing, encoding only the layers whose pixels changed since they were last saved (or that are new).
|
|
141
139
|
*/
|
|
142
140
|
private write;
|
|
143
141
|
/**
|
|
@@ -145,10 +143,6 @@ export default class LayerAutosave extends EventTarget {
|
|
|
145
143
|
* changed.
|
|
146
144
|
*/
|
|
147
145
|
private rememberSizes;
|
|
148
|
-
/**
|
|
149
|
-
* A fingerprint of a layer's pixels.
|
|
150
|
-
*/
|
|
151
|
-
private fingerprint;
|
|
152
146
|
/**
|
|
153
147
|
* Save once the layers have stopped changing for a moment and drawing pauses.
|
|
154
148
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import PngEncoder from "../PngEncoder/index.js";
|
|
2
2
|
import "../../storage/index.js";
|
|
3
3
|
// src/lib/classes/LayerAutosave/index.ts
|
|
4
4
|
/** How long to wait after the layers change before saving, so quick changes are saved together (milliseconds) */
|
|
@@ -18,12 +18,14 @@ var AutosaveState = {
|
|
|
18
18
|
/** Not saving any more (see `stop`) */
|
|
19
19
|
Stopped: "stopped"
|
|
20
20
|
};
|
|
21
|
+
/** Encodes layers as PNGs in a background worker, shared by every autosave on the page (made on first use) */
|
|
22
|
+
var sharedEncoder;
|
|
21
23
|
/**
|
|
22
|
-
* Encode a canvas as a PNG.
|
|
24
|
+
* Encode a canvas as a PNG, in a background worker where one can be used, so saving doesn't freeze the page.
|
|
23
25
|
*/
|
|
24
26
|
var encodePng = (canvas) => {
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
+
sharedEncoder ??= new PngEncoder();
|
|
28
|
+
return sharedEncoder.encode(canvas);
|
|
27
29
|
};
|
|
28
30
|
/**
|
|
29
31
|
* Whether a saved drawing has the expected shape and an image for each layer.
|
|
@@ -32,8 +34,8 @@ var isComplete = (drawing, images) => drawing.version === 1 && drawing.layers.le
|
|
|
32
34
|
/**
|
|
33
35
|
* Keeps a canvas's layers in a store, so the drawing survives reloading the page: restores the saved drawing when it
|
|
34
36
|
* starts, then saves soon after the layers change (once drawing pauses), and straight away when the page is hidden.
|
|
35
|
-
* Only layers whose pixels changed are encoded and saved again
|
|
36
|
-
* which.
|
|
37
|
+
* Only layers whose pixels changed are encoded and saved again: the layers' change counts tell which (see
|
|
38
|
+
* `LayerStack.pixelVersion`), without reading their pixels, which is slow on phones.
|
|
37
39
|
*
|
|
38
40
|
* Only one window draws on a drawing at a time: the first to start has it, and others wait in the `elsewhere` state
|
|
39
41
|
* (the UI blocks drawing there). When the page is shown again it tries again, and `takeOver` takes the drawing from
|
|
@@ -62,14 +64,10 @@ var LayerAutosave = class extends EventTarget {
|
|
|
62
64
|
delayMs;
|
|
63
65
|
/** The layers being watched for changes */
|
|
64
66
|
watched;
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
+
/** Each layer as last saved, by layer ID: the layer itself and its change count (see `LayerStack.pixelVersion`) */
|
|
68
|
+
saved = /* @__PURE__ */ new Map();
|
|
67
69
|
/** Size in bytes of each layer's image as last saved or restored, by layer ID */
|
|
68
70
|
sizes = /* @__PURE__ */ new Map();
|
|
69
|
-
/** Layers whose pixels changed since the last save */
|
|
70
|
-
changed = /* @__PURE__ */ new Set();
|
|
71
|
-
/** Whether any layer may have changed since the last save (not only those in `changed`) */
|
|
72
|
-
checkAll = false;
|
|
73
71
|
timer;
|
|
74
72
|
/** The save being written, if any */
|
|
75
73
|
saving;
|
|
@@ -77,12 +75,7 @@ var LayerAutosave = class extends EventTarget {
|
|
|
77
75
|
again = false;
|
|
78
76
|
/** Lets go of the drawing's lock, while held */
|
|
79
77
|
release;
|
|
80
|
-
onLayersChange = () =>
|
|
81
|
-
const id = this.watched?.changedLayerId;
|
|
82
|
-
if (id) this.changed.add(id);
|
|
83
|
-
else this.checkAll = true;
|
|
84
|
-
this.schedule();
|
|
85
|
-
};
|
|
78
|
+
onLayersChange = () => this.schedule();
|
|
86
79
|
onCanvasChange = () => {
|
|
87
80
|
if (this.canvas.layers === this.watched) return;
|
|
88
81
|
this.watch();
|
|
@@ -202,7 +195,7 @@ var LayerAutosave = class extends EventTarget {
|
|
|
202
195
|
return;
|
|
203
196
|
}
|
|
204
197
|
if (this.currentState !== AutosaveState.Starting) return;
|
|
205
|
-
this.
|
|
198
|
+
this.saved = /* @__PURE__ */ new Map();
|
|
206
199
|
this.watch();
|
|
207
200
|
if (restored) this.remember();
|
|
208
201
|
this.setState(AutosaveState.Ready);
|
|
@@ -222,29 +215,32 @@ var LayerAutosave = class extends EventTarget {
|
|
|
222
215
|
return true;
|
|
223
216
|
}
|
|
224
217
|
/**
|
|
225
|
-
* Remember each layer
|
|
218
|
+
* Remember each layer as saved (just restored).
|
|
226
219
|
*/
|
|
227
220
|
remember() {
|
|
228
|
-
|
|
229
|
-
this.
|
|
230
|
-
|
|
221
|
+
const stack = this.canvas.layers;
|
|
222
|
+
for (const layer of stack.layers) this.saved.set(layer.id, this.record(layer));
|
|
223
|
+
}
|
|
224
|
+
/** A layer and its change count, to tell later whether it changed */
|
|
225
|
+
record(layer) {
|
|
226
|
+
return {
|
|
227
|
+
layer,
|
|
228
|
+
version: this.canvas.layers.pixelVersion(layer.id)
|
|
229
|
+
};
|
|
231
230
|
}
|
|
232
231
|
/**
|
|
233
|
-
* Save the drawing, encoding only the layers whose pixels changed since they were last saved.
|
|
232
|
+
* Save the drawing, encoding only the layers whose pixels changed since they were last saved (or that are new).
|
|
234
233
|
*/
|
|
235
234
|
async write() {
|
|
236
235
|
const stack = this.canvas.layers;
|
|
237
|
-
const
|
|
238
|
-
this.checkAll = false;
|
|
239
|
-
this.changed = /* @__PURE__ */ new Set();
|
|
240
|
-
const prints = /* @__PURE__ */ new Map();
|
|
236
|
+
const encoded = /* @__PURE__ */ new Map();
|
|
241
237
|
try {
|
|
242
238
|
const encoding = [];
|
|
243
239
|
for (const layer of stack.layers) {
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
|
|
240
|
+
const record = this.record(layer);
|
|
241
|
+
const saved = this.saved.get(layer.id);
|
|
242
|
+
if (saved?.layer === layer && saved.version === record.version) continue;
|
|
243
|
+
encoded.set(layer.id, record);
|
|
248
244
|
encoding.push(this.encode(layer.canvas).then((image) => [layer.id, image]));
|
|
249
245
|
}
|
|
250
246
|
const drawing = {
|
|
@@ -258,13 +254,11 @@ var LayerAutosave = class extends EventTarget {
|
|
|
258
254
|
if (this.currentState !== AutosaveState.Ready) return;
|
|
259
255
|
await this.store.save(drawing, images);
|
|
260
256
|
const kept = new Set(drawing.layers.map((layer) => layer.id));
|
|
261
|
-
for (const id of this.
|
|
262
|
-
for (const [id,
|
|
257
|
+
for (const id of this.saved.keys()) if (!kept.has(id)) this.saved.delete(id);
|
|
258
|
+
for (const [id, record] of encoded) this.saved.set(id, record);
|
|
263
259
|
this.error = void 0;
|
|
264
260
|
this.rememberSizes(kept, images);
|
|
265
261
|
} catch (error) {
|
|
266
|
-
for (const id of [...changed, ...prints.keys()]) this.changed.add(id);
|
|
267
|
-
this.checkAll ||= checkAll;
|
|
268
262
|
this.fail(error);
|
|
269
263
|
}
|
|
270
264
|
}
|
|
@@ -279,13 +273,6 @@ var LayerAutosave = class extends EventTarget {
|
|
|
279
273
|
if (this.storedBytes !== before) this.dispatchEvent(new Event("change"));
|
|
280
274
|
}
|
|
281
275
|
/**
|
|
282
|
-
* A fingerprint of a layer's pixels.
|
|
283
|
-
*/
|
|
284
|
-
fingerprint(id) {
|
|
285
|
-
const layer = this.canvas.layers.get(id);
|
|
286
|
-
return pixelFingerprint(layer.context.getImageData(0, 0, layer.width, layer.height).data);
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
276
|
* Save once the layers have stopped changing for a moment and drawing pauses.
|
|
290
277
|
*/
|
|
291
278
|
schedule() {
|
|
@@ -29,6 +29,8 @@ export default class LayerStack extends EventTarget {
|
|
|
29
29
|
/** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
|
|
30
30
|
private onlyChanged?;
|
|
31
31
|
private readonly createCanvas;
|
|
32
|
+
/** How many times each layer's image data has been changed through the stack (see `pixelVersion`) */
|
|
33
|
+
private readonly versions;
|
|
32
34
|
/** Most layers the stack can have */
|
|
33
35
|
readonly maxLayers: number;
|
|
34
36
|
constructor(width: number, height: number, createCanvas?: CanvasFactory, maxLayers?: number);
|
|
@@ -71,6 +73,17 @@ export default class LayerStack extends EventTarget {
|
|
|
71
73
|
* `markChanged`, e.g. a brush stroke); undefined when anything may have changed.
|
|
72
74
|
*/
|
|
73
75
|
get changedLayerId(): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* How many times a layer's image data has been changed through the stack (`markChanged`, `changeImage`, `clear`,
|
|
78
|
+
* merging into it, and undoing or redoing those), to tell whether it changed since some point without reading its
|
|
79
|
+
* pixels (which is slow on phones). A new layer starts at 0.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const before = stack.pixelVersion(id);
|
|
83
|
+
* stack.markChanged(id);
|
|
84
|
+
* stack.pixelVersion(id) > before; // true
|
|
85
|
+
*/
|
|
86
|
+
pixelVersion(id: string): number;
|
|
74
87
|
/**
|
|
75
88
|
* Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
|
|
76
89
|
*/
|
|
@@ -150,6 +163,8 @@ export default class LayerStack extends EventTarget {
|
|
|
150
163
|
* An undoable change of image data: undo and redo both swap the stored pixels with the layer's, then select the layer.
|
|
151
164
|
*/
|
|
152
165
|
private imageEntry;
|
|
166
|
+
/** Count a change to a layer's image data */
|
|
167
|
+
private touch;
|
|
153
168
|
/**
|
|
154
169
|
* Create a layer with the stack's size and canvas factory.
|
|
155
170
|
*/
|
|
@@ -31,6 +31,8 @@ var LayerStack = class extends EventTarget {
|
|
|
31
31
|
/** The layer the change being announced is about, when only its pixels changed (see `changedLayerId`) */
|
|
32
32
|
onlyChanged;
|
|
33
33
|
createCanvas;
|
|
34
|
+
/** How many times each layer's image data has been changed through the stack (see `pixelVersion`) */
|
|
35
|
+
versions = /* @__PURE__ */ new WeakMap();
|
|
34
36
|
/** Most layers the stack can have */
|
|
35
37
|
maxLayers;
|
|
36
38
|
constructor(width, height, createCanvas = createOffscreenCanvas, maxLayers = 5) {
|
|
@@ -130,10 +132,23 @@ var LayerStack = class extends EventTarget {
|
|
|
130
132
|
return this.onlyChanged;
|
|
131
133
|
}
|
|
132
134
|
/**
|
|
135
|
+
* How many times a layer's image data has been changed through the stack (`markChanged`, `changeImage`, `clear`,
|
|
136
|
+
* merging into it, and undoing or redoing those), to tell whether it changed since some point without reading its
|
|
137
|
+
* pixels (which is slow on phones). A new layer starts at 0.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* const before = stack.pixelVersion(id);
|
|
141
|
+
* stack.markChanged(id);
|
|
142
|
+
* stack.pixelVersion(id) > before; // true
|
|
143
|
+
*/
|
|
144
|
+
pixelVersion(id) {
|
|
145
|
+
return this.versions.get(this.get(id)) ?? 0;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
133
148
|
* Tell listeners a layer's image data changed (e.g. after a brush stroke), so thumbnails and the map redraw.
|
|
134
149
|
*/
|
|
135
150
|
markChanged(id) {
|
|
136
|
-
this.get(id);
|
|
151
|
+
this.touch(this.get(id));
|
|
137
152
|
this.onlyChanged = id;
|
|
138
153
|
try {
|
|
139
154
|
this.notify();
|
|
@@ -150,6 +165,7 @@ var LayerStack = class extends EventTarget {
|
|
|
150
165
|
const area = contentBounds(data, this.width, this.height);
|
|
151
166
|
if (area) this.recordImage(id, area);
|
|
152
167
|
layer.clear();
|
|
168
|
+
this.touch(layer);
|
|
153
169
|
this.notify();
|
|
154
170
|
}
|
|
155
171
|
/**
|
|
@@ -174,7 +190,9 @@ var LayerStack = class extends EventTarget {
|
|
|
174
190
|
*/
|
|
175
191
|
changeImage(id, area, change) {
|
|
176
192
|
this.recordImage(id, area);
|
|
177
|
-
|
|
193
|
+
const layer = this.get(id);
|
|
194
|
+
change(layer.context);
|
|
195
|
+
this.touch(layer);
|
|
178
196
|
this.notify();
|
|
179
197
|
}
|
|
180
198
|
/**
|
|
@@ -209,6 +227,7 @@ var LayerStack = class extends EventTarget {
|
|
|
209
227
|
ctx.globalCompositeOperation = layer.blendMode;
|
|
210
228
|
ctx.drawImage(layer.canvas, x, y, width, height, x, y, width, height);
|
|
211
229
|
ctx.restore();
|
|
230
|
+
this.touch(below);
|
|
212
231
|
}
|
|
213
232
|
this.items = this.items.filter((l) => l !== layer);
|
|
214
233
|
this.activeId = below.id;
|
|
@@ -332,6 +351,7 @@ var LayerStack = class extends EventTarget {
|
|
|
332
351
|
ctx.clearRect(area.x, area.y, area.width, area.height);
|
|
333
352
|
ctx.drawImage(stored, area.x, area.y);
|
|
334
353
|
stored = current;
|
|
354
|
+
this.touch(layer);
|
|
335
355
|
this.select(id);
|
|
336
356
|
};
|
|
337
357
|
return {
|
|
@@ -339,6 +359,10 @@ var LayerStack = class extends EventTarget {
|
|
|
339
359
|
redo: swap
|
|
340
360
|
};
|
|
341
361
|
}
|
|
362
|
+
/** Count a change to a layer's image data */
|
|
363
|
+
touch(layer) {
|
|
364
|
+
this.versions.set(layer, this.pixelVersion(layer.id) + 1);
|
|
365
|
+
}
|
|
342
366
|
/**
|
|
343
367
|
* Create a layer with the stack's size and canvas factory.
|
|
344
368
|
*/
|
|
@@ -18,8 +18,9 @@ import type { LayerStoreLike } from "../../storage";
|
|
|
18
18
|
import type { AssetAuthor, BrushConfig } from "../../brushes/types";
|
|
19
19
|
import CustomPresets, { type PresetStoreLike } from "../CustomPresets";
|
|
20
20
|
import InputSettings from "../InputSettings";
|
|
21
|
-
import UiSettings from "../UiSettings";
|
|
21
|
+
import UiSettings, { type Rendering } from "../UiSettings";
|
|
22
22
|
import Palette from "../Palette";
|
|
23
|
+
import ProfileGraphs from "../ProfileGraphs";
|
|
23
24
|
import ShortcutSettings from "../ShortcutSettings";
|
|
24
25
|
import ExternalSheets from "../ExternalSheets";
|
|
25
26
|
import { type CanvasMessage, type PatchMessage, type StrokeMessage } from "../../messages";
|
|
@@ -114,7 +115,17 @@ export interface PaintCanvasOptions {
|
|
|
114
115
|
maxCanvasSize?: number;
|
|
115
116
|
/** Most layers the user can have (default `DEFAULT_MAX_LAYERS`) */
|
|
116
117
|
maxLayers?: number;
|
|
117
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Where the canvases draw (`cpu` by default): `cpu` keeps their pixels in regular memory (`createMemoryCanvas`) so
|
|
120
|
+
* saving and copying don't stall phones; `gpu` asks the browser to keep them on the GPU (`createOffscreenCanvas`).
|
|
121
|
+
* Browsers fix this when a canvas is made, so it can't change while the canvas runs: to follow the user's choice
|
|
122
|
+
* (`ui.data.rendering`), pass the one they made last time (`<paint-canvas>` does this with its `settings-store`).
|
|
123
|
+
*/
|
|
124
|
+
rendering?: Rendering;
|
|
125
|
+
/**
|
|
126
|
+
* Creates the canvases the engine draws in: layers, stroke buffers, masks and copies. Chosen by `rendering` unless
|
|
127
|
+
* given (replaceable for tests).
|
|
128
|
+
*/
|
|
118
129
|
createCanvas?: CanvasFactory;
|
|
119
130
|
/** Where custom brush presets are kept (defaults to keeping them in memory only) */
|
|
120
131
|
presetStore?: PresetStoreLike;
|
|
@@ -167,10 +178,12 @@ export default class PaintCanvas extends EventTarget {
|
|
|
167
178
|
readonly background: BackgroundWork;
|
|
168
179
|
/** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
|
|
169
180
|
readonly input: InputSettings;
|
|
170
|
-
/** The user's interface choices (theme and
|
|
181
|
+
/** The user's interface choices (theme, language and where canvases draw), kept with the other app settings */
|
|
171
182
|
readonly ui: UiSettings;
|
|
172
183
|
/** The user's saved colours, shown as swatches */
|
|
173
184
|
readonly palette: Palette;
|
|
185
|
+
/** The user's own profile graphs (and the presets), for shaping how brush settings vary */
|
|
186
|
+
readonly profiles: ProfileGraphs;
|
|
174
187
|
/** The user's keyboard shortcuts: each configurable action's key combination */
|
|
175
188
|
readonly shortcuts: ShortcutSettings;
|
|
176
189
|
/** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
|
|
@@ -226,6 +239,8 @@ export default class PaintCanvas extends EventTarget {
|
|
|
226
239
|
/** The layer stack whose changes are being sent */
|
|
227
240
|
private sendingLayers?;
|
|
228
241
|
private readonly onLayersChangeToSend;
|
|
242
|
+
/** Where the canvases draw, chosen when the canvas started (the user's choice for next time is `ui.data.rendering`) */
|
|
243
|
+
readonly rendering: Rendering;
|
|
229
244
|
/** Creates the canvases the app draws on (fake ones in tests) */
|
|
230
245
|
readonly createCanvas: CanvasFactory;
|
|
231
246
|
/** Who the custom presets belong to (passed to the preset store) */
|
|
@@ -439,6 +454,20 @@ export default class PaintCanvas extends EventTarget {
|
|
|
439
454
|
* nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
440
455
|
*/
|
|
441
456
|
copy(): Rect | undefined;
|
|
457
|
+
/**
|
|
458
|
+
* Copy the masked image data of all the shown layers, mixed as they're shown (each layer's opacity and blend mode,
|
|
459
|
+
* without the white background), to the clipboard. Hidden layers are left out. Like `copy`, it needs a mask with image
|
|
460
|
+
* data inside it, and does nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
461
|
+
*
|
|
462
|
+
* @example
|
|
463
|
+
* app.selection.selectAll();
|
|
464
|
+
* app.copyMerged(); // { x: 10, y: 20, width: 30, height: 5 }: the part of the canvas with paint on any shown layer
|
|
465
|
+
*/
|
|
466
|
+
copyMerged(): Rect | undefined;
|
|
467
|
+
/**
|
|
468
|
+
* Copy the masked part of a canvas-sized image to the clipboard (see `copy`), returning the area copied, if any.
|
|
469
|
+
*/
|
|
470
|
+
private copyMasked;
|
|
442
471
|
/**
|
|
443
472
|
* How many times image data has been copied or cut, to tell whether it was copied here since something else may have
|
|
444
473
|
* been copied in another app (see `KeyboardShortcuts`).
|
|
@@ -484,9 +513,11 @@ export default class PaintCanvas extends EventTarget {
|
|
|
484
513
|
* data when its alpha lock is on. None if that is empty.
|
|
485
514
|
*/
|
|
486
515
|
private fillArea;
|
|
516
|
+
/** Whether image data can be copied now: not during a stroke or while a transform is pending */
|
|
517
|
+
private get canCopy();
|
|
487
518
|
/**
|
|
488
|
-
* The bounds of the selected layer's
|
|
489
|
-
* during a stroke or while a transform is pending.
|
|
519
|
+
* The bounds of a canvas-sized image's data (the selected layer's by default) inside the mask, if there is a mask
|
|
520
|
+
* with image data inside it. None during a stroke or while a transform is pending.
|
|
490
521
|
*/
|
|
491
522
|
private maskedArea;
|
|
492
523
|
/**
|