@lvl99dotcom/paintcanvas 0.3.1 → 0.5.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 (212) hide show
  1. package/README.md +30 -15
  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/brushes/presets/round/index.d.ts +1 -1
  5. package/dist/lib/brushes/presets/round/index.js +12 -7
  6. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  7. package/dist/lib/brushes/presets/square/index.js +7 -3
  8. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  9. package/dist/lib/brushes/shapeImage/index.js +15 -9
  10. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  11. package/dist/lib/brushes/shapes/index.js +86 -0
  12. package/dist/lib/classes/BrushSettings/index.d.ts +53 -2
  13. package/dist/lib/classes/BrushSettings/index.js +85 -14
  14. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +47 -7
  15. package/dist/lib/classes/KeyboardShortcuts/index.js +145 -20
  16. package/dist/lib/classes/PaintCanvas/index.d.ts +123 -27
  17. package/dist/lib/classes/PaintCanvas/index.js +208 -41
  18. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  19. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  20. package/dist/lib/classes/Palette/index.d.ts +58 -0
  21. package/dist/lib/classes/Palette/index.js +92 -0
  22. package/dist/lib/classes/SettingsAutosave/index.d.ts +53 -0
  23. package/dist/lib/classes/SettingsAutosave/index.js +98 -0
  24. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  25. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  26. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  27. package/dist/lib/classes/UiSettings/index.js +82 -0
  28. package/dist/lib/palette/index.d.ts +49 -0
  29. package/dist/lib/palette/index.js +82 -0
  30. package/dist/lib/palette/schema.d.ts +45 -0
  31. package/dist/lib/palette/schema.js +41 -0
  32. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  33. package/dist/lib/rendering/cursors/index.js +3 -1
  34. package/dist/lib/rendering/sample/index.js +3 -2
  35. package/dist/lib/settings/index.d.ts +63 -0
  36. package/dist/lib/settings/index.js +93 -0
  37. package/dist/lib/settings/schema.d.ts +168 -0
  38. package/dist/lib/settings/schema.js +116 -0
  39. package/dist/lib/storage/index.d.ts +36 -5
  40. package/dist/lib/storage/index.js +87 -29
  41. package/dist/lib/utils/colour/index.d.ts +72 -0
  42. package/dist/lib/utils/colour/index.js +154 -0
  43. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  44. package/dist/lib/utils/shortcuts/index.js +391 -145
  45. package/dist/lib/validation/validators.generated.d.ts +5 -0
  46. package/dist/lib/validation/validators.generated.js +1040 -1
  47. package/dist/locales/en-GB.js +96 -3
  48. package/dist/locales/en-US.js +96 -3
  49. package/dist/locales/fr.js +96 -3
  50. package/dist/locales/ja.js +96 -3
  51. package/dist/locales/ko.js +96 -3
  52. package/dist/locales/zh-Hans.js +96 -3
  53. package/dist/locales/zh-Hant.js +96 -3
  54. package/dist/package.js +1 -1
  55. package/dist/testing/recordingCanvas.js +2 -1
  56. package/dist/ui/base/index.d.ts +2 -2
  57. package/dist/ui/dismiss/index.d.ts +21 -0
  58. package/dist/ui/dismiss/index.js +27 -0
  59. package/dist/ui/drag/index.d.ts +51 -0
  60. package/dist/ui/drag/index.js +81 -0
  61. package/dist/ui/elements/PaintCanvas/index.d.ts +57 -50
  62. package/dist/ui/elements/PaintCanvas/index.js +141 -174
  63. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  64. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  65. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  66. package/dist/ui/elements/PcBrushConfig/index.d.ts +9 -2
  67. package/dist/ui/elements/PcBrushConfig/index.js +47 -7
  68. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  69. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  70. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  71. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  72. package/dist/ui/elements/PcBrushTest/index.js +73 -5
  73. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  74. package/dist/ui/elements/PcCanvas/index.d.ts +22 -2
  75. package/dist/ui/elements/PcCanvas/index.js +60 -15
  76. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  77. package/dist/ui/elements/PcClipboardTools/index.d.ts +48 -0
  78. package/dist/ui/elements/PcClipboardTools/index.js +118 -0
  79. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  80. package/dist/ui/elements/PcColourPair/index.js +125 -0
  81. package/dist/ui/elements/PcContextMenu/index.d.ts +71 -34
  82. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  83. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  84. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  85. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  86. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  87. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  88. package/dist/ui/elements/PcMenu/index.d.ts +138 -0
  89. package/dist/ui/elements/PcMenu/index.js +296 -0
  90. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  91. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  92. package/dist/ui/elements/PcPane/index.d.ts +120 -0
  93. package/dist/ui/elements/PcPane/index.js +394 -0
  94. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  95. package/dist/ui/elements/PcPanel/index.js +76 -8
  96. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +22 -21
  97. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +24 -57
  98. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  99. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  100. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  101. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  102. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  103. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  104. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  105. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  106. package/dist/ui/elements/PcPopover/index.js +72 -4
  107. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  108. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  109. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  110. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  111. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  112. package/dist/ui/elements/PcToolSettings/index.js +384 -319
  113. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  114. package/dist/ui/elements/PcToolbar/index.d.ts +62 -12
  115. package/dist/ui/elements/PcToolbar/index.js +119 -57
  116. package/dist/ui/elements/PcToolbar/style.js +1 -1
  117. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  118. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  119. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  120. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  121. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  122. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  123. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  124. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  125. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  126. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  127. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  128. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  129. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  130. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  131. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  132. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  133. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  134. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  135. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  136. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  137. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  138. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  139. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  140. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  141. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  142. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  143. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  144. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  145. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  146. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  147. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  148. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  149. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  150. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  151. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  152. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  153. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  154. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  155. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  156. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  157. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  158. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  159. package/dist/ui/icons/index.d.ts +1 -1
  160. package/dist/ui/icons/index.js +84 -72
  161. package/dist/ui/index.d.ts +13 -2
  162. package/dist/ui/index.js +16 -4
  163. package/dist/ui/layout/index.d.ts +9 -1
  164. package/dist/ui/layout/index.js +11 -2
  165. package/dist/ui/tokens/index.d.ts +5 -4
  166. package/dist/ui/tokens/index.js +15 -8
  167. package/locales/en-GB.json +96 -3
  168. package/locales/en-US.json +96 -3
  169. package/locales/fr.json +96 -3
  170. package/locales/ja.json +96 -3
  171. package/locales/ko.json +96 -3
  172. package/locales/zh-Hans.json +96 -3
  173. package/locales/zh-Hant.json +96 -3
  174. package/package.json +11 -4
  175. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  176. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
  177. package/dist/ui/icons/images/brush.js +0 -3
  178. package/dist/ui/icons/images/circle-mask.js +0 -3
  179. package/dist/ui/icons/images/clipboard.js +0 -3
  180. package/dist/ui/icons/images/cursor.js +0 -3
  181. package/dist/ui/icons/images/eraser.js +0 -3
  182. package/dist/ui/icons/images/eyedropper.js +0 -3
  183. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  184. package/dist/ui/icons/images/flip-vertical.js +0 -3
  185. package/dist/ui/icons/images/focus-pen.js +0 -3
  186. package/dist/ui/icons/images/freehand-mask.js +0 -3
  187. package/dist/ui/icons/images/hidden.js +0 -3
  188. package/dist/ui/icons/images/layers.js +0 -3
  189. package/dist/ui/icons/images/linked.js +0 -3
  190. package/dist/ui/icons/images/locked.js +0 -3
  191. package/dist/ui/icons/images/moon.js +0 -3
  192. package/dist/ui/icons/images/pan.js +0 -3
  193. package/dist/ui/icons/images/polygon-mask.js +0 -3
  194. package/dist/ui/icons/images/pressure.js +0 -3
  195. package/dist/ui/icons/images/randomise.js +0 -3
  196. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  197. package/dist/ui/icons/images/redo.js +0 -3
  198. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  199. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  200. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  201. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  202. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  203. package/dist/ui/icons/images/star.js +0 -3
  204. package/dist/ui/icons/images/sun.js +0 -3
  205. package/dist/ui/icons/images/tilt.js +0 -3
  206. package/dist/ui/icons/images/undo.js +0 -3
  207. package/dist/ui/icons/images/unlinked.js +0 -3
  208. package/dist/ui/icons/images/unlocked.js +0 -3
  209. package/dist/ui/icons/images/visible.js +0 -3
  210. package/dist/ui/icons/images/zoom-in.js +0 -3
  211. package/dist/ui/icons/images/zoom-out.js +0 -3
  212. package/dist/ui/icons/images/zoom.js +0 -3
