@lvl99dotcom/paintcanvas 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
- package/dist/lib/brushes/StrokeEngine/index.js +26 -28
- package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
- package/dist/lib/classes/BrushSettings/index.js +35 -1
- package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
- package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
- package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
- package/dist/lib/classes/PaintCanvas/index.js +143 -42
- package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
- package/dist/lib/classes/PaintCanvas/tool.js +18 -0
- package/dist/lib/classes/Palette/index.d.ts +58 -0
- package/dist/lib/classes/Palette/index.js +92 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
- package/dist/lib/classes/SettingsAutosave/index.js +96 -0
- package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
- package/dist/lib/classes/ShortcutSettings/index.js +110 -0
- package/dist/lib/palette/index.d.ts +49 -0
- package/dist/lib/palette/index.js +82 -0
- package/dist/lib/palette/schema.d.ts +45 -0
- package/dist/lib/palette/schema.js +41 -0
- package/dist/lib/rendering/cursors/index.d.ts +2 -0
- package/dist/lib/rendering/cursors/index.js +3 -1
- package/dist/lib/rendering/sample/index.js +3 -2
- package/dist/lib/settings/index.d.ts +63 -0
- package/dist/lib/settings/index.js +91 -0
- package/dist/lib/settings/schema.d.ts +150 -0
- package/dist/lib/settings/schema.js +102 -0
- package/dist/lib/storage/index.d.ts +36 -5
- package/dist/lib/storage/index.js +87 -29
- package/dist/lib/utils/colour/index.d.ts +72 -0
- package/dist/lib/utils/colour/index.js +154 -0
- package/dist/lib/utils/shortcuts/index.d.ts +174 -11
- package/dist/lib/utils/shortcuts/index.js +391 -145
- package/dist/lib/validation/validators.generated.d.ts +5 -0
- package/dist/lib/validation/validators.generated.js +872 -1
- package/dist/locales/en-GB.js +87 -1
- package/dist/locales/en-US.js +87 -1
- package/dist/locales/fr.js +87 -1
- package/dist/locales/ja.js +87 -1
- package/dist/locales/ko.js +87 -1
- package/dist/locales/zh-Hans.js +87 -1
- package/dist/locales/zh-Hant.js +87 -1
- package/dist/package.js +1 -1
- package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
- package/dist/ui/elements/PaintCanvas/index.js +128 -169
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
- package/dist/ui/elements/PcAppSettings/index.js +222 -0
- package/dist/ui/elements/PcBrushConfig/index.js +1 -0
- package/dist/ui/elements/PcButton/index.js +1 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
- package/dist/ui/elements/PcCanvas/index.js +7 -5
- package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
- package/dist/ui/elements/PcColourPair/index.js +125 -0
- package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
- package/dist/ui/elements/PcContextMenu/index.js +128 -97
- package/dist/ui/elements/PcDialog/index.js +1 -0
- package/dist/ui/elements/PcIconToggle/index.js +1 -0
- package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
- package/dist/ui/elements/PcLayerItem/index.js +39 -91
- package/dist/ui/elements/PcLayerItem/style.js +1 -1
- package/dist/ui/elements/PcMenu/index.d.ts +93 -0
- package/dist/ui/elements/PcMenu/index.js +204 -0
- package/dist/ui/elements/PcPane/index.d.ts +118 -0
- package/dist/ui/elements/PcPane/index.js +357 -0
- package/dist/ui/elements/PcPanel/index.d.ts +26 -8
- package/dist/ui/elements/PcPanel/index.js +76 -8
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +20 -15
- package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
- package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
- package/dist/ui/elements/PcPanelColours/index.js +650 -0
- package/dist/ui/elements/PcPanelColours/style.js +3 -0
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
- package/dist/ui/elements/PcPanelLayers/style.js +3 -0
- package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
- package/dist/ui/elements/PcThemePicker/index.js +1 -0
- package/dist/ui/elements/PcToast/index.js +1 -0
- package/dist/ui/elements/PcToolSettings/index.js +2 -1
- package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
- package/dist/ui/elements/PcToolbar/index.js +77 -56
- package/dist/ui/elements/PcToolbar/style.js +1 -1
- package/dist/ui/index.d.ts +7 -1
- package/dist/ui/index.js +8 -2
- package/dist/ui/tokens/index.js +3 -1
- package/locales/en-GB.json +87 -1
- package/locales/en-US.json +87 -1
- package/locales/fr.json +87 -1
- package/locales/ja.json +87 -1
- package/locales/ko.json +87 -1
- package/locales/zh-Hans.json +87 -1
- package/locales/zh-Hant.json +87 -1
- package/package.json +1 -1
- package/dist/ui/elements/PcBrushMenu/style.js +0 -3
- package/dist/ui/elements/PcLayersMenu/style.js +0 -3
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ pnpm dev
|
|
|
87
87
|
|
|
88
88
|
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.)
|
|
89
89
|
|
|
90
|
-
- http://localhost:5190/: a full-page PaintCanvas with a layers panel, a menu with input settings, a dark theme switch and a "New drawing" button. The drawing
|
|
90
|
+
- 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 dark theme switch and a "New drawing" button. The drawing and the settings are kept in the browser, so they survive reloads.
|
|
91
91
|
- http://localhost:5190/elements.html: the base elements.
|
|
92
92
|
|
|
93
93
|
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.
|
|
@@ -19,7 +19,7 @@ export interface StrokeOptions {
|
|
|
19
19
|
width: number;
|
|
20
20
|
height: number;
|
|
21
21
|
};
|
|
22
|
-
/** Paint colour as a hex string */
|
|
22
|
+
/** Paint colour as a hex string (for the eraser, the colour it paints when the layer's alpha lock is on) */
|
|
23
23
|
colour: string;
|
|
24
24
|
tip: BrushTip;
|
|
25
25
|
/** Whether input comes from a pen (enables pressure and tilt) */
|
|
@@ -76,7 +76,7 @@ export interface StrokeCommitDetail {
|
|
|
76
76
|
* Stamps are drawn at full strength into a stroke buffer, so overlapping stamps within one stroke do not build up opacity.
|
|
77
77
|
* The buffer is merged into the layer with the stroke's opacity (the preset's `alpha`):
|
|
78
78
|
* - Brush: painted over the layer (or only onto existing pixels when the layer's alpha lock is on).
|
|
79
|
-
* - Eraser: removes the buffer's shape from the layer (
|
|
79
|
+
* - Eraser: removes the buffer's shape from the layer (or paints its colour onto existing pixels when alpha lock is on).
|
|
80
80
|
* - With a mask, the buffer is first cut to the mask, so the stroke only applies inside it.
|
|
81
81
|
*
|
|
82
82
|
* While drawing, `preview` provides the layer with the stroke merged in, to draw instead of the layer. The stroke is written into the layer when it ends.
|
|
@@ -39,10 +39,13 @@ var clipRect = (rect, width, height) => {
|
|
|
39
39
|
height: bottom - y
|
|
40
40
|
} : void 0;
|
|
41
41
|
};
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* How the stroke buffer is merged into the layer: with alpha lock on, both brush and eraser paint only onto existing
|
|
44
|
+
* pixels; otherwise the brush paints over the layer and the eraser removes from it.
|
|
45
|
+
*/
|
|
43
46
|
var compositeOperation = (mode, alphaLock) => {
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
47
|
+
if (alphaLock) return "source-atop";
|
|
48
|
+
return mode === "erase" ? "destination-out" : "source-over";
|
|
46
49
|
};
|
|
47
50
|
/** Event dispatched just before a finished stroke is written into its layer */
|
|
48
51
|
var BEFORE_COMMIT_EVENT = "beforecommit";
|
|
@@ -56,7 +59,7 @@ var STROKE_END_EVENT = "strokeend";
|
|
|
56
59
|
* Stamps are drawn at full strength into a stroke buffer, so overlapping stamps within one stroke do not build up opacity.
|
|
57
60
|
* The buffer is merged into the layer with the stroke's opacity (the preset's `alpha`):
|
|
58
61
|
* - Brush: painted over the layer (or only onto existing pixels when the layer's alpha lock is on).
|
|
59
|
-
* - Eraser: removes the buffer's shape from the layer (
|
|
62
|
+
* - Eraser: removes the buffer's shape from the layer (or paints its colour onto existing pixels when alpha lock is on).
|
|
60
63
|
* - With a mask, the buffer is first cut to the mask, so the stroke only applies inside it.
|
|
61
64
|
*
|
|
62
65
|
* While drawing, `preview` provides the layer with the stroke merged in, to draw instead of the layer. The stroke is written into the layer when it ends.
|
|
@@ -215,7 +218,7 @@ var StrokeEngine = class extends EventTarget {
|
|
|
215
218
|
const active = this.active;
|
|
216
219
|
if (points.length === 0) return;
|
|
217
220
|
if (this.debugEnabled) this.debugTrace.stamps.push(...points);
|
|
218
|
-
const { preset, size, tip, colour,
|
|
221
|
+
const { preset, size, tip, colour, isPen } = active.options;
|
|
219
222
|
const { random } = active;
|
|
220
223
|
const ctx = this.buffer.getContext("2d");
|
|
221
224
|
const stampPreset = {
|
|
@@ -230,7 +233,7 @@ var StrokeEngine = class extends EventTarget {
|
|
|
230
233
|
y: point.y,
|
|
231
234
|
params: varied
|
|
232
235
|
};
|
|
233
|
-
tip.stamp(ctx, x, y, params,
|
|
236
|
+
tip.stamp(ctx, x, y, params, colour);
|
|
234
237
|
const reach = Math.hypot(params.width, params.height) / 2 + STAMP_MARGIN;
|
|
235
238
|
changed = unionRect(changed, {
|
|
236
239
|
x: x + params.offsetX - reach,
|
|
@@ -258,12 +261,9 @@ var StrokeEngine = class extends EventTarget {
|
|
|
258
261
|
ctx.clearRect(x, y, width, height);
|
|
259
262
|
ctx.drawImage(layer.canvas, x, y, width, height, x, y, width, height);
|
|
260
263
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
ctx.globalCompositeOperation = operation;
|
|
265
|
-
ctx.drawImage(this.strokeImage(active, area), x, y, width, height, x, y, width, height);
|
|
266
|
-
}
|
|
264
|
+
ctx.globalAlpha = Math.max(0, Math.min(1, preset.alpha ?? 1));
|
|
265
|
+
ctx.globalCompositeOperation = compositeOperation(mode, layer.alphaLock);
|
|
266
|
+
ctx.drawImage(this.strokeImage(active, area), x, y, width, height, x, y, width, height);
|
|
267
267
|
ctx.restore();
|
|
268
268
|
}
|
|
269
269
|
/**
|
|
@@ -289,22 +289,20 @@ var StrokeEngine = class extends EventTarget {
|
|
|
289
289
|
commit(active) {
|
|
290
290
|
const { layer, mode, preset, size, colour, isPen } = active.options;
|
|
291
291
|
if (active.dirty) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
} }));
|
|
307
|
-
}
|
|
292
|
+
const stroke = {
|
|
293
|
+
mode,
|
|
294
|
+
preset,
|
|
295
|
+
size,
|
|
296
|
+
colour,
|
|
297
|
+
isPen,
|
|
298
|
+
seed: active.seed,
|
|
299
|
+
points: active.points
|
|
300
|
+
};
|
|
301
|
+
this.dispatchEvent(new CustomEvent(BEFORE_COMMIT_EVENT, { detail: {
|
|
302
|
+
layer,
|
|
303
|
+
area: active.dirty,
|
|
304
|
+
stroke
|
|
305
|
+
} }));
|
|
308
306
|
this.mergeInto(active.options.layer.canvas, active, active.dirty, false);
|
|
309
307
|
this.clearBuffer(active.dirty);
|
|
310
308
|
this.changedArea = unionRect(this.changedArea, active.dirty);
|
|
@@ -5,6 +5,10 @@ export declare const MIN_BRUSH_SIZE = 1;
|
|
|
5
5
|
export declare const MAX_BRUSH_SIZE = 256;
|
|
6
6
|
/** Fraction of the current size added or removed by `grow` and `shrink` ("]" and "[" keys) */
|
|
7
7
|
export declare const BRUSH_SIZE_STEP = 0.1;
|
|
8
|
+
/** Opacity step for the opacity keyboard shortcuts, in percent */
|
|
9
|
+
export declare const BRUSH_OPACITY_STEP = 10;
|
|
10
|
+
/** Lowest opacity the opacity keyboard shortcuts go to, in percent */
|
|
11
|
+
export declare const MIN_BRUSH_OPACITY = 1;
|
|
8
12
|
/**
|
|
9
13
|
* Clamp a value between min and max.
|
|
10
14
|
*
|
|
@@ -12,6 +16,16 @@ export declare const BRUSH_SIZE_STEP = 0.1;
|
|
|
12
16
|
* clamp(300, 1, 256); // 256
|
|
13
17
|
*/
|
|
14
18
|
export declare const clamp: (value: number, min: number, max: number) => number;
|
|
19
|
+
/**
|
|
20
|
+
* The next opacity step up or down, in percent: to the next multiple of `BRUSH_OPACITY_STEP`, from
|
|
21
|
+
* `MIN_BRUSH_OPACITY` to 100.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* stepOpacity(45, 1); // 50
|
|
25
|
+
* stepOpacity(10, -1); // 1
|
|
26
|
+
* stepOpacity(1, 1); // 10
|
|
27
|
+
*/
|
|
28
|
+
export declare const stepOpacity: (percent: number, direction: 1 | -1) => number;
|
|
15
29
|
/**
|
|
16
30
|
* The settings of a brush or eraser: the selected preset, an editable copy of its settings (changed from the tool
|
|
17
31
|
* settings bar or the configuration window, without changing the preset) and the size. The brush and eraser each have
|
|
@@ -80,6 +94,16 @@ export default class BrushSettings extends EventTarget {
|
|
|
80
94
|
* Decrease the size by one step (at least 1 pixel).
|
|
81
95
|
*/
|
|
82
96
|
shrink(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Increase the opacity by one step (see `stepOpacity`).
|
|
99
|
+
*/
|
|
100
|
+
moreOpaque(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Decrease the opacity by one step (see `stepOpacity`).
|
|
103
|
+
*/
|
|
104
|
+
lessOpaque(): void;
|
|
105
|
+
/** Change the opacity by one step up or down */
|
|
106
|
+
private stepOpacity;
|
|
83
107
|
/**
|
|
84
108
|
* Use a preset's (or configuration's) default size.
|
|
85
109
|
*/
|
|
@@ -5,6 +5,10 @@ var MIN_BRUSH_SIZE = 1;
|
|
|
5
5
|
var MAX_BRUSH_SIZE = 256;
|
|
6
6
|
/** Fraction of the current size added or removed by `grow` and `shrink` ("]" and "[" keys) */
|
|
7
7
|
var BRUSH_SIZE_STEP = .1;
|
|
8
|
+
/** Opacity step for the opacity keyboard shortcuts, in percent */
|
|
9
|
+
var BRUSH_OPACITY_STEP = 10;
|
|
10
|
+
/** Lowest opacity the opacity keyboard shortcuts go to, in percent */
|
|
11
|
+
var MIN_BRUSH_OPACITY = 1;
|
|
8
12
|
/**
|
|
9
13
|
* Clamp a value between min and max.
|
|
10
14
|
*
|
|
@@ -13,6 +17,19 @@ var BRUSH_SIZE_STEP = .1;
|
|
|
13
17
|
*/
|
|
14
18
|
var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
15
19
|
/**
|
|
20
|
+
* The next opacity step up or down, in percent: to the next multiple of `BRUSH_OPACITY_STEP`, from
|
|
21
|
+
* `MIN_BRUSH_OPACITY` to 100.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* stepOpacity(45, 1); // 50
|
|
25
|
+
* stepOpacity(10, -1); // 1
|
|
26
|
+
* stepOpacity(1, 1); // 10
|
|
27
|
+
*/
|
|
28
|
+
var stepOpacity = (percent, direction) => {
|
|
29
|
+
const steps = percent / 10;
|
|
30
|
+
return clamp((direction > 0 ? Math.floor(steps + 1e-9) + 1 : Math.ceil(steps - 1e-9) - 1) * 10, 1, 100);
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
16
33
|
* The settings of a brush or eraser: the selected preset, an editable copy of its settings (changed from the tool
|
|
17
34
|
* settings bar or the configuration window, without changing the preset) and the size. The brush and eraser each have
|
|
18
35
|
* their own instance.
|
|
@@ -136,6 +153,23 @@ var BrushSettings = class extends EventTarget {
|
|
|
136
153
|
this.resize(-Math.max(1, Math.round(this.width * BRUSH_SIZE_STEP)));
|
|
137
154
|
}
|
|
138
155
|
/**
|
|
156
|
+
* Increase the opacity by one step (see `stepOpacity`).
|
|
157
|
+
*/
|
|
158
|
+
moreOpaque() {
|
|
159
|
+
this.stepOpacity(1);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Decrease the opacity by one step (see `stepOpacity`).
|
|
163
|
+
*/
|
|
164
|
+
lessOpaque() {
|
|
165
|
+
this.stepOpacity(-1);
|
|
166
|
+
}
|
|
167
|
+
/** Change the opacity by one step up or down */
|
|
168
|
+
stepOpacity(direction) {
|
|
169
|
+
const percent = Math.round((this.current.alpha ?? 1) * 100);
|
|
170
|
+
this.update({ alpha: stepOpacity(percent, direction) / 100 });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
139
173
|
* Use a preset's (or configuration's) default size.
|
|
140
174
|
*/
|
|
141
175
|
applyPresetSize(preset) {
|
|
@@ -149,4 +183,4 @@ var BrushSettings = class extends EventTarget {
|
|
|
149
183
|
this.dispatchEvent(new Event("change"));
|
|
150
184
|
}
|
|
151
185
|
};
|
|
152
|
-
export { BRUSH_SIZE_STEP, MAX_BRUSH_SIZE, MIN_BRUSH_SIZE, clamp, BrushSettings as default };
|
|
186
|
+
export { BRUSH_OPACITY_STEP, BRUSH_SIZE_STEP, MAX_BRUSH_SIZE, MIN_BRUSH_OPACITY, MIN_BRUSH_SIZE, clamp, BrushSettings as default, stepOpacity };
|
|
@@ -3,12 +3,15 @@ import { type ShortcutAction } from "../../utils/shortcuts";
|
|
|
3
3
|
/** Event dispatched on the window for shortcut actions handled elsewhere (e.g. saving) */
|
|
4
4
|
export declare const SHORTCUT_EVENT = "pc-shortcut";
|
|
5
5
|
/**
|
|
6
|
-
* Listens for key presses and runs the matching shortcut on the app
|
|
6
|
+
* Listens for key presses and runs the matching shortcut on the app, using the user's key combinations
|
|
7
|
+
* (`app.shortcuts`). Shortcuts are ignored while typing in text fields and inside open `<dialog>` elements. Key presses are matched on keyboards with
|
|
8
|
+
* different layouts (see `comboFromEvent`).
|
|
7
9
|
*
|
|
8
|
-
* Tool selection, brush/eraser size, interface visibility, select all/deselect, undo/redo,
|
|
9
|
-
* masked image data (Delete or Backspace)
|
|
10
|
-
* lasso shape are handled here; other actions (and Escape/Enter for later features)
|
|
11
|
-
*
|
|
10
|
+
* Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
|
|
11
|
+
* cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
|
|
12
|
+
* and finishing or cancelling a lasso shape are handled here; other actions (and Escape/Enter for later features)
|
|
13
|
+
* dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
|
|
14
|
+
* temporarily; releasing it returns to the selected tool.
|
|
12
15
|
*
|
|
13
16
|
* @example
|
|
14
17
|
* const shortcuts = new KeyboardShortcuts(app);
|
|
@@ -17,6 +20,8 @@ export declare const SHORTCUT_EVENT = "pc-shortcut";
|
|
|
17
20
|
export default class KeyboardShortcuts {
|
|
18
21
|
private readonly app;
|
|
19
22
|
private target;
|
|
23
|
+
/** The hold-to-pan key while it is held */
|
|
24
|
+
private heldKey?;
|
|
20
25
|
private readonly onKeyDown;
|
|
21
26
|
private readonly onKeyUp;
|
|
22
27
|
private readonly onBlur;
|
|
@@ -34,13 +39,19 @@ export default class KeyboardShortcuts {
|
|
|
34
39
|
*/
|
|
35
40
|
handle(event: KeyboardEvent): ShortcutAction | undefined;
|
|
36
41
|
/**
|
|
37
|
-
* Return to the selected tool when the hold-to-pan key is released.
|
|
42
|
+
* Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
|
|
38
43
|
*/
|
|
39
44
|
handleKeyUp(event: KeyboardEvent): void;
|
|
45
|
+
/** Stop holding the hold-to-pan key */
|
|
46
|
+
private release;
|
|
40
47
|
/**
|
|
41
48
|
* Run a shortcut action.
|
|
42
49
|
*/
|
|
43
50
|
run(action: ShortcutAction): void;
|
|
51
|
+
/** The brush or eraser settings while that tool is selected */
|
|
52
|
+
private brushSettings;
|
|
53
|
+
/** The middle of the viewport, which zoom shortcuts zoom around */
|
|
54
|
+
private viewportCentre;
|
|
44
55
|
/**
|
|
45
56
|
* Dispatch an action as an `pc-shortcut` event for features handled elsewhere.
|
|
46
57
|
*/
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { Tool } from "../PaintCanvas/
|
|
2
|
-
import
|
|
1
|
+
import { Tool } from "../PaintCanvas/tool.js";
|
|
2
|
+
import "../PaintCanvas/index.js";
|
|
3
|
+
import { comboFromEvent, isTypingTarget, sameCombo } from "../../utils/shortcuts/index.js";
|
|
3
4
|
// src/lib/classes/KeyboardShortcuts/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a key press happened inside an open `<dialog>` (e.g. a settings window), where shortcuts must not apply and
|
|
7
|
+
* Escape is left to close it.
|
|
8
|
+
*/
|
|
9
|
+
var inOpenDialog = (path) => path.some((target) => target instanceof HTMLDialogElement && target.open);
|
|
4
10
|
/** Event dispatched on the window for shortcut actions handled elsewhere (e.g. saving) */
|
|
5
11
|
var SHORTCUT_EVENT = "pc-shortcut";
|
|
6
12
|
/**
|
|
7
|
-
* Listens for key presses and runs the matching shortcut on the app
|
|
13
|
+
* Listens for key presses and runs the matching shortcut on the app, using the user's key combinations
|
|
14
|
+
* (`app.shortcuts`). Shortcuts are ignored while typing in text fields and inside open `<dialog>` elements. Key presses are matched on keyboards with
|
|
15
|
+
* different layouts (see `comboFromEvent`).
|
|
8
16
|
*
|
|
9
|
-
* Tool selection, brush/eraser size, interface visibility, select all/deselect, undo/redo,
|
|
10
|
-
* masked image data (Delete or Backspace)
|
|
11
|
-
* lasso shape are handled here; other actions (and Escape/Enter for later features)
|
|
12
|
-
*
|
|
17
|
+
* Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
|
|
18
|
+
* cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
|
|
19
|
+
* and finishing or cancelling a lasso shape are handled here; other actions (and Escape/Enter for later features)
|
|
20
|
+
* dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
|
|
21
|
+
* temporarily; releasing it returns to the selected tool.
|
|
13
22
|
*
|
|
14
23
|
* @example
|
|
15
24
|
* const shortcuts = new KeyboardShortcuts(app);
|
|
@@ -18,9 +27,11 @@ var SHORTCUT_EVENT = "pc-shortcut";
|
|
|
18
27
|
var KeyboardShortcuts = class {
|
|
19
28
|
app;
|
|
20
29
|
target = null;
|
|
30
|
+
/** The hold-to-pan key while it is held */
|
|
31
|
+
heldKey;
|
|
21
32
|
onKeyDown = (event) => this.handle(event);
|
|
22
33
|
onKeyUp = (event) => this.handleKeyUp(event);
|
|
23
|
-
onBlur = () => this.
|
|
34
|
+
onBlur = () => this.release();
|
|
24
35
|
constructor(app) {
|
|
25
36
|
this.app = app;
|
|
26
37
|
}
|
|
@@ -47,23 +58,33 @@ var KeyboardShortcuts = class {
|
|
|
47
58
|
* Run the shortcut for a key press, if any. Returns the action run.
|
|
48
59
|
*/
|
|
49
60
|
handle(event) {
|
|
50
|
-
|
|
51
|
-
if (
|
|
61
|
+
const path = event.composedPath();
|
|
62
|
+
if (isTypingTarget(path[0] ?? event.target) || inOpenDialog(path)) return void 0;
|
|
63
|
+
const combo = comboFromEvent(event);
|
|
64
|
+
if (!combo) return void 0;
|
|
65
|
+
const { holdToPan, shortcuts } = this.app.shortcuts;
|
|
66
|
+
if (holdToPan && sameCombo(combo, holdToPan)) {
|
|
52
67
|
event.preventDefault();
|
|
68
|
+
this.heldKey = combo.key;
|
|
53
69
|
this.app.holdTool(Tool.Pan);
|
|
54
70
|
return;
|
|
55
71
|
}
|
|
56
|
-
const action =
|
|
72
|
+
const action = shortcuts.find((shortcut) => sameCombo(shortcut, combo))?.action;
|
|
57
73
|
if (!action) return void 0;
|
|
58
74
|
event.preventDefault();
|
|
59
75
|
this.run(action);
|
|
60
76
|
return action;
|
|
61
77
|
}
|
|
62
78
|
/**
|
|
63
|
-
* Return to the selected tool when the hold-to-pan key is released.
|
|
79
|
+
* Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
|
|
64
80
|
*/
|
|
65
81
|
handleKeyUp(event) {
|
|
66
|
-
if (event
|
|
82
|
+
if (this.heldKey !== void 0 && comboFromEvent(event)?.key === this.heldKey) this.release();
|
|
83
|
+
}
|
|
84
|
+
/** Stop holding the hold-to-pan key */
|
|
85
|
+
release() {
|
|
86
|
+
this.heldKey = void 0;
|
|
87
|
+
this.app.releaseHeldTool();
|
|
67
88
|
}
|
|
68
89
|
/**
|
|
69
90
|
* Run a shortcut action.
|
|
@@ -75,21 +96,41 @@ var KeyboardShortcuts = class {
|
|
|
75
96
|
app.selectTool(action.tool);
|
|
76
97
|
break;
|
|
77
98
|
case "brush-smaller":
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
this.brushSettings()?.shrink();
|
|
100
|
+
break;
|
|
101
|
+
case "brush-bigger":
|
|
102
|
+
this.brushSettings()?.grow();
|
|
103
|
+
break;
|
|
104
|
+
case "brush-less-opaque":
|
|
105
|
+
this.brushSettings()?.lessOpaque();
|
|
106
|
+
break;
|
|
107
|
+
case "brush-more-opaque":
|
|
108
|
+
this.brushSettings()?.moreOpaque();
|
|
82
109
|
break;
|
|
83
|
-
}
|
|
84
110
|
case "toggle-interface":
|
|
85
111
|
app.toggleInterface();
|
|
86
112
|
break;
|
|
87
113
|
case "select-all":
|
|
88
114
|
app.selection.selectAll();
|
|
89
115
|
break;
|
|
116
|
+
case "invert-selection":
|
|
117
|
+
app.selection.invert();
|
|
118
|
+
break;
|
|
90
119
|
case "deselect":
|
|
91
120
|
app.selection.clear();
|
|
92
121
|
break;
|
|
122
|
+
case "zoom-in":
|
|
123
|
+
app.viewport.zoomIn(this.viewportCentre());
|
|
124
|
+
break;
|
|
125
|
+
case "zoom-out":
|
|
126
|
+
app.viewport.zoomOut(this.viewportCentre());
|
|
127
|
+
break;
|
|
128
|
+
case "reset-zoom":
|
|
129
|
+
app.viewport.zoomAt(this.viewportCentre(), 1);
|
|
130
|
+
break;
|
|
131
|
+
case "layer-opacity":
|
|
132
|
+
app.layers.update(app.layers.active.id, { opacity: action.opacity });
|
|
133
|
+
break;
|
|
93
134
|
case "undo":
|
|
94
135
|
app.undo();
|
|
95
136
|
break;
|
|
@@ -102,6 +143,15 @@ var KeyboardShortcuts = class {
|
|
|
102
143
|
case "delete":
|
|
103
144
|
app.deleteMasked();
|
|
104
145
|
break;
|
|
146
|
+
case "fill-foreground":
|
|
147
|
+
app.fill("foreground");
|
|
148
|
+
break;
|
|
149
|
+
case "fill-background":
|
|
150
|
+
app.fill("background");
|
|
151
|
+
break;
|
|
152
|
+
case "swap-colours":
|
|
153
|
+
app.swapColours();
|
|
154
|
+
break;
|
|
105
155
|
case "copy":
|
|
106
156
|
app.copy();
|
|
107
157
|
break;
|
|
@@ -127,6 +177,19 @@ var KeyboardShortcuts = class {
|
|
|
127
177
|
default: this.dispatch(action);
|
|
128
178
|
}
|
|
129
179
|
}
|
|
180
|
+
/** The brush or eraser settings while that tool is selected */
|
|
181
|
+
brushSettings() {
|
|
182
|
+
const { app } = this;
|
|
183
|
+
return app.tool === Tool.Eraser ? app.eraser : app.tool === Tool.Brush ? app.brush : void 0;
|
|
184
|
+
}
|
|
185
|
+
/** The middle of the viewport, which zoom shortcuts zoom around */
|
|
186
|
+
viewportCentre() {
|
|
187
|
+
const { width, height } = this.app.viewport;
|
|
188
|
+
return {
|
|
189
|
+
x: width / 2,
|
|
190
|
+
y: height / 2
|
|
191
|
+
};
|
|
192
|
+
}
|
|
130
193
|
/**
|
|
131
194
|
* Dispatch an action as an `pc-shortcut` event for features handled elsewhere.
|
|
132
195
|
*/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import StrokeLog from "../StrokeLog";
|
|
2
|
+
import { Tool } from "./tool";
|
|
3
|
+
import { isHexColour } from "../../utils/colour";
|
|
2
4
|
import BackgroundWork from "../BackgroundWork";
|
|
3
5
|
import type { DrawableLayer } from "../../rendering/compositing";
|
|
4
6
|
import BrushSettings from "../BrushSettings";
|
|
@@ -16,25 +18,13 @@ import type { LayerStoreLike } from "../../storage";
|
|
|
16
18
|
import type { AssetAuthor, BrushConfig } from "../../brushes/types";
|
|
17
19
|
import CustomPresets, { type PresetStoreLike } from "../CustomPresets";
|
|
18
20
|
import InputSettings from "../InputSettings";
|
|
21
|
+
import Palette from "../Palette";
|
|
22
|
+
import ShortcutSettings from "../ShortcutSettings";
|
|
19
23
|
import ExternalSheets from "../ExternalSheets";
|
|
20
24
|
import { type CanvasMessage, type PatchMessage, type StrokeMessage } from "../../messages";
|
|
21
25
|
import { type BoundingBox } from "../../image";
|
|
22
26
|
import { type PaintCanvasDocument } from "../../document";
|
|
23
|
-
|
|
24
|
-
export declare enum Tool {
|
|
25
|
-
Brush = "brush",
|
|
26
|
-
Eraser = "eraser",
|
|
27
|
-
/** Move, scale and rotate */
|
|
28
|
-
Move = "move",
|
|
29
|
-
RectangleLasso = "rectangle-lasso",
|
|
30
|
-
CircleLasso = "circle-lasso",
|
|
31
|
-
PolygonLasso = "polygon-lasso",
|
|
32
|
-
FreeformLasso = "freeform-lasso",
|
|
33
|
-
Pan = "pan",
|
|
34
|
-
Zoom = "zoom",
|
|
35
|
-
/** Set the current colour from the canvas (eyedropper) */
|
|
36
|
-
ColourSample = "colour-sample"
|
|
37
|
-
}
|
|
27
|
+
export { Tool };
|
|
38
28
|
/** How the colour sample tool samples: the pixel under the pointer, or the average of a dragged rectangle */
|
|
39
29
|
export type SampleMode = "point" | "area";
|
|
40
30
|
/** Toggleable panels */
|
|
@@ -43,7 +33,9 @@ export declare enum Panel {
|
|
|
43
33
|
Eraser = "eraser",
|
|
44
34
|
Layers = "layers",
|
|
45
35
|
/** The implementer's own app menu */
|
|
46
|
-
Menu = "menu"
|
|
36
|
+
Menu = "menu",
|
|
37
|
+
/** The colour palette: the foreground and background colours, their values and the saved colours */
|
|
38
|
+
Palette = "palette"
|
|
47
39
|
}
|
|
48
40
|
/** Panels opened by tools */
|
|
49
41
|
export declare const TOOL_PANELS: Partial<Record<Tool, Panel>>;
|
|
@@ -51,8 +43,15 @@ export declare const TOOL_PANELS: Partial<Record<Tool, Panel>>;
|
|
|
51
43
|
export declare const DEFAULT_CANVAS_SIZE = 2000;
|
|
52
44
|
/** Largest canvas width or height allowed when the `maxCanvasSize` option isn't given */
|
|
53
45
|
export declare const MAX_CANVAS_SIZE = 2000;
|
|
54
|
-
/**
|
|
46
|
+
/** Foreground colour to start with */
|
|
55
47
|
export declare const DEFAULT_COLOUR = "#000000";
|
|
48
|
+
/** Background colour to start with */
|
|
49
|
+
export declare const DEFAULT_BACKGROUND_COLOUR = "#ffffff";
|
|
50
|
+
/**
|
|
51
|
+
* One of the two colours: the foreground colour (painted by the brush and foreground fills) or the background colour
|
|
52
|
+
* (painted by the eraser and Delete when the layer's alpha lock is on, and by background fills).
|
|
53
|
+
*/
|
|
54
|
+
export type ColourSlot = "foreground" | "background";
|
|
56
55
|
/** Owner of the custom presets when the `ownerId` option isn't given */
|
|
57
56
|
export declare const DEFAULT_OWNER_ID = "local";
|
|
58
57
|
/** Author name of presets made with `newPreset` when the `presetAuthor` option isn't given */
|
|
@@ -65,11 +64,16 @@ export interface PaintCanvasNames {
|
|
|
65
64
|
filePrefix: string;
|
|
66
65
|
/** The `format` written in exported brush preset files */
|
|
67
66
|
presetFileFormat: string;
|
|
67
|
+
/** The `format` written in exported colour palette files */
|
|
68
|
+
paletteFileFormat: string;
|
|
69
|
+
/** The `format` written in exported settings files */
|
|
70
|
+
settingsFileFormat: string;
|
|
68
71
|
/** Author name of presets made by a user without a handle, e.g. from `presetAuthor` */
|
|
69
72
|
unknownAuthor: string;
|
|
70
73
|
}
|
|
71
74
|
/** PaintCanvas's own names */
|
|
72
75
|
export declare const DEFAULT_NAMES: Readonly<PaintCanvasNames>;
|
|
76
|
+
export { isHexColour };
|
|
73
77
|
/** How much the canvas holds, e.g. to show in a stats panel */
|
|
74
78
|
export interface CanvasStats {
|
|
75
79
|
/** Canvas width in pixels */
|
|
@@ -123,7 +127,7 @@ export interface PaintCanvasOptions {
|
|
|
123
127
|
layerStore?: LayerStoreLike;
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
126
|
-
* The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool,
|
|
130
|
+
* The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool, foreground and background colours,
|
|
127
131
|
* selection, transforms, clipboard, undo/redo history, viewport and input settings. Where its data is kept and who it
|
|
128
132
|
* is shared with are up to the implementer (see `onMessage` and `receive`).
|
|
129
133
|
*
|
|
@@ -134,6 +138,7 @@ export interface PaintCanvasOptions {
|
|
|
134
138
|
* const canvas = new PaintCanvas({ width: 1000, height: 800 });
|
|
135
139
|
* canvas.selectTool(Tool.Brush, { fromToolbar: true });
|
|
136
140
|
* canvas.setColour("#ff0000");
|
|
141
|
+
* canvas.setColour("#ffffff", "background");
|
|
137
142
|
*/
|
|
138
143
|
export default class PaintCanvas extends EventTarget {
|
|
139
144
|
/** Brush settings */
|
|
@@ -154,6 +159,10 @@ export default class PaintCanvas extends EventTarget {
|
|
|
154
159
|
readonly background: BackgroundWork;
|
|
155
160
|
/** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
|
|
156
161
|
readonly input: InputSettings;
|
|
162
|
+
/** The user's saved colours, shown as swatches */
|
|
163
|
+
readonly palette: Palette;
|
|
164
|
+
/** The user's keyboard shortcuts: each configurable action's key combination */
|
|
165
|
+
readonly shortcuts: ShortcutSettings;
|
|
157
166
|
/** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
|
|
158
167
|
pointer?: Point;
|
|
159
168
|
/** Selected tool */
|
|
@@ -190,7 +199,8 @@ export default class PaintCanvas extends EventTarget {
|
|
|
190
199
|
private sendingStrokes;
|
|
191
200
|
/** Whether a stroke from elsewhere is being drawn (it isn't sent again) */
|
|
192
201
|
private replaying;
|
|
193
|
-
private
|
|
202
|
+
private currentColours;
|
|
203
|
+
private currentSelectedColour;
|
|
194
204
|
/** Image data copied with cut or copy, and the area it was copied from */
|
|
195
205
|
private clipboard?;
|
|
196
206
|
/** Drawings from elsewhere */
|
|
@@ -416,24 +426,45 @@ export default class PaintCanvas extends EventTarget {
|
|
|
416
426
|
*/
|
|
417
427
|
copy(): Rect | undefined;
|
|
418
428
|
/**
|
|
419
|
-
* Copy the masked image data of the selected layer to the clipboard, then
|
|
420
|
-
* action.
|
|
429
|
+
* Copy the masked image data of the selected layer to the clipboard, then remove it from the layer as one undoable
|
|
430
|
+
* action (see `deleteMasked`).
|
|
421
431
|
*/
|
|
422
432
|
cut(): void;
|
|
423
433
|
/**
|
|
424
|
-
* Erase the masked image data from the selected layer as one undoable action (Delete or Backspace).
|
|
434
|
+
* Erase the masked image data from the selected layer as one undoable action (Delete or Backspace). When the layer's
|
|
435
|
+
* alpha lock is on, it is painted with the background colour instead, keeping its transparency. Needs a mask with
|
|
425
436
|
* image data inside it; does nothing during a stroke or while a transform is pending.
|
|
426
437
|
*/
|
|
427
438
|
deleteMasked(): void;
|
|
439
|
+
/**
|
|
440
|
+
* Fill the selected layer with the foreground or background colour as one undoable action: only the masked area when
|
|
441
|
+
* there is a mask (partly where it is partly), and only existing pixels when the layer's alpha lock is on. Does
|
|
442
|
+
* nothing during a stroke or while a transform is pending.
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* canvas.fill("foreground");
|
|
446
|
+
*/
|
|
447
|
+
fill(slot: ColourSlot): void;
|
|
448
|
+
/**
|
|
449
|
+
* The area a fill changes: the mask's bounds (or the whole canvas without a mask), cut to the selected layer's image
|
|
450
|
+
* data when its alpha lock is on. None if that is empty.
|
|
451
|
+
*/
|
|
452
|
+
private fillArea;
|
|
428
453
|
/**
|
|
429
454
|
* The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
|
|
430
455
|
* during a stroke or while a transform is pending.
|
|
431
456
|
*/
|
|
432
457
|
private maskedArea;
|
|
433
458
|
/**
|
|
434
|
-
* Erase the mask's part of an area from the selected layer,
|
|
459
|
+
* Erase the mask's part of an area from the selected layer, or paint it with the background colour when the layer's
|
|
460
|
+
* alpha lock is on, as one undoable action.
|
|
435
461
|
*/
|
|
436
|
-
private
|
|
462
|
+
private clearMasked;
|
|
463
|
+
/**
|
|
464
|
+
* Paint a colour over an area of the selected layer, cut to the mask if there is one (partly where it is partly), as
|
|
465
|
+
* one undoable action. Only existing pixels are painted when the layer's alpha lock is on.
|
|
466
|
+
*/
|
|
467
|
+
private paintArea;
|
|
437
468
|
/**
|
|
438
469
|
* Show the clipboard's image data over the selected layer where it was copied from, and select the Move tool to place,
|
|
439
470
|
* scale and rotate it. It is written into the layer (and sent) on Enter or when switching to another tool; Escape
|
|
@@ -468,12 +499,31 @@ export default class PaintCanvas extends EventTarget {
|
|
|
468
499
|
get activeTool(): Tool;
|
|
469
500
|
/** The user's layers */
|
|
470
501
|
get layers(): LayerStack;
|
|
471
|
-
/** Current
|
|
502
|
+
/** Current foreground colour as a hex string, e.g. `#000000` (the same as `colours.foreground`) */
|
|
472
503
|
get colour(): string;
|
|
504
|
+
/** Current foreground and background colours as hex strings, e.g. `{ foreground: "#000000", background: "#ffffff" }` */
|
|
505
|
+
get colours(): Readonly<Record<ColourSlot, string>>;
|
|
506
|
+
/** Which colour is selected for configuration, e.g. by a colour picker or the colour sample tool ("foreground" by default) */
|
|
507
|
+
get selectedColour(): ColourSlot;
|
|
508
|
+
/**
|
|
509
|
+
* Set the foreground colour, or the background colour. Must be a 6-digit hex colour, e.g. `#ff0000`.
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* canvas.setColour("#ff0000"); // foreground
|
|
513
|
+
* canvas.setColour("#ffffff", "background");
|
|
514
|
+
*/
|
|
515
|
+
setColour(colour: string, slot?: ColourSlot): void;
|
|
516
|
+
/**
|
|
517
|
+
* Select which colour is configured, e.g. by a colour picker or the colour sample tool. One colour is always selected.
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* canvas.selectColour("background");
|
|
521
|
+
*/
|
|
522
|
+
selectColour(slot: ColourSlot): void;
|
|
473
523
|
/**
|
|
474
|
-
*
|
|
524
|
+
* Swap the foreground and background colours. The selected colour stays the same slot.
|
|
475
525
|
*/
|
|
476
|
-
|
|
526
|
+
swapColours(): void;
|
|
477
527
|
/**
|
|
478
528
|
* Change the canvas size, from 1 to the maximum canvas size (`maxCanvasSize` option). Replaces all layers.
|
|
479
529
|
*/
|