@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.
Files changed (92) 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 +141 -41
  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 +121 -171
  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/PcCanvas/index.d.ts +1 -1
  51. package/dist/ui/elements/PcCanvas/index.js +7 -5
  52. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  53. package/dist/ui/elements/PcColourPair/index.js +125 -0
  54. package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
  55. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  56. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  57. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  58. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  59. package/dist/ui/elements/PcMenu/index.d.ts +93 -0
  60. package/dist/ui/elements/PcMenu/index.js +204 -0
  61. package/dist/ui/elements/PcPane/index.d.ts +118 -0
  62. package/dist/ui/elements/PcPane/index.js +357 -0
  63. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  64. package/dist/ui/elements/PcPanel/index.js +76 -8
  65. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
  66. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +18 -14
  67. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  68. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  69. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  70. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  71. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  72. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  73. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  74. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  75. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  76. package/dist/ui/elements/PcToolSettings/index.js +2 -1
  77. package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
  78. package/dist/ui/elements/PcToolbar/index.js +77 -56
  79. package/dist/ui/elements/PcToolbar/style.js +1 -1
  80. package/dist/ui/index.d.ts +7 -1
  81. package/dist/ui/index.js +8 -2
  82. package/dist/ui/tokens/index.js +3 -1
  83. package/locales/en-GB.json +87 -1
  84. package/locales/en-US.json +87 -1
  85. package/locales/fr.json +87 -1
  86. package/locales/ja.json +87 -1
  87. package/locales/ko.json +87 -1
  88. package/locales/zh-Hans.json +87 -1
  89. package/locales/zh-Hant.json +87 -1
  90. package/package.json +1 -1
  91. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  92. 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
- ["brush"]: "brush",
59
- ["eraser"]: "eraser"
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
- /** Drawing colour to start with */
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, colour,
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 = "brush";
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
- currentColour = DEFAULT_COLOUR;
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 !== "move") {
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 erase it from the layer as one undoable
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.eraseMasked(area);
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). 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
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.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
+ };
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, 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.
746
782
  */
747
- 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
+ }
748
789
  const mask = this.currentSelection.mask;
749
790
  const maskPiece = copyArea(this.createCanvas, mask, area);
750
- this.layerStack.changeImage(this.layerStack.active.id, area, (ctx) => {
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("move");
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 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`) */
831
894
  get colour() {
832
- 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;
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
- * Set the drawing colour. Must be a 6-digit hex colour, e.g. `#ff0000`.
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
- setColour(colour) {
838
- if (!COLOUR_PATTERN.test(colour)) throw new Error(`Invalid colour ${colour}`);
839
- this.currentColour = colour.toLowerCase();
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 };