@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.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
  3. package/dist/lib/brushes/StrokeEngine/index.js +26 -28
  4. package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
  5. package/dist/lib/classes/BrushSettings/index.js +35 -1
  6. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
  7. package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
  8. package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
  9. package/dist/lib/classes/PaintCanvas/index.js +143 -42
  10. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  11. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  12. package/dist/lib/classes/Palette/index.d.ts +58 -0
  13. package/dist/lib/classes/Palette/index.js +92 -0
  14. package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
  15. package/dist/lib/classes/SettingsAutosave/index.js +96 -0
  16. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  17. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  18. package/dist/lib/palette/index.d.ts +49 -0
  19. package/dist/lib/palette/index.js +82 -0
  20. package/dist/lib/palette/schema.d.ts +45 -0
  21. package/dist/lib/palette/schema.js +41 -0
  22. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  23. package/dist/lib/rendering/cursors/index.js +3 -1
  24. package/dist/lib/rendering/sample/index.js +3 -2
  25. package/dist/lib/settings/index.d.ts +63 -0
  26. package/dist/lib/settings/index.js +91 -0
  27. package/dist/lib/settings/schema.d.ts +150 -0
  28. package/dist/lib/settings/schema.js +102 -0
  29. package/dist/lib/storage/index.d.ts +36 -5
  30. package/dist/lib/storage/index.js +87 -29
  31. package/dist/lib/utils/colour/index.d.ts +72 -0
  32. package/dist/lib/utils/colour/index.js +154 -0
  33. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  34. package/dist/lib/utils/shortcuts/index.js +391 -145
  35. package/dist/lib/validation/validators.generated.d.ts +5 -0
  36. package/dist/lib/validation/validators.generated.js +872 -1
  37. package/dist/locales/en-GB.js +87 -1
  38. package/dist/locales/en-US.js +87 -1
  39. package/dist/locales/fr.js +87 -1
  40. package/dist/locales/ja.js +87 -1
  41. package/dist/locales/ko.js +87 -1
  42. package/dist/locales/zh-Hans.js +87 -1
  43. package/dist/locales/zh-Hant.js +87 -1
  44. package/dist/package.js +1 -1
  45. package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
  46. package/dist/ui/elements/PaintCanvas/index.js +128 -169
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  49. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  50. package/dist/ui/elements/PcBrushConfig/index.js +1 -0
  51. package/dist/ui/elements/PcButton/index.js +1 -0
  52. package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
  53. package/dist/ui/elements/PcCanvas/index.js +7 -5
  54. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  55. package/dist/ui/elements/PcColourPair/index.js +125 -0
  56. package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
  57. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  58. package/dist/ui/elements/PcDialog/index.js +1 -0
  59. package/dist/ui/elements/PcIconToggle/index.js +1 -0
  60. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  61. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  62. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  63. package/dist/ui/elements/PcMenu/index.d.ts +93 -0
  64. package/dist/ui/elements/PcMenu/index.js +204 -0
  65. package/dist/ui/elements/PcPane/index.d.ts +118 -0
  66. package/dist/ui/elements/PcPane/index.js +357 -0
  67. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  68. package/dist/ui/elements/PcPanel/index.js +76 -8
  69. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
  70. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +20 -15
  71. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  72. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  73. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  74. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  75. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  76. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  77. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  78. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  79. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  80. package/dist/ui/elements/PcThemePicker/index.js +1 -0
  81. package/dist/ui/elements/PcToast/index.js +1 -0
  82. package/dist/ui/elements/PcToolSettings/index.js +2 -1
  83. package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
  84. package/dist/ui/elements/PcToolbar/index.js +77 -56
  85. package/dist/ui/elements/PcToolbar/style.js +1 -1
  86. package/dist/ui/index.d.ts +7 -1
  87. package/dist/ui/index.js +8 -2
  88. package/dist/ui/tokens/index.js +3 -1
  89. package/locales/en-GB.json +87 -1
  90. package/locales/en-US.json +87 -1
  91. package/locales/fr.json +87 -1
  92. package/locales/ja.json +87 -1
  93. package/locales/ko.json +87 -1
  94. package/locales/zh-Hans.json +87 -1
  95. package/locales/zh-Hant.json +87 -1
  96. package/package.json +1 -1
  97. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  98. 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 { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
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
- ["brush"]: "brush",
58
- ["eraser"]: "eraser"
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
- /** Drawing colour to start with */
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, colour,
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 = "brush";
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
- currentColour = DEFAULT_COLOUR;
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 !== "move") {
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 erase it from the layer as one undoable
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.eraseMasked(area);
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). Needs a mask with
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.eraseMasked(area);
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, as one undoable action.
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
- eraseMasked(area) {
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(this.layerStack.active.id, area, (ctx) => {
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("move");
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 drawing colour as a hex string, e.g. `#000000` */
893
+ /** Current foreground colour as a hex string, e.g. `#000000` (the same as `colours.foreground`) */
830
894
  get colour() {
831
- return this.currentColour;
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 drawing colour. Must be a 6-digit hex colour, e.g. `#ff0000`.
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 (!COLOUR_PATTERN.test(colour)) throw new Error(`Invalid colour ${colour}`);
838
- this.currentColour = colour.toLowerCase();
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 };