@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
|
@@ -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";
|
|
@@ -13,9 +15,15 @@ import Layer, { createOffscreenCanvas } from "../Layer/index.js";
|
|
|
13
15
|
import LayerAutosave from "../LayerAutosave/index.js";
|
|
14
16
|
import { brushPresets, eraserPresets } from "../../brushes/index.js";
|
|
15
17
|
import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
|
|
16
|
-
import
|
|
18
|
+
import "../../presets/index.js";
|
|
17
19
|
import CustomPresets from "../CustomPresets/index.js";
|
|
20
|
+
import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
|
|
18
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";
|
|
19
27
|
import ExternalSheets from "../ExternalSheets/index.js";
|
|
20
28
|
import PatchSender from "../../messages/PatchSender/index.js";
|
|
21
29
|
import { createPatchMessage, validateCanvasMessage } from "../../messages/index.js";
|
|
@@ -27,22 +35,6 @@ import { DOCUMENT_FORMAT } from "../../document/schema.js";
|
|
|
27
35
|
import { isValidDocument } from "../../document/index.js";
|
|
28
36
|
import { averageColour, flattenArea, pixelRect, readPixels } from "../../rendering/sample/index.js";
|
|
29
37
|
// src/lib/classes/PaintCanvas/index.ts
|
|
30
|
-
/** Drawing and navigation tools */
|
|
31
|
-
var Tool = /* @__PURE__ */ function(Tool) {
|
|
32
|
-
Tool["Brush"] = "brush";
|
|
33
|
-
Tool["Eraser"] = "eraser";
|
|
34
|
-
/** Move, scale and rotate */
|
|
35
|
-
Tool["Move"] = "move";
|
|
36
|
-
Tool["RectangleLasso"] = "rectangle-lasso";
|
|
37
|
-
Tool["CircleLasso"] = "circle-lasso";
|
|
38
|
-
Tool["PolygonLasso"] = "polygon-lasso";
|
|
39
|
-
Tool["FreeformLasso"] = "freeform-lasso";
|
|
40
|
-
Tool["Pan"] = "pan";
|
|
41
|
-
Tool["Zoom"] = "zoom";
|
|
42
|
-
/** Set the current colour from the canvas (eyedropper) */
|
|
43
|
-
Tool["ColourSample"] = "colour-sample";
|
|
44
|
-
return Tool;
|
|
45
|
-
}({});
|
|
46
38
|
/** Toggleable panels */
|
|
47
39
|
var Panel = /* @__PURE__ */ function(Panel) {
|
|
48
40
|
Panel["Brush"] = "brush";
|
|
@@ -50,19 +42,23 @@ var Panel = /* @__PURE__ */ function(Panel) {
|
|
|
50
42
|
Panel["Layers"] = "layers";
|
|
51
43
|
/** The implementer's own app menu */
|
|
52
44
|
Panel["Menu"] = "menu";
|
|
45
|
+
/** The colour palette: the foreground and background colours, their values and the saved colours */
|
|
46
|
+
Panel["Palette"] = "palette";
|
|
53
47
|
return Panel;
|
|
54
48
|
}({});
|
|
55
49
|
/** Panels opened by tools */
|
|
56
50
|
var TOOL_PANELS = {
|
|
57
|
-
[
|
|
58
|
-
[
|
|
51
|
+
[Tool.Brush]: "brush",
|
|
52
|
+
[Tool.Eraser]: "eraser"
|
|
59
53
|
};
|
|
60
54
|
/** Canvas width and height when none is given */
|
|
61
55
|
var DEFAULT_CANVAS_SIZE = 2e3;
|
|
62
56
|
/** Largest canvas width or height allowed when the `maxCanvasSize` option isn't given */
|
|
63
57
|
var MAX_CANVAS_SIZE = 2e3;
|
|
64
|
-
/**
|
|
58
|
+
/** Foreground colour to start with */
|
|
65
59
|
var DEFAULT_COLOUR = "#000000";
|
|
60
|
+
/** Background colour to start with */
|
|
61
|
+
var DEFAULT_BACKGROUND_COLOUR = "#ffffff";
|
|
66
62
|
/** Owner of the custom presets when the `ownerId` option isn't given */
|
|
67
63
|
var DEFAULT_OWNER_ID = "local";
|
|
68
64
|
/** Author name of presets made with `newPreset` when the `presetAuthor` option isn't given */
|
|
@@ -72,10 +68,10 @@ var DEFAULT_NAMES = {
|
|
|
72
68
|
app: BUILT_IN_AUTHOR,
|
|
73
69
|
filePrefix: "paintcanvas",
|
|
74
70
|
presetFileFormat: BRUSH_PRESET_FILE_FORMAT,
|
|
71
|
+
paletteFileFormat: PALETTE_FILE_FORMAT,
|
|
72
|
+
settingsFileFormat: SETTINGS_FILE_FORMAT,
|
|
75
73
|
unknownAuthor: UNKNOWN_AUTHOR
|
|
76
74
|
};
|
|
77
|
-
/** Pattern for colour input values */
|
|
78
|
-
var COLOUR_PATTERN = /^#[0-9a-f]{6}$/i;
|
|
79
75
|
/**
|
|
80
76
|
* A canvas's pixels as a PNG data URL.
|
|
81
77
|
*/
|
|
@@ -97,7 +93,7 @@ var decodePatch = async (patch) => {
|
|
|
97
93
|
return new ImageData(await decodeRgbaDeflate(patch.data, patch.coords), width, height);
|
|
98
94
|
};
|
|
99
95
|
/**
|
|
100
|
-
* 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,
|
|
101
97
|
* selection, transforms, clipboard, undo/redo history, viewport and input settings. Where its data is kept and who it
|
|
102
98
|
* is shared with are up to the implementer (see `onMessage` and `receive`).
|
|
103
99
|
*
|
|
@@ -108,6 +104,7 @@ var decodePatch = async (patch) => {
|
|
|
108
104
|
* const canvas = new PaintCanvas({ width: 1000, height: 800 });
|
|
109
105
|
* canvas.selectTool(Tool.Brush, { fromToolbar: true });
|
|
110
106
|
* canvas.setColour("#ff0000");
|
|
107
|
+
* canvas.setColour("#ffffff", "background");
|
|
111
108
|
*/
|
|
112
109
|
var PaintCanvas = class extends EventTarget {
|
|
113
110
|
/** Brush settings */
|
|
@@ -128,10 +125,14 @@ var PaintCanvas = class extends EventTarget {
|
|
|
128
125
|
background = new BackgroundWork(this.strokeLog);
|
|
129
126
|
/** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
|
|
130
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();
|
|
131
132
|
/** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
|
|
132
133
|
pointer;
|
|
133
134
|
/** Selected tool */
|
|
134
|
-
tool =
|
|
135
|
+
tool = Tool.Brush;
|
|
135
136
|
/** Tool used temporarily while a key is held (e.g. Space for pan), overriding the selected tool */
|
|
136
137
|
heldTool = null;
|
|
137
138
|
/** Open panel, if any */
|
|
@@ -185,7 +186,11 @@ var PaintCanvas = class extends EventTarget {
|
|
|
185
186
|
sendingStrokes = false;
|
|
186
187
|
/** Whether a stroke from elsewhere is being drawn (it isn't sent again) */
|
|
187
188
|
replaying = false;
|
|
188
|
-
|
|
189
|
+
currentColours = {
|
|
190
|
+
foreground: DEFAULT_COLOUR,
|
|
191
|
+
background: DEFAULT_BACKGROUND_COLOUR
|
|
192
|
+
};
|
|
193
|
+
currentSelectedColour = "foreground";
|
|
189
194
|
/** Image data copied with cut or copy, and the area it was copied from */
|
|
190
195
|
clipboard;
|
|
191
196
|
/** Drawings from elsewhere */
|
|
@@ -641,7 +646,7 @@ var PaintCanvas = class extends EventTarget {
|
|
|
641
646
|
*/
|
|
642
647
|
syncTransform() {
|
|
643
648
|
const transformer = this.transformer;
|
|
644
|
-
if (this.tool !==
|
|
649
|
+
if (this.tool !== Tool.Move) {
|
|
645
650
|
if (transformer.pending) this.commitTransform();
|
|
646
651
|
else if (transformer.active) {
|
|
647
652
|
transformer.reset();
|
|
@@ -715,20 +720,51 @@ var PaintCanvas = class extends EventTarget {
|
|
|
715
720
|
return area;
|
|
716
721
|
}
|
|
717
722
|
/**
|
|
718
|
-
* Copy the masked image data of the selected layer to the clipboard, then
|
|
719
|
-
* 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`).
|
|
720
725
|
*/
|
|
721
726
|
cut() {
|
|
722
727
|
const area = this.copy();
|
|
723
|
-
if (area) this.
|
|
728
|
+
if (area) this.clearMasked(area);
|
|
724
729
|
}
|
|
725
730
|
/**
|
|
726
|
-
* 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
|
|
727
733
|
* image data inside it; does nothing during a stroke or while a transform is pending.
|
|
728
734
|
*/
|
|
729
735
|
deleteMasked() {
|
|
730
736
|
const area = this.maskedArea();
|
|
731
|
-
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
|
+
};
|
|
732
768
|
}
|
|
733
769
|
/**
|
|
734
770
|
* The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
|
|
@@ -741,12 +777,18 @@ var PaintCanvas = class extends EventTarget {
|
|
|
741
777
|
return contentBounds(this.layerStack.active.context.getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
|
|
742
778
|
}
|
|
743
779
|
/**
|
|
744
|
-
* 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.
|
|
745
782
|
*/
|
|
746
|
-
|
|
783
|
+
clearMasked(area) {
|
|
784
|
+
const layer = this.layerStack.active;
|
|
785
|
+
if (layer.alphaLock) {
|
|
786
|
+
this.paintArea(area, this.currentColours.background);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
747
789
|
const mask = this.currentSelection.mask;
|
|
748
790
|
const maskPiece = copyArea(this.createCanvas, mask, area);
|
|
749
|
-
this.layerStack.changeImage(
|
|
791
|
+
this.layerStack.changeImage(layer.id, area, (ctx) => {
|
|
750
792
|
ctx.save();
|
|
751
793
|
ctx.globalCompositeOperation = "destination-out";
|
|
752
794
|
ctx.drawImage(maskPiece, area.x, area.y);
|
|
@@ -754,6 +796,28 @@ var PaintCanvas = class extends EventTarget {
|
|
|
754
796
|
});
|
|
755
797
|
}
|
|
756
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
|
+
/**
|
|
757
821
|
* Show the clipboard's image data over the selected layer where it was copied from, and select the Move tool to place,
|
|
758
822
|
* scale and rotate it. It is written into the layer (and sent) on Enter or when switching to another tool; Escape
|
|
759
823
|
* discards it. A pending transform is applied first.
|
|
@@ -762,7 +826,7 @@ var PaintCanvas = class extends EventTarget {
|
|
|
762
826
|
const clipboard = this.clipboard;
|
|
763
827
|
if (!clipboard || this.strokeEngine.drawing) return;
|
|
764
828
|
if (this.transformer.pending) this.commitTransform();
|
|
765
|
-
this.selectTool(
|
|
829
|
+
this.selectTool(Tool.Move);
|
|
766
830
|
this.transformer.float(this.layerStack.active, clipboard.image, clipboard.area);
|
|
767
831
|
}
|
|
768
832
|
/**
|
|
@@ -826,16 +890,53 @@ var PaintCanvas = class extends EventTarget {
|
|
|
826
890
|
get layers() {
|
|
827
891
|
return this.layerStack;
|
|
828
892
|
}
|
|
829
|
-
/** Current
|
|
893
|
+
/** Current foreground colour as a hex string, e.g. `#000000` (the same as `colours.foreground`) */
|
|
830
894
|
get colour() {
|
|
831
|
-
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;
|
|
832
904
|
}
|
|
833
905
|
/**
|
|
834
|
-
* Set the
|
|
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");
|
|
835
911
|
*/
|
|
836
|
-
setColour(colour) {
|
|
837
|
-
if (!
|
|
838
|
-
this.
|
|
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();
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
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");
|
|
925
|
+
*/
|
|
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
|
+
};
|
|
839
940
|
this.notify();
|
|
840
941
|
}
|
|
841
942
|
/**
|
|
@@ -962,4 +1063,4 @@ var PaintCanvas = class extends EventTarget {
|
|
|
962
1063
|
this.dispatchEvent(new Event("change"));
|
|
963
1064
|
}
|
|
964
1065
|
};
|
|
965
|
-
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 };
|