@lvl99dotcom/paintcanvas 0.5.0 → 0.6.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/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 +26 -3
- package/dist/lib/classes/PaintCanvas/index.js +39 -12
- 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/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 +49 -0
- package/dist/lib/settings/schema.js +6 -0
- package/dist/lib/validation/validators.generated.js +13064 -3696
- package/dist/locales/en-GB.js +30 -0
- package/dist/locales/en-US.js +30 -0
- package/dist/locales/fr.js +30 -0
- package/dist/locales/ja.js +30 -0
- package/dist/locales/ko.js +30 -0
- package/dist/locales/zh-Hans.js +30 -0
- package/dist/locales/zh-Hant.js +30 -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.js +3 -1
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
- package/dist/ui/elements/PcAppSettings/index.js +21 -65
- package/dist/ui/elements/PcBrushConfig/index.d.ts +25 -14
- package/dist/ui/elements/PcBrushConfig/index.js +216 -146
- package/dist/ui/elements/PcBrushConfig/style.js +1 -1
- package/dist/ui/elements/PcBrushTest/index.js +7 -2
- 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/PcCanvas/index.d.ts +6 -1
- package/dist/ui/elements/PcCanvas/index.js +10 -3
- package/dist/ui/elements/PcCanvas/style.js +1 -1
- package/dist/ui/elements/PcClipboardTools/index.d.ts +5 -4
- package/dist/ui/elements/PcClipboardTools/index.js +12 -5
- 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/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 +307 -0
- package/dist/ui/elements/PcStats/index.d.ts +19 -4
- package/dist/ui/elements/PcStats/index.js +29 -13
- package/dist/ui/elements/PcToolSettings/index.js +6 -6
- 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 +6 -1
- package/dist/ui/index.js +6 -1
- package/dist/ui/styles/index.d.ts +21 -1
- package/dist/ui/styles/index.js +73 -14
- package/locales/en-GB.json +30 -0
- package/locales/en-US.json +30 -0
- package/locales/fr.json +30 -0
- package/locales/ja.json +30 -0
- package/locales/ko.json +30 -0
- package/locales/zh-Hans.json +30 -0
- package/locales/zh-Hant.json +30 -0
- package/package.json +25 -23
|
@@ -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
|
*/
|
|
@@ -20,6 +20,7 @@ import CustomPresets, { type PresetStoreLike } from "../CustomPresets";
|
|
|
20
20
|
import InputSettings from "../InputSettings";
|
|
21
21
|
import UiSettings 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,11 @@ 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
|
+
* Creates the canvases the engine draws in: layers, stroke buffers, masks and copies. `createMemoryCanvas` by default,
|
|
120
|
+
* keeping pixels in regular memory so saving and copying don't stall phones; pass `createOffscreenCanvas` to keep them
|
|
121
|
+
* on the GPU instead (replaceable for tests).
|
|
122
|
+
*/
|
|
118
123
|
createCanvas?: CanvasFactory;
|
|
119
124
|
/** Where custom brush presets are kept (defaults to keeping them in memory only) */
|
|
120
125
|
presetStore?: PresetStoreLike;
|
|
@@ -171,6 +176,8 @@ export default class PaintCanvas extends EventTarget {
|
|
|
171
176
|
readonly ui: UiSettings;
|
|
172
177
|
/** The user's saved colours, shown as swatches */
|
|
173
178
|
readonly palette: Palette;
|
|
179
|
+
/** The user's own profile graphs (and the presets), for shaping how brush settings vary */
|
|
180
|
+
readonly profiles: ProfileGraphs;
|
|
174
181
|
/** The user's keyboard shortcuts: each configurable action's key combination */
|
|
175
182
|
readonly shortcuts: ShortcutSettings;
|
|
176
183
|
/** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
|
|
@@ -439,6 +446,20 @@ export default class PaintCanvas extends EventTarget {
|
|
|
439
446
|
* nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
440
447
|
*/
|
|
441
448
|
copy(): Rect | undefined;
|
|
449
|
+
/**
|
|
450
|
+
* Copy the masked image data of all the shown layers, mixed as they're shown (each layer's opacity and blend mode,
|
|
451
|
+
* without the white background), to the clipboard. Hidden layers are left out. Like `copy`, it needs a mask with image
|
|
452
|
+
* data inside it, and does nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* app.selection.selectAll();
|
|
456
|
+
* app.copyMerged(); // { x: 10, y: 20, width: 30, height: 5 }: the part of the canvas with paint on any shown layer
|
|
457
|
+
*/
|
|
458
|
+
copyMerged(): Rect | undefined;
|
|
459
|
+
/**
|
|
460
|
+
* Copy the masked part of a canvas-sized image to the clipboard (see `copy`), returning the area copied, if any.
|
|
461
|
+
*/
|
|
462
|
+
private copyMasked;
|
|
442
463
|
/**
|
|
443
464
|
* How many times image data has been copied or cut, to tell whether it was copied here since something else may have
|
|
444
465
|
* been copied in another app (see `KeyboardShortcuts`).
|
|
@@ -484,9 +505,11 @@ export default class PaintCanvas extends EventTarget {
|
|
|
484
505
|
* data when its alpha lock is on. None if that is empty.
|
|
485
506
|
*/
|
|
486
507
|
private fillArea;
|
|
508
|
+
/** Whether image data can be copied now: not during a stroke or while a transform is pending */
|
|
509
|
+
private get canCopy();
|
|
487
510
|
/**
|
|
488
|
-
* The bounds of the selected layer's
|
|
489
|
-
* during a stroke or while a transform is pending.
|
|
511
|
+
* The bounds of a canvas-sized image's data (the selected layer's by default) inside the mask, if there is a mask
|
|
512
|
+
* with image data inside it. None during a stroke or while a transform is pending.
|
|
490
513
|
*/
|
|
491
514
|
private maskedArea;
|
|
492
515
|
/**
|