@lvl99dotcom/paintcanvas 0.3.1 → 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 +141 -41
- 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 +121 -171
- 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/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/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 +18 -14
- 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/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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import StrokeLog from "../StrokeLog/index.js";
|
|
2
|
+
import { Tool } from "./tool.js";
|
|
3
|
+
import { isHexColour } from "../../utils/colour/index.js";
|
|
2
4
|
import BackgroundWork from "../BackgroundWork/index.js";
|
|
3
5
|
import BrushSettings from "../BrushSettings/index.js";
|
|
4
6
|
import LayerStack from "../LayerStack/index.js";
|
|
@@ -17,6 +19,11 @@ import "../../presets/index.js";
|
|
|
17
19
|
import CustomPresets from "../CustomPresets/index.js";
|
|
18
20
|
import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
|
|
19
21
|
import InputSettings from "../InputSettings/index.js";
|
|
22
|
+
import Palette from "../Palette/index.js";
|
|
23
|
+
import ShortcutSettings from "../ShortcutSettings/index.js";
|
|
24
|
+
import { PALETTE_FILE_FORMAT } from "../../palette/schema.js";
|
|
25
|
+
import "../../palette/index.js";
|
|
26
|
+
import { SETTINGS_FILE_FORMAT } from "../../settings/schema.js";
|
|
20
27
|
import ExternalSheets from "../ExternalSheets/index.js";
|
|
21
28
|
import PatchSender from "../../messages/PatchSender/index.js";
|
|
22
29
|
import { createPatchMessage, validateCanvasMessage } from "../../messages/index.js";
|
|
@@ -28,22 +35,6 @@ import { DOCUMENT_FORMAT } from "../../document/schema.js";
|
|
|
28
35
|
import { isValidDocument } from "../../document/index.js";
|
|
29
36
|
import { averageColour, flattenArea, pixelRect, readPixels } from "../../rendering/sample/index.js";
|
|
30
37
|
// src/lib/classes/PaintCanvas/index.ts
|
|
31
|
-
/** Drawing and navigation tools */
|
|
32
|
-
var Tool = /* @__PURE__ */ function(Tool) {
|
|
33
|
-
Tool["Brush"] = "brush";
|
|
34
|
-
Tool["Eraser"] = "eraser";
|
|
35
|
-
/** Move, scale and rotate */
|
|
36
|
-
Tool["Move"] = "move";
|
|
37
|
-
Tool["RectangleLasso"] = "rectangle-lasso";
|
|
38
|
-
Tool["CircleLasso"] = "circle-lasso";
|
|
39
|
-
Tool["PolygonLasso"] = "polygon-lasso";
|
|
40
|
-
Tool["FreeformLasso"] = "freeform-lasso";
|
|
41
|
-
Tool["Pan"] = "pan";
|
|
42
|
-
Tool["Zoom"] = "zoom";
|
|
43
|
-
/** Set the current colour from the canvas (eyedropper) */
|
|
44
|
-
Tool["ColourSample"] = "colour-sample";
|
|
45
|
-
return Tool;
|
|
46
|
-
}({});
|
|
47
38
|
/** Toggleable panels */
|
|
48
39
|
var Panel = /* @__PURE__ */ function(Panel) {
|
|
49
40
|
Panel["Brush"] = "brush";
|
|
@@ -51,19 +42,23 @@ var Panel = /* @__PURE__ */ function(Panel) {
|
|
|
51
42
|
Panel["Layers"] = "layers";
|
|
52
43
|
/** The implementer's own app menu */
|
|
53
44
|
Panel["Menu"] = "menu";
|
|
45
|
+
/** The colour palette: the foreground and background colours, their values and the saved colours */
|
|
46
|
+
Panel["Palette"] = "palette";
|
|
54
47
|
return Panel;
|
|
55
48
|
}({});
|
|
56
49
|
/** Panels opened by tools */
|
|
57
50
|
var TOOL_PANELS = {
|
|
58
|
-
[
|
|
59
|
-
[
|
|
51
|
+
[Tool.Brush]: "brush",
|
|
52
|
+
[Tool.Eraser]: "eraser"
|
|
60
53
|
};
|
|
61
54
|
/** Canvas width and height when none is given */
|
|
62
55
|
var DEFAULT_CANVAS_SIZE = 2e3;
|
|
63
56
|
/** Largest canvas width or height allowed when the `maxCanvasSize` option isn't given */
|
|
64
57
|
var MAX_CANVAS_SIZE = 2e3;
|
|
65
|
-
/**
|
|
58
|
+
/** Foreground colour to start with */
|
|
66
59
|
var DEFAULT_COLOUR = "#000000";
|
|
60
|
+
/** Background colour to start with */
|
|
61
|
+
var DEFAULT_BACKGROUND_COLOUR = "#ffffff";
|
|
67
62
|
/** Owner of the custom presets when the `ownerId` option isn't given */
|
|
68
63
|
var DEFAULT_OWNER_ID = "local";
|
|
69
64
|
/** Author name of presets made with `newPreset` when the `presetAuthor` option isn't given */
|
|
@@ -73,10 +68,10 @@ var DEFAULT_NAMES = {
|
|
|
73
68
|
app: BUILT_IN_AUTHOR,
|
|
74
69
|
filePrefix: "paintcanvas",
|
|
75
70
|
presetFileFormat: BRUSH_PRESET_FILE_FORMAT,
|
|
71
|
+
paletteFileFormat: PALETTE_FILE_FORMAT,
|
|
72
|
+
settingsFileFormat: SETTINGS_FILE_FORMAT,
|
|
76
73
|
unknownAuthor: UNKNOWN_AUTHOR
|
|
77
74
|
};
|
|
78
|
-
/** Pattern for colour input values */
|
|
79
|
-
var COLOUR_PATTERN = /^#[0-9a-f]{6}$/i;
|
|
80
75
|
/**
|
|
81
76
|
* A canvas's pixels as a PNG data URL.
|
|
82
77
|
*/
|
|
@@ -98,7 +93,7 @@ var decodePatch = async (patch) => {
|
|
|
98
93
|
return new ImageData(await decodeRgbaDeflate(patch.data, patch.coords), width, height);
|
|
99
94
|
};
|
|
100
95
|
/**
|
|
101
|
-
* The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool,
|
|
96
|
+
* The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool, foreground and background colours,
|
|
102
97
|
* selection, transforms, clipboard, undo/redo history, viewport and input settings. Where its data is kept and who it
|
|
103
98
|
* is shared with are up to the implementer (see `onMessage` and `receive`).
|
|
104
99
|
*
|
|
@@ -109,6 +104,7 @@ var decodePatch = async (patch) => {
|
|
|
109
104
|
* const canvas = new PaintCanvas({ width: 1000, height: 800 });
|
|
110
105
|
* canvas.selectTool(Tool.Brush, { fromToolbar: true });
|
|
111
106
|
* canvas.setColour("#ff0000");
|
|
107
|
+
* canvas.setColour("#ffffff", "background");
|
|
112
108
|
*/
|
|
113
109
|
var PaintCanvas = class extends EventTarget {
|
|
114
110
|
/** Brush settings */
|
|
@@ -129,10 +125,14 @@ var PaintCanvas = class extends EventTarget {
|
|
|
129
125
|
background = new BackgroundWork(this.strokeLog);
|
|
130
126
|
/** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
|
|
131
127
|
input = new InputSettings();
|
|
128
|
+
/** The user's saved colours, shown as swatches */
|
|
129
|
+
palette = new Palette();
|
|
130
|
+
/** The user's keyboard shortcuts: each configurable action's key combination */
|
|
131
|
+
shortcuts = new ShortcutSettings();
|
|
132
132
|
/** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
|
|
133
133
|
pointer;
|
|
134
134
|
/** Selected tool */
|
|
135
|
-
tool =
|
|
135
|
+
tool = Tool.Brush;
|
|
136
136
|
/** Tool used temporarily while a key is held (e.g. Space for pan), overriding the selected tool */
|
|
137
137
|
heldTool = null;
|
|
138
138
|
/** Open panel, if any */
|
|
@@ -186,7 +186,11 @@ var PaintCanvas = class extends EventTarget {
|
|
|
186
186
|
sendingStrokes = false;
|
|
187
187
|
/** Whether a stroke from elsewhere is being drawn (it isn't sent again) */
|
|
188
188
|
replaying = false;
|
|
189
|
-
|
|
189
|
+
currentColours = {
|
|
190
|
+
foreground: DEFAULT_COLOUR,
|
|
191
|
+
background: DEFAULT_BACKGROUND_COLOUR
|
|
192
|
+
};
|
|
193
|
+
currentSelectedColour = "foreground";
|
|
190
194
|
/** Image data copied with cut or copy, and the area it was copied from */
|
|
191
195
|
clipboard;
|
|
192
196
|
/** Drawings from elsewhere */
|
|
@@ -642,7 +646,7 @@ var PaintCanvas = class extends EventTarget {
|
|
|
642
646
|
*/
|
|
643
647
|
syncTransform() {
|
|
644
648
|
const transformer = this.transformer;
|
|
645
|
-
if (this.tool !==
|
|
649
|
+
if (this.tool !== Tool.Move) {
|
|
646
650
|
if (transformer.pending) this.commitTransform();
|
|
647
651
|
else if (transformer.active) {
|
|
648
652
|
transformer.reset();
|
|
@@ -716,20 +720,51 @@ var PaintCanvas = class extends EventTarget {
|
|
|
716
720
|
return area;
|
|
717
721
|
}
|
|
718
722
|
/**
|
|
719
|
-
* Copy the masked image data of the selected layer to the clipboard, then
|
|
720
|
-
* action.
|
|
723
|
+
* Copy the masked image data of the selected layer to the clipboard, then remove it from the layer as one undoable
|
|
724
|
+
* action (see `deleteMasked`).
|
|
721
725
|
*/
|
|
722
726
|
cut() {
|
|
723
727
|
const area = this.copy();
|
|
724
|
-
if (area) this.
|
|
728
|
+
if (area) this.clearMasked(area);
|
|
725
729
|
}
|
|
726
730
|
/**
|
|
727
|
-
* Erase the masked image data from the selected layer as one undoable action (Delete or Backspace).
|
|
731
|
+
* Erase the masked image data from the selected layer as one undoable action (Delete or Backspace). When the layer's
|
|
732
|
+
* alpha lock is on, it is painted with the background colour instead, keeping its transparency. Needs a mask with
|
|
728
733
|
* image data inside it; does nothing during a stroke or while a transform is pending.
|
|
729
734
|
*/
|
|
730
735
|
deleteMasked() {
|
|
731
736
|
const area = this.maskedArea();
|
|
732
|
-
if (area) this.
|
|
737
|
+
if (area) this.clearMasked(area);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Fill the selected layer with the foreground or background colour as one undoable action: only the masked area when
|
|
741
|
+
* there is a mask (partly where it is partly), and only existing pixels when the layer's alpha lock is on. Does
|
|
742
|
+
* nothing during a stroke or while a transform is pending.
|
|
743
|
+
*
|
|
744
|
+
* @example
|
|
745
|
+
* canvas.fill("foreground");
|
|
746
|
+
*/
|
|
747
|
+
fill(slot) {
|
|
748
|
+
if (this.strokeEngine.drawing || this.transformer.pending) return;
|
|
749
|
+
const area = this.fillArea();
|
|
750
|
+
if (area) this.paintArea(area, this.currentColours[slot]);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* The area a fill changes: the mask's bounds (or the whole canvas without a mask), cut to the selected layer's image
|
|
754
|
+
* data when its alpha lock is on. None if that is empty.
|
|
755
|
+
*/
|
|
756
|
+
fillArea() {
|
|
757
|
+
const { width, height } = this;
|
|
758
|
+
const layer = this.layerStack.active;
|
|
759
|
+
const mask = this.currentSelection.mask;
|
|
760
|
+
const maskData = mask ? mask.getContext("2d").getImageData(0, 0, width, height).data : void 0;
|
|
761
|
+
if (layer.alphaLock) return contentBounds(layer.context.getImageData(0, 0, width, height).data, width, height, maskData);
|
|
762
|
+
return maskData ? contentBounds(maskData, width, height) : {
|
|
763
|
+
x: 0,
|
|
764
|
+
y: 0,
|
|
765
|
+
width,
|
|
766
|
+
height
|
|
767
|
+
};
|
|
733
768
|
}
|
|
734
769
|
/**
|
|
735
770
|
* The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
|
|
@@ -742,12 +777,18 @@ var PaintCanvas = class extends EventTarget {
|
|
|
742
777
|
return contentBounds(this.layerStack.active.context.getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
|
|
743
778
|
}
|
|
744
779
|
/**
|
|
745
|
-
* Erase the mask's part of an area from the selected layer,
|
|
780
|
+
* Erase the mask's part of an area from the selected layer, or paint it with the background colour when the layer's
|
|
781
|
+
* alpha lock is on, as one undoable action.
|
|
746
782
|
*/
|
|
747
|
-
|
|
783
|
+
clearMasked(area) {
|
|
784
|
+
const layer = this.layerStack.active;
|
|
785
|
+
if (layer.alphaLock) {
|
|
786
|
+
this.paintArea(area, this.currentColours.background);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
748
789
|
const mask = this.currentSelection.mask;
|
|
749
790
|
const maskPiece = copyArea(this.createCanvas, mask, area);
|
|
750
|
-
this.layerStack.changeImage(
|
|
791
|
+
this.layerStack.changeImage(layer.id, area, (ctx) => {
|
|
751
792
|
ctx.save();
|
|
752
793
|
ctx.globalCompositeOperation = "destination-out";
|
|
753
794
|
ctx.drawImage(maskPiece, area.x, area.y);
|
|
@@ -755,6 +796,28 @@ var PaintCanvas = class extends EventTarget {
|
|
|
755
796
|
});
|
|
756
797
|
}
|
|
757
798
|
/**
|
|
799
|
+
* Paint a colour over an area of the selected layer, cut to the mask if there is one (partly where it is partly), as
|
|
800
|
+
* one undoable action. Only existing pixels are painted when the layer's alpha lock is on.
|
|
801
|
+
*/
|
|
802
|
+
paintArea(area, colour) {
|
|
803
|
+
const layer = this.layerStack.active;
|
|
804
|
+
const mask = this.currentSelection.mask;
|
|
805
|
+
const paint = this.createCanvas(area.width, area.height);
|
|
806
|
+
const paintCtx = paint.getContext("2d");
|
|
807
|
+
if (mask) {
|
|
808
|
+
paintCtx.drawImage(copyArea(this.createCanvas, mask, area), 0, 0);
|
|
809
|
+
paintCtx.globalCompositeOperation = "source-in";
|
|
810
|
+
}
|
|
811
|
+
paintCtx.fillStyle = colour;
|
|
812
|
+
paintCtx.fillRect(0, 0, area.width, area.height);
|
|
813
|
+
this.layerStack.changeImage(layer.id, area, (ctx) => {
|
|
814
|
+
ctx.save();
|
|
815
|
+
ctx.globalCompositeOperation = layer.alphaLock ? "source-atop" : "source-over";
|
|
816
|
+
ctx.drawImage(paint, area.x, area.y);
|
|
817
|
+
ctx.restore();
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
758
821
|
* Show the clipboard's image data over the selected layer where it was copied from, and select the Move tool to place,
|
|
759
822
|
* scale and rotate it. It is written into the layer (and sent) on Enter or when switching to another tool; Escape
|
|
760
823
|
* discards it. A pending transform is applied first.
|
|
@@ -763,7 +826,7 @@ var PaintCanvas = class extends EventTarget {
|
|
|
763
826
|
const clipboard = this.clipboard;
|
|
764
827
|
if (!clipboard || this.strokeEngine.drawing) return;
|
|
765
828
|
if (this.transformer.pending) this.commitTransform();
|
|
766
|
-
this.selectTool(
|
|
829
|
+
this.selectTool(Tool.Move);
|
|
767
830
|
this.transformer.float(this.layerStack.active, clipboard.image, clipboard.area);
|
|
768
831
|
}
|
|
769
832
|
/**
|
|
@@ -827,16 +890,53 @@ var PaintCanvas = class extends EventTarget {
|
|
|
827
890
|
get layers() {
|
|
828
891
|
return this.layerStack;
|
|
829
892
|
}
|
|
830
|
-
/** Current
|
|
893
|
+
/** Current foreground colour as a hex string, e.g. `#000000` (the same as `colours.foreground`) */
|
|
831
894
|
get colour() {
|
|
832
|
-
return this.
|
|
895
|
+
return this.currentColours.foreground;
|
|
896
|
+
}
|
|
897
|
+
/** Current foreground and background colours as hex strings, e.g. `{ foreground: "#000000", background: "#ffffff" }` */
|
|
898
|
+
get colours() {
|
|
899
|
+
return this.currentColours;
|
|
900
|
+
}
|
|
901
|
+
/** Which colour is selected for configuration, e.g. by a colour picker or the colour sample tool ("foreground" by default) */
|
|
902
|
+
get selectedColour() {
|
|
903
|
+
return this.currentSelectedColour;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Set the foreground colour, or the background colour. Must be a 6-digit hex colour, e.g. `#ff0000`.
|
|
907
|
+
*
|
|
908
|
+
* @example
|
|
909
|
+
* canvas.setColour("#ff0000"); // foreground
|
|
910
|
+
* canvas.setColour("#ffffff", "background");
|
|
911
|
+
*/
|
|
912
|
+
setColour(colour, slot = "foreground") {
|
|
913
|
+
if (!isHexColour(colour)) throw new Error(`Invalid colour ${colour}`);
|
|
914
|
+
this.currentColours = {
|
|
915
|
+
...this.currentColours,
|
|
916
|
+
[slot]: colour.toLowerCase()
|
|
917
|
+
};
|
|
918
|
+
this.notify();
|
|
833
919
|
}
|
|
834
920
|
/**
|
|
835
|
-
*
|
|
921
|
+
* Select which colour is configured, e.g. by a colour picker or the colour sample tool. One colour is always selected.
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* canvas.selectColour("background");
|
|
836
925
|
*/
|
|
837
|
-
|
|
838
|
-
if (
|
|
839
|
-
this.
|
|
926
|
+
selectColour(slot) {
|
|
927
|
+
if (slot === this.currentSelectedColour) return;
|
|
928
|
+
this.currentSelectedColour = slot;
|
|
929
|
+
this.notify();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Swap the foreground and background colours. The selected colour stays the same slot.
|
|
933
|
+
*/
|
|
934
|
+
swapColours() {
|
|
935
|
+
const { foreground, background } = this.currentColours;
|
|
936
|
+
this.currentColours = {
|
|
937
|
+
foreground: background,
|
|
938
|
+
background: foreground
|
|
939
|
+
};
|
|
840
940
|
this.notify();
|
|
841
941
|
}
|
|
842
942
|
/**
|
|
@@ -963,4 +1063,4 @@ var PaintCanvas = class extends EventTarget {
|
|
|
963
1063
|
this.dispatchEvent(new Event("change"));
|
|
964
1064
|
}
|
|
965
1065
|
};
|
|
966
|
-
export { DEFAULT_AUTHOR, DEFAULT_CANVAS_SIZE, DEFAULT_COLOUR, DEFAULT_NAMES, DEFAULT_OWNER_ID, MAX_CANVAS_SIZE, Panel, TOOL_PANELS, Tool, PaintCanvas as default };
|
|
1066
|
+
export { DEFAULT_AUTHOR, DEFAULT_BACKGROUND_COLOUR, DEFAULT_CANVAS_SIZE, DEFAULT_COLOUR, DEFAULT_NAMES, DEFAULT_OWNER_ID, MAX_CANVAS_SIZE, Panel, TOOL_PANELS, Tool, PaintCanvas as default, isHexColour };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Drawing and navigation tools */
|
|
2
|
+
export declare enum Tool {
|
|
3
|
+
Brush = "brush",
|
|
4
|
+
Eraser = "eraser",
|
|
5
|
+
/** Move, scale and rotate */
|
|
6
|
+
Move = "move",
|
|
7
|
+
RectangleLasso = "rectangle-lasso",
|
|
8
|
+
CircleLasso = "circle-lasso",
|
|
9
|
+
PolygonLasso = "polygon-lasso",
|
|
10
|
+
FreeformLasso = "freeform-lasso",
|
|
11
|
+
Pan = "pan",
|
|
12
|
+
Zoom = "zoom",
|
|
13
|
+
/** Set the current colour from the canvas (eyedropper) */
|
|
14
|
+
ColourSample = "colour-sample"
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/lib/classes/PaintCanvas/tool.ts
|
|
2
|
+
/** Drawing and navigation tools */
|
|
3
|
+
var Tool = /* @__PURE__ */ function(Tool) {
|
|
4
|
+
Tool["Brush"] = "brush";
|
|
5
|
+
Tool["Eraser"] = "eraser";
|
|
6
|
+
/** Move, scale and rotate */
|
|
7
|
+
Tool["Move"] = "move";
|
|
8
|
+
Tool["RectangleLasso"] = "rectangle-lasso";
|
|
9
|
+
Tool["CircleLasso"] = "circle-lasso";
|
|
10
|
+
Tool["PolygonLasso"] = "polygon-lasso";
|
|
11
|
+
Tool["FreeformLasso"] = "freeform-lasso";
|
|
12
|
+
Tool["Pan"] = "pan";
|
|
13
|
+
Tool["Zoom"] = "zoom";
|
|
14
|
+
/** Set the current colour from the canvas (eyedropper) */
|
|
15
|
+
Tool["ColourSample"] = "colour-sample";
|
|
16
|
+
return Tool;
|
|
17
|
+
}({});
|
|
18
|
+
export { Tool };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep only valid colours from an untrusted value (e.g. stored or imported): lowercase `#rrggbb` strings, each once, up
|
|
3
|
+
* to `MAX_PALETTE_COLOURS`. Undefined if the value isn't an array.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* sanitisePalette(["#FF0000", "red", "#ff0000", "#00ff00"]); // ["#ff0000", "#00ff00"]
|
|
7
|
+
* sanitisePalette("#ff0000"); // undefined
|
|
8
|
+
*/
|
|
9
|
+
export declare const sanitisePalette: (input: unknown) => string[] | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The user's saved colours, shown as swatches. Each colour is kept once, as lowercase `#rrggbb`, up to
|
|
12
|
+
* `MAX_PALETTE_COLOURS`. Kept in memory; the implementer saves and restores them (see `set` and the `change` event).
|
|
13
|
+
*
|
|
14
|
+
* Dispatches a `change` event when the colours change.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const palette = new Palette();
|
|
18
|
+
* palette.add("#123456");
|
|
19
|
+
* palette.colours.at(-1); // "#123456"
|
|
20
|
+
*/
|
|
21
|
+
export default class Palette extends EventTarget {
|
|
22
|
+
private values;
|
|
23
|
+
/** Start with some colours (invalid ones are left out), or `DEFAULT_PALETTE` */
|
|
24
|
+
constructor(colours?: readonly string[]);
|
|
25
|
+
/** The colours in order (read-only copy) */
|
|
26
|
+
get colours(): readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* Add a colour at the end. Returns whether it was added: not if it's already there, the palette is full, or it isn't a
|
|
29
|
+
* `#rrggbb` colour.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* palette.add("#FF0000"); // true, added as "#ff0000"
|
|
33
|
+
*/
|
|
34
|
+
add(colour: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Add colours at the end, e.g. from an imported palette file, leaving out ones already there. Returns how many were
|
|
37
|
+
* added.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* palette.merge(["#ff0000", "#00ff00"]); // 1, if "#ff0000" was already there
|
|
41
|
+
*/
|
|
42
|
+
merge(colours: readonly string[]): number;
|
|
43
|
+
/**
|
|
44
|
+
* Remove the colour at a position.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* palette.remove(0);
|
|
48
|
+
*/
|
|
49
|
+
remove(index: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Replace all the colours (e.g. when restoring saved ones), keeping only valid ones, and dispatch `change` if they
|
|
52
|
+
* changed.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* palette.set(["#000000", "#ffffff"]);
|
|
56
|
+
*/
|
|
57
|
+
set(colours: unknown): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { isHexColour } from "../../utils/colour/index.js";
|
|
2
|
+
import "../../palette/schema.js";
|
|
3
|
+
import { DEFAULT_PALETTE } from "../../palette/index.js";
|
|
4
|
+
// src/lib/classes/Palette/index.ts
|
|
5
|
+
/**
|
|
6
|
+
* Keep only valid colours from an untrusted value (e.g. stored or imported): lowercase `#rrggbb` strings, each once, up
|
|
7
|
+
* to `MAX_PALETTE_COLOURS`. Undefined if the value isn't an array.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* sanitisePalette(["#FF0000", "red", "#ff0000", "#00ff00"]); // ["#ff0000", "#00ff00"]
|
|
11
|
+
* sanitisePalette("#ff0000"); // undefined
|
|
12
|
+
*/
|
|
13
|
+
var sanitisePalette = (input) => {
|
|
14
|
+
if (!Array.isArray(input)) return void 0;
|
|
15
|
+
const colours = /* @__PURE__ */ new Set();
|
|
16
|
+
for (const value of input) {
|
|
17
|
+
if (colours.size >= 1e3) break;
|
|
18
|
+
if (isHexColour(value)) colours.add(value.toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
return [...colours];
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The user's saved colours, shown as swatches. Each colour is kept once, as lowercase `#rrggbb`, up to
|
|
24
|
+
* `MAX_PALETTE_COLOURS`. Kept in memory; the implementer saves and restores them (see `set` and the `change` event).
|
|
25
|
+
*
|
|
26
|
+
* Dispatches a `change` event when the colours change.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const palette = new Palette();
|
|
30
|
+
* palette.add("#123456");
|
|
31
|
+
* palette.colours.at(-1); // "#123456"
|
|
32
|
+
*/
|
|
33
|
+
var Palette = class extends EventTarget {
|
|
34
|
+
values;
|
|
35
|
+
/** Start with some colours (invalid ones are left out), or `DEFAULT_PALETTE` */
|
|
36
|
+
constructor(colours = DEFAULT_PALETTE) {
|
|
37
|
+
super();
|
|
38
|
+
this.values = sanitisePalette(colours);
|
|
39
|
+
}
|
|
40
|
+
/** The colours in order (read-only copy) */
|
|
41
|
+
get colours() {
|
|
42
|
+
return this.values;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Add a colour at the end. Returns whether it was added: not if it's already there, the palette is full, or it isn't a
|
|
46
|
+
* `#rrggbb` colour.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* palette.add("#FF0000"); // true, added as "#ff0000"
|
|
50
|
+
*/
|
|
51
|
+
add(colour) {
|
|
52
|
+
const before = this.values.length;
|
|
53
|
+
this.set([...this.values, colour]);
|
|
54
|
+
return this.values.length > before;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add colours at the end, e.g. from an imported palette file, leaving out ones already there. Returns how many were
|
|
58
|
+
* added.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* palette.merge(["#ff0000", "#00ff00"]); // 1, if "#ff0000" was already there
|
|
62
|
+
*/
|
|
63
|
+
merge(colours) {
|
|
64
|
+
const before = this.values.length;
|
|
65
|
+
this.set([...this.values, ...colours]);
|
|
66
|
+
return this.values.length - before;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Remove the colour at a position.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* palette.remove(0);
|
|
73
|
+
*/
|
|
74
|
+
remove(index) {
|
|
75
|
+
if (index < 0 || index >= this.values.length) return;
|
|
76
|
+
this.set(this.values.filter((_, i) => i !== index));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Replace all the colours (e.g. when restoring saved ones), keeping only valid ones, and dispatch `change` if they
|
|
80
|
+
* changed.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* palette.set(["#000000", "#ffffff"]);
|
|
84
|
+
*/
|
|
85
|
+
set(colours) {
|
|
86
|
+
const next = sanitisePalette(colours);
|
|
87
|
+
if (!next || next.length === this.values.length && next.every((colour, i) => colour === this.values[i])) return;
|
|
88
|
+
this.values = next;
|
|
89
|
+
this.dispatchEvent(new Event("change"));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
export { Palette as default, sanitisePalette };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type PaintCanvas from "../PaintCanvas";
|
|
2
|
+
import type { SettingsStoreLike } from "../../storage";
|
|
3
|
+
/** How long to wait after settings change before saving, so quick changes (e.g. dragging a colour slider) are saved together (milliseconds) */
|
|
4
|
+
export declare const SETTINGS_SAVE_DELAY_MS = 500;
|
|
5
|
+
/** Event dispatched when loading or saving settings fails (detail: the error) */
|
|
6
|
+
export declare const SETTINGS_AUTOSAVE_ERROR_EVENT = "error";
|
|
7
|
+
/** Options for `SettingsAutosave` */
|
|
8
|
+
export interface SettingsAutosaveOptions {
|
|
9
|
+
/** How long to wait after settings change before saving (default `SETTINGS_SAVE_DELAY_MS`) */
|
|
10
|
+
delayMs?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette and colours) in a store: the saved
|
|
14
|
+
* settings are restored when it starts (invalid ones are ignored), then changes are saved soon after they happen.
|
|
15
|
+
*
|
|
16
|
+
* Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* const autosave = new SettingsAutosave(canvas, new IndexedDbSettingsStore());
|
|
20
|
+
* await autosave.ready;
|
|
21
|
+
* autosave.stop(); // saves any pending change
|
|
22
|
+
*/
|
|
23
|
+
export default class SettingsAutosave extends EventTarget {
|
|
24
|
+
/** Where the settings are kept */
|
|
25
|
+
readonly store: SettingsStoreLike;
|
|
26
|
+
/** Resolves once the saved settings are restored (or there were none) */
|
|
27
|
+
readonly ready: Promise<void>;
|
|
28
|
+
private readonly app;
|
|
29
|
+
private readonly delayMs;
|
|
30
|
+
private timer?;
|
|
31
|
+
/** The settings last saved or restored, as JSON, to skip saving unchanged settings */
|
|
32
|
+
private saved?;
|
|
33
|
+
private stopped;
|
|
34
|
+
private readonly onChange;
|
|
35
|
+
constructor(app: PaintCanvas, store: SettingsStoreLike, options?: SettingsAutosaveOptions);
|
|
36
|
+
/** The canvas parts whose changes are saved */
|
|
37
|
+
private get sources();
|
|
38
|
+
/**
|
|
39
|
+
* Stop saving changes, saving any that are waiting.
|
|
40
|
+
*/
|
|
41
|
+
stop(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Save the settings now if they changed since they were last saved.
|
|
44
|
+
*/
|
|
45
|
+
save(): Promise<void>;
|
|
46
|
+
/** Restore the saved settings, then listen for changes */
|
|
47
|
+
private start;
|
|
48
|
+
/** Save soon, after changes stop for a moment */
|
|
49
|
+
private schedule;
|
|
50
|
+
/** Tell listeners loading or saving failed */
|
|
51
|
+
private fail;
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { applySettings, collectSettings, isValidSettings } from "../../settings/index.js";
|
|
2
|
+
// src/lib/classes/SettingsAutosave/index.ts
|
|
3
|
+
/** How long to wait after settings change before saving, so quick changes (e.g. dragging a colour slider) are saved together (milliseconds) */
|
|
4
|
+
var SETTINGS_SAVE_DELAY_MS = 500;
|
|
5
|
+
/** Event dispatched when loading or saving settings fails (detail: the error) */
|
|
6
|
+
var SETTINGS_AUTOSAVE_ERROR_EVENT = "error";
|
|
7
|
+
/**
|
|
8
|
+
* Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette and colours) in a store: the saved
|
|
9
|
+
* settings are restored when it starts (invalid ones are ignored), then changes are saved soon after they happen.
|
|
10
|
+
*
|
|
11
|
+
* Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const autosave = new SettingsAutosave(canvas, new IndexedDbSettingsStore());
|
|
15
|
+
* await autosave.ready;
|
|
16
|
+
* autosave.stop(); // saves any pending change
|
|
17
|
+
*/
|
|
18
|
+
var SettingsAutosave = class extends EventTarget {
|
|
19
|
+
/** Where the settings are kept */
|
|
20
|
+
store;
|
|
21
|
+
/** Resolves once the saved settings are restored (or there were none) */
|
|
22
|
+
ready;
|
|
23
|
+
app;
|
|
24
|
+
delayMs;
|
|
25
|
+
timer;
|
|
26
|
+
/** The settings last saved or restored, as JSON, to skip saving unchanged settings */
|
|
27
|
+
saved;
|
|
28
|
+
stopped = false;
|
|
29
|
+
onChange = () => this.schedule();
|
|
30
|
+
constructor(app, store, options = {}) {
|
|
31
|
+
super();
|
|
32
|
+
this.app = app;
|
|
33
|
+
this.store = store;
|
|
34
|
+
this.delayMs = options.delayMs ?? 500;
|
|
35
|
+
this.ready = this.start();
|
|
36
|
+
}
|
|
37
|
+
/** The canvas parts whose changes are saved */
|
|
38
|
+
get sources() {
|
|
39
|
+
const { app } = this;
|
|
40
|
+
return [
|
|
41
|
+
app,
|
|
42
|
+
app.input,
|
|
43
|
+
app.shortcuts,
|
|
44
|
+
app.palette
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stop saving changes, saving any that are waiting.
|
|
49
|
+
*/
|
|
50
|
+
stop() {
|
|
51
|
+
if (this.stopped) return;
|
|
52
|
+
this.stopped = true;
|
|
53
|
+
for (const source of this.sources) source.removeEventListener("change", this.onChange);
|
|
54
|
+
if (this.timer === void 0) return;
|
|
55
|
+
clearTimeout(this.timer);
|
|
56
|
+
this.save();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Save the settings now if they changed since they were last saved.
|
|
60
|
+
*/
|
|
61
|
+
async save() {
|
|
62
|
+
this.timer = void 0;
|
|
63
|
+
const settings = collectSettings(this.app);
|
|
64
|
+
const json = JSON.stringify(settings);
|
|
65
|
+
if (json === this.saved) return;
|
|
66
|
+
this.saved = json;
|
|
67
|
+
try {
|
|
68
|
+
await this.store.save(settings);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.saved = void 0;
|
|
71
|
+
this.fail(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Restore the saved settings, then listen for changes */
|
|
75
|
+
async start() {
|
|
76
|
+
try {
|
|
77
|
+
const result = isValidSettings(await this.store.load());
|
|
78
|
+
if (result.valid && !this.stopped) applySettings(this.app, result.value);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.fail(error);
|
|
81
|
+
}
|
|
82
|
+
if (this.stopped) return;
|
|
83
|
+
this.saved = JSON.stringify(collectSettings(this.app));
|
|
84
|
+
for (const source of this.sources) source.addEventListener("change", this.onChange);
|
|
85
|
+
}
|
|
86
|
+
/** Save soon, after changes stop for a moment */
|
|
87
|
+
schedule() {
|
|
88
|
+
clearTimeout(this.timer);
|
|
89
|
+
this.timer = setTimeout(() => void this.save(), this.delayMs);
|
|
90
|
+
}
|
|
91
|
+
/** Tell listeners loading or saving failed */
|
|
92
|
+
fail(error) {
|
|
93
|
+
this.dispatchEvent(new CustomEvent(SETTINGS_AUTOSAVE_ERROR_EVENT, { detail: error }));
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
export { SETTINGS_AUTOSAVE_ERROR_EVENT, SETTINGS_SAVE_DELAY_MS, SettingsAutosave as default };
|