@@ -5,6 +5,10 @@ var MIN_BRUSH_SIZE = 1;
5
5
  var MAX_BRUSH_SIZE = 256;
6
6
  /** Fraction of the current size added or removed by `grow` and `shrink` ("]" and "[" keys) */
7
7
  var BRUSH_SIZE_STEP = .1;
8
+ /** Opacity step for the opacity keyboard shortcuts, in percent */
9
+ var BRUSH_OPACITY_STEP = 10;
10
+ /** Lowest opacity the opacity keyboard shortcuts go to, in percent */
11
+ var MIN_BRUSH_OPACITY = 1;
8
12
  /**
9
13
  * Clamp a value between min and max.
10
14
  *
@@ -13,6 +17,35 @@ var BRUSH_SIZE_STEP = .1;
13
17
  */
14
18
  var clamp = (value, min, max) => Math.max(min, Math.min(max, value));
15
19
  /**
20
+ * The next opacity step up or down, in percent: to the next multiple of `BRUSH_OPACITY_STEP`, from
21
+ * `MIN_BRUSH_OPACITY` to 100.
22
+ *
23
+ * @example
24
+ * stepOpacity(45, 1); // 50
25
+ * stepOpacity(10, -1); // 1
26
+ * stepOpacity(1, 1); // 10
27
+ */
28
+ var stepOpacity = (percent, direction) => {
29
+ const steps = percent / 10;
30
+ return clamp((direction > 0 ? Math.floor(steps + 1e-9) + 1 : Math.ceil(steps - 1e-9) - 1) * 10, 1, 100);
31
+ };
32
+ /**
33
+ * A brush size with the given width and proportions (width ÷ height), in whole pixels. The width is kept within the
34
+ * range where the smaller side is at least `MIN_BRUSH_SIZE` and the larger side at most `MAX_BRUSH_SIZE`, so the
35
+ * proportions are kept at either end.
36
+ *
37
+ * @example
38
+ * proportionalSize(0, 3); // { width: 3, height: 1 }
39
+ * proportionalSize(1000, 0.5); // { width: 128, height: 256 }
40
+ */
41
+ var proportionalSize = (width, aspect) => {
42
+ const fitted = Math.round(clamp(width, Math.max(1, 1 * aspect), Math.min(256, 256 * aspect)));
43
+ return {
44
+ width: fitted,
45
+ height: Math.round(clamp(fitted / aspect, 1, 256))
46
+ };
47
+ };
48
+ /**
16
49
  * The settings of a brush or eraser: the selected preset, an editable copy of its settings (changed from the tool
17
50
  * settings bar or the configuration window, without changing the preset) and the size. The brush and eraser each have
18
51
  * their own instance.
@@ -35,6 +68,11 @@ var BrushSettings = class extends EventTarget {
35
68
  presetId;
36
69
  /** The current settings: a copy of the selected preset, with any changes */
37
70
  current;
71
+ /**
72
+ * The brush's proportions (width ÷ height), kept apart from the rounded size so resizing to either end and back
73
+ * doesn't change them. Set by the preset and by setting the size while proportions aren't constrained.
74
+ */
75
+ aspect = 1;
38
76
  constructor(presets) {
39
77
  super();
40
78
  const first = Object.values(presets)[0];
@@ -101,39 +139,64 @@ var BrushSettings = class extends EventTarget {
101
139
  * Set the height, clamped to 1–256 pixels. If the settings constrain proportions, width follows height.
102
140
  */
103
141
  setHeight(height) {
104
- if (!this.current.constrainProportions) {
105
- this.setSize(this.width, height);
106
- return;
107
- }
108
- const ratio = this.width / this.height;
109
- this.setSize(clamp(height, 1, 256) * ratio);
142
+ if (this.current.constrainProportions) this.setProportionalSize(height * this.aspect);
143
+ else this.setSize(this.width, height);
110
144
  }
111
145
  /**
112
- * Set the size, clamped to 1–256 pixels. If the settings constrain proportions, height follows width.
146
+ * Set the size, clamped to 1–256 pixels. If the settings constrain proportions, height follows width, and the width
147
+ * stops where either side would leave that range.
113
148
  */
114
149
  setSize(width, height = width) {
115
- const ratio = this.height / this.width;
150
+ if (this.current.constrainProportions) {
151
+ this.setProportionalSize(width);
152
+ return;
153
+ }
116
154
  this.width = Math.round(clamp(width, 1, 256));
117
- this.height = Math.round(clamp(this.preset.constrainProportions ? this.width * ratio : height, 1, 256));
155
+ this.height = Math.round(clamp(height, 1, 256));
156
+ this.aspect = this.width / this.height;
118
157
  this.notify();
119
158
  }
120
159
  /**
121
- * Change the size by a number of pixels (positive = bigger). Used when dragging up/right (bigger) or down/left (smaller).
160
+ * Change the size by a number of pixels (positive = bigger), keeping the proportions: the larger side changes by
161
+ * `delta`. Stops when the smaller side reaches 1 pixel or the larger 256. Used when dragging up/right (bigger) or
162
+ * down/left (smaller).
122
163
  */
123
164
  resize(delta) {
124
- this.setSize(this.width + delta, this.height + delta);
165
+ const larger = Math.max(this.width, this.height) + delta;
166
+ this.setProportionalSize(this.aspect >= 1 ? larger : larger * this.aspect);
167
+ }
168
+ /** The larger side (what `resize` changes), which `grow` and `shrink` step from */
169
+ get largerSide() {
170
+ return Math.max(this.width, this.height);
125
171
  }
126
172
  /**
127
173
  * Increase the size by one step (at least 1 pixel).
128
174
  */
129
175
  grow() {
130
- this.resize(Math.max(1, Math.round(this.width * BRUSH_SIZE_STEP)));
176
+ this.resize(Math.max(1, Math.round(this.largerSide * BRUSH_SIZE_STEP)));
131
177
  }
132
178
  /**
133
179
  * Decrease the size by one step (at least 1 pixel).
134
180
  */
135
181
  shrink() {
136
- this.resize(-Math.max(1, Math.round(this.width * BRUSH_SIZE_STEP)));
182
+ this.resize(-Math.max(1, Math.round(this.largerSide * BRUSH_SIZE_STEP)));
183
+ }
184
+ /**
185
+ * Increase the opacity by one step (see `stepOpacity`).
186
+ */
187
+ moreOpaque() {
188
+ this.stepOpacity(1);
189
+ }
190
+ /**
191
+ * Decrease the opacity by one step (see `stepOpacity`).
192
+ */
193
+ lessOpaque() {
194
+ this.stepOpacity(-1);
195
+ }
196
+ /** Change the opacity by one step up or down */
197
+ stepOpacity(direction) {
198
+ const percent = Math.round((this.current.alpha ?? 1) * 100);
199
+ this.update({ alpha: stepOpacity(percent, direction) / 100 });
137
200
  }
138
201
  /**
139
202
  * Use a preset's (or configuration's) default size.
@@ -141,6 +204,14 @@ var BrushSettings = class extends EventTarget {
141
204
  applyPresetSize(preset) {
142
205
  this.width = preset.width ?? 1;
143
206
  this.height = preset.height ?? this.width;
207
+ this.aspect = this.width / this.height;
208
+ }
209
+ /**
210
+ * Set the width, with the height following the stored proportions (see `proportionalSize`).
211
+ */
212
+ setProportionalSize(width) {
213
+ Object.assign(this, proportionalSize(width, this.aspect));
214
+ this.notify();
144
215
  }
145
216
  /**
146
217
  * Dispatch a `change` event.
@@ -149,4 +220,4 @@ var BrushSettings = class extends EventTarget {
149
220
  this.dispatchEvent(new Event("change"));
150
221
  }
151
222
  };
152
- export { BRUSH_SIZE_STEP, MAX_BRUSH_SIZE, MIN_BRUSH_SIZE, clamp, BrushSettings as default };
223
+ export { BRUSH_OPACITY_STEP, BRUSH_SIZE_STEP, MAX_BRUSH_SIZE, MIN_BRUSH_OPACITY, MIN_BRUSH_SIZE, clamp, BrushSettings as default, proportionalSize, stepOpacity };
@@ -1,14 +1,32 @@
1
1
  import { type default as PaintCanvas } from "../PaintCanvas";
2
2
  import { type ShortcutAction } from "../../utils/shortcuts";
3
+ /**
4
+ * How long (milliseconds) the paste shortcut waits for the browser's `paste` event (which carries other apps' images)
5
+ * before pasting the image data copied here
6
+ */
7
+ export declare const PASTE_EVENT_WAIT_MS = 50;
8
+ /**
9
+ * The first image in a paste's clipboard data (e.g. copied in another app), if any.
10
+ *
11
+ * @example
12
+ * pastedImage(event.clipboardData); // File of type "image/png", or undefined
13
+ */
14
+ export declare const pastedImage: (data: DataTransfer | null) => File | undefined;
3
15
  /** Event dispatched on the window for shortcut actions handled elsewhere (e.g. saving) */
4
16
  export declare const SHORTCUT_EVENT = "pc-shortcut";
5
17
  /**
6
- * Listens for key presses and runs the matching shortcut on the app. Shortcuts are ignored while typing in text fields.
18
+ * Listens for key presses and runs the matching shortcut on the app, using the user's key combinations
19
+ * (`app.shortcuts`). Shortcuts are ignored while typing in text fields and inside open `<dialog>` elements. Key presses are matched on keyboards with
20
+ * different layouts (see `comboFromEvent`).
7
21
  *
8
- * Tool selection, brush/eraser size, interface visibility, select all/deselect, undo/redo, cut/copy/paste, deleting the
9
- * masked image data (Delete or Backspace) and finishing or cancelling a
10
- * lasso shape are handled here; other actions (and Escape/Enter for later features) dispatch an `pc-shortcut` event with the action as `detail`.
11
- * Holding Space pans temporarily; releasing it returns to the selected tool.
22
+ * Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
23
+ * cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
24
+ * and finishing or cancelling a lasso shape are handled here. Pasting (the shortcut, or the browser's own paste, e.g. from
25
+ * its Edit menu) pastes the image data copied here, unless the window has lost focus since it was copied and the
26
+ * clipboard has an image (e.g. copied in another app): that image is pasted instead (see `PaintCanvas.pasteImage`).
27
+ * Other actions other actions (and Escape/Enter for later features)
28
+ * dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
29
+ * temporarily; releasing it returns to the selected tool.
12
30
  *
13
31
  * @example
14
32
  * const shortcuts = new KeyboardShortcuts(app);
@@ -17,10 +35,19 @@ export declare const SHORTCUT_EVENT = "pc-shortcut";
17
35
  export default class KeyboardShortcuts {
18
36
  private readonly app;
19
37
  private target;
38
+ /** The hold-to-pan key while it is held */
39
+ private heldKey?;
20
40
  private readonly onKeyDown;
21
41
  private readonly onKeyUp;
42
+ /** Turns pasted image files into images (replaceable for tests) */
43
+ private readonly decodeImage;
44
+ /** `app.copyCount` when the window last lost focus: copies since then were made here */
45
+ private copiesBeforeBlur;
46
+ /** Pastes the image data copied here if the browser doesn't fire `paste` soon after the shortcut */
47
+ private pasteTimer?;
22
48
  private readonly onBlur;
23
- constructor(app: PaintCanvas);
49
+ private readonly onPaste;
50
+ constructor(app: PaintCanvas, decodeImage?: (blob: Blob) => Promise<ImageBitmap>);
24
51
  /**
25
52
  * Start listening for key presses on a target (usually `window`).
26
53
  */
@@ -34,13 +61,26 @@ export default class KeyboardShortcuts {
34
61
  */
35
62
  handle(event: KeyboardEvent): ShortcutAction | undefined;
36
63
  /**
37
- * Return to the selected tool when the hold-to-pan key is released.
64
+ * Paste from the browser's `paste` event: the image data copied here if it was copied since the window last lost
65
+ * focus (or the clipboard has no image), otherwise the clipboard's image. Ignored in text fields and open dialogs.
66
+ */
67
+ handlePaste(event: ClipboardEvent): Promise<void>;
68
+ /** Stop waiting for the browser's `paste` event */
69
+ private cancelPasteTimer;
70
+ /**
71
+ * Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
38
72
  */
39
73
  handleKeyUp(event: KeyboardEvent): void;
74
+ /** Stop holding the hold-to-pan key */
75
+ private release;
40
76
  /**
41
77
  * Run a shortcut action.
42
78
  */
43
79
  run(action: ShortcutAction): void;
80
+ /** The brush or eraser settings while that tool is selected */
81
+ private brushSettings;
82
+ /** The middle of the viewport, which zoom shortcuts zoom around */
83
+ private viewportCentre;
44
84
  /**
45
85
  * Dispatch an action as an `pc-shortcut` event for features handled elsewhere.
46
86
  */
@@ -1,15 +1,39 @@
1
- import { Tool } from "../PaintCanvas/index.js";
2
- import { isTypingTarget, matchShortcut } from "../../utils/shortcuts/index.js";
1
+ import { Tool } from "../PaintCanvas/tool.js";
2
+ import "../PaintCanvas/index.js";
3
+ import { comboFromEvent, isTypingTarget, sameCombo } from "../../utils/shortcuts/index.js";
3
4
  // src/lib/classes/KeyboardShortcuts/index.ts
5
+ /**
6
+ * Whether a key press happened inside an open `<dialog>` (e.g. a settings window), where shortcuts must not apply and
7
+ * Escape is left to close it.
8
+ */
9
+ var inOpenDialog = (path) => path.some((target) => target instanceof HTMLDialogElement && target.open);
10
+ /**
11
+ * How long (milliseconds) the paste shortcut waits for the browser's `paste` event (which carries other apps' images)
12
+ * before pasting the image data copied here
13
+ */
14
+ var PASTE_EVENT_WAIT_MS = 50;
15
+ /**
16
+ * The first image in a paste's clipboard data (e.g. copied in another app), if any.
17
+ *
18
+ * @example
19
+ * pastedImage(event.clipboardData); // File of type "image/png", or undefined
20
+ */
21
+ var pastedImage = (data) => [...data?.files ?? []].find((file) => file.type.startsWith("image/"));
4
22
  /** Event dispatched on the window for shortcut actions handled elsewhere (e.g. saving) */
5
23
  var SHORTCUT_EVENT = "pc-shortcut";
6
24
  /**
7
- * Listens for key presses and runs the matching shortcut on the app. Shortcuts are ignored while typing in text fields.
25
+ * Listens for key presses and runs the matching shortcut on the app, using the user's key combinations
26
+ * (`app.shortcuts`). Shortcuts are ignored while typing in text fields and inside open `<dialog>` elements. Key presses are matched on keyboards with
27
+ * different layouts (see `comboFromEvent`).
8
28
  *
9
- * Tool selection, brush/eraser size, interface visibility, select all/deselect, undo/redo, cut/copy/paste, deleting the
10
- * masked image data (Delete or Backspace) and finishing or cancelling a
11
- * lasso shape are handled here; other actions (and Escape/Enter for later features) dispatch an `pc-shortcut` event with the action as `detail`.
12
- * Holding Space pans temporarily; releasing it returns to the selected tool.
29
+ * Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
30
+ * cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
31
+ * and finishing or cancelling a lasso shape are handled here. Pasting (the shortcut, or the browser's own paste, e.g. from
32
+ * its Edit menu) pastes the image data copied here, unless the window has lost focus since it was copied and the
33
+ * clipboard has an image (e.g. copied in another app): that image is pasted instead (see `PaintCanvas.pasteImage`).
34
+ * Other actions other actions (and Escape/Enter for later features)
35
+ * dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
36
+ * temporarily; releasing it returns to the selected tool.
13
37
  *
14
38
  * @example
15
39
  * const shortcuts = new KeyboardShortcuts(app);
@@ -18,11 +42,24 @@ var SHORTCUT_EVENT = "pc-shortcut";
18
42
  var KeyboardShortcuts = class {
19
43
  app;
20
44
  target = null;
45
+ /** The hold-to-pan key while it is held */
46
+ heldKey;
21
47
  onKeyDown = (event) => this.handle(event);
22
48
  onKeyUp = (event) => this.handleKeyUp(event);
23
- onBlur = () => this.app.releaseHeldTool();
24
- constructor(app) {
49
+ /** Turns pasted image files into images (replaceable for tests) */
50
+ decodeImage;
51
+ /** `app.copyCount` when the window last lost focus: copies since then were made here */
52
+ copiesBeforeBlur = 0;
53
+ /** Pastes the image data copied here if the browser doesn't fire `paste` soon after the shortcut */
54
+ pasteTimer;
55
+ onBlur = () => {
56
+ this.release();
57
+ this.copiesBeforeBlur = this.app.copyCount;
58
+ };
59
+ onPaste = (event) => void this.handlePaste(event);
60
+ constructor(app, decodeImage = (blob) => createImageBitmap(blob)) {
25
61
  this.app = app;
62
+ this.decodeImage = decodeImage;
26
63
  }
27
64
  /**
28
65
  * Start listening for key presses on a target (usually `window`).
@@ -33,6 +70,7 @@ var KeyboardShortcuts = class {
33
70
  target.addEventListener("keydown", this.onKeyDown);
34
71
  target.addEventListener("keyup", this.onKeyUp);
35
72
  target.addEventListener("blur", this.onBlur);
73
+ target.addEventListener("paste", this.onPaste);
36
74
  }
37
75
  /**
38
76
  * Stop listening for key presses.
@@ -41,29 +79,74 @@ var KeyboardShortcuts = class {
41
79
  this.target?.removeEventListener("keydown", this.onKeyDown);
42
80
  this.target?.removeEventListener("keyup", this.onKeyUp);
43
81
  this.target?.removeEventListener("blur", this.onBlur);
82
+ this.target?.removeEventListener("paste", this.onPaste);
44
83
  this.target = null;
84
+ this.cancelPasteTimer();
45
85
  }
46
86
  /**
47
87
  * Run the shortcut for a key press, if any. Returns the action run.
48
88
  */
49
89
  handle(event) {
50
- if (isTypingTarget(event.composedPath()[0] ?? event.target)) return void 0;
51
- if (event.key === " " && !event.metaKey && !event.ctrlKey && !event.altKey) {
90
+ const path = event.composedPath();
91
+ if (isTypingTarget(path[0] ?? event.target) || inOpenDialog(path)) return void 0;
92
+ const combo = comboFromEvent(event);
93
+ if (!combo) return void 0;
94
+ const { holdToPan, shortcuts } = this.app.shortcuts;
95
+ if (holdToPan && sameCombo(combo, holdToPan)) {
52
96
  event.preventDefault();
97
+ this.heldKey = combo.key;
53
98
  this.app.holdTool(Tool.Pan);
54
99
  return;
55
100
  }
56
- const action = matchShortcut(event);
101
+ const action = shortcuts.find((shortcut) => sameCombo(shortcut, combo))?.action;
57
102
  if (!action) return void 0;
103
+ if (action.type === "paste") {
104
+ this.cancelPasteTimer();
105
+ this.pasteTimer = setTimeout(() => {
106
+ this.pasteTimer = void 0;
107
+ this.app.paste();
108
+ }, 50);
109
+ return action;
110
+ }
58
111
  event.preventDefault();
59
112
  this.run(action);
60
113
  return action;
61
114
  }
62
115
  /**
63
- * Return to the selected tool when the hold-to-pan key is released.
116
+ * Paste from the browser's `paste` event: the image data copied here if it was copied since the window last lost
117
+ * focus (or the clipboard has no image), otherwise the clipboard's image. Ignored in text fields and open dialogs.
118
+ */
119
+ async handlePaste(event) {
120
+ const path = event.composedPath();
121
+ if (isTypingTarget(path[0] ?? event.target) || inOpenDialog(path)) return;
122
+ this.cancelPasteTimer();
123
+ event.preventDefault();
124
+ const app = this.app;
125
+ const image = pastedImage(event.clipboardData);
126
+ const copiedHere = app.copyCount > this.copiesBeforeBlur;
127
+ if (!image || copiedHere && app.hasClipboard) {
128
+ app.paste();
129
+ return;
130
+ }
131
+ try {
132
+ app.pasteImage(await this.decodeImage(image));
133
+ } catch {}
134
+ }
135
+ /** Stop waiting for the browser's `paste` event */
136
+ cancelPasteTimer() {
137
+ clearTimeout(this.pasteTimer);
138
+ this.pasteTimer = void 0;
139
+ }
140
+ /**
141
+ * Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
64
142
  */
65
143
  handleKeyUp(event) {
66
- if (event.key === " ") this.app.releaseHeldTool();
144
+ if (this.heldKey !== void 0 && comboFromEvent(event)?.key === this.heldKey) this.release();
145
+ }
146
+ /** Stop holding the hold-to-pan key */
147
+ release() {
148
+ this.heldKey = void 0;
149
+ this.app.releaseHeldTool();
67
150
  }
68
151
  /**
69
152
  * Run a shortcut action.
@@ -75,21 +158,41 @@ var KeyboardShortcuts = class {
75
158
  app.selectTool(action.tool);
76
159
  break;
77
160
  case "brush-smaller":
78
- case "brush-bigger": {
79
- const settings = app.tool === Tool.Eraser ? app.eraser : app.tool === Tool.Brush ? app.brush : void 0;
80
- if (action.type === "brush-bigger") settings?.grow();
81
- else settings?.shrink();
161
+ this.brushSettings()?.shrink();
162
+ break;
163
+ case "brush-bigger":
164
+ this.brushSettings()?.grow();
165
+ break;
166
+ case "brush-less-opaque":
167
+ this.brushSettings()?.lessOpaque();
168
+ break;
169
+ case "brush-more-opaque":
170
+ this.brushSettings()?.moreOpaque();
82
171
  break;
83
- }
84
172
  case "toggle-interface":
85
173
  app.toggleInterface();
86
174
  break;
87
175
  case "select-all":
88
176
  app.selection.selectAll();
89
177
  break;
178
+ case "invert-selection":
179
+ app.selection.invert();
180
+ break;
90
181
  case "deselect":
91
182
  app.selection.clear();
92
183
  break;
184
+ case "zoom-in":
185
+ app.viewport.zoomIn(this.viewportCentre());
186
+ break;
187
+ case "zoom-out":
188
+ app.viewport.zoomOut(this.viewportCentre());
189
+ break;
190
+ case "reset-zoom":
191
+ app.viewport.zoomAt(this.viewportCentre(), 1);
192
+ break;
193
+ case "layer-opacity":
194
+ app.layers.update(app.layers.active.id, { opacity: action.opacity });
195
+ break;
93
196
  case "undo":
94
197
  app.undo();
95
198
  break;
@@ -102,6 +205,15 @@ var KeyboardShortcuts = class {
102
205
  case "delete":
103
206
  app.deleteMasked();
104
207
  break;
208
+ case "fill-foreground":
209
+ app.fill("foreground");
210
+ break;
211
+ case "fill-background":
212
+ app.fill("background");
213
+ break;
214
+ case "swap-colours":
215
+ app.swapColours();
216
+ break;
105
217
  case "copy":
106
218
  app.copy();
107
219
  break;
@@ -127,6 +239,19 @@ var KeyboardShortcuts = class {
127
239
  default: this.dispatch(action);
128
240
  }
129
241
  }
242
+ /** The brush or eraser settings while that tool is selected */
243
+ brushSettings() {
244
+ const { app } = this;
245
+ return app.tool === Tool.Eraser ? app.eraser : app.tool === Tool.Brush ? app.brush : void 0;
246
+ }
247
+ /** The middle of the viewport, which zoom shortcuts zoom around */
248
+ viewportCentre() {
249
+ const { width, height } = this.app.viewport;
250
+ return {
251
+ x: width / 2,
252
+ y: height / 2
253
+ };
254
+ }
130
255
  /**
131
256
  * Dispatch an action as an `pc-shortcut` event for features handled elsewhere.
132
257
  */
@@ -134,4 +259,4 @@ var KeyboardShortcuts = class {
134
259
  (this.target ?? this.app).dispatchEvent(new CustomEvent(SHORTCUT_EVENT, { detail: action }));
135
260
  }
136
261
  };
137
- export { SHORTCUT_EVENT, KeyboardShortcuts as default };
262
+ export { PASTE_EVENT_WAIT_MS, SHORTCUT_EVENT, KeyboardShortcuts as default, pastedImage };