@lvl99dotcom/paintcanvas 0.4.0 → 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 (174) hide show
  1. package/README.md +30 -15
  2. package/dist/lib/brushes/presets/round/index.d.ts +1 -1
  3. package/dist/lib/brushes/presets/round/index.js +12 -7
  4. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  5. package/dist/lib/brushes/presets/square/index.js +7 -3
  6. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  7. package/dist/lib/brushes/shapeImage/index.js +15 -9
  8. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  9. package/dist/lib/brushes/shapes/index.js +86 -0
  10. package/dist/lib/classes/BrushSettings/index.d.ts +29 -2
  11. package/dist/lib/classes/BrushSettings/index.js +51 -14
  12. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +31 -2
  13. package/dist/lib/classes/KeyboardShortcuts/index.js +66 -4
  14. package/dist/lib/classes/PaintCanvas/index.d.ts +46 -0
  15. package/dist/lib/classes/PaintCanvas/index.js +67 -0
  16. package/dist/lib/classes/SettingsAutosave/index.d.ts +3 -2
  17. package/dist/lib/classes/SettingsAutosave/index.js +5 -3
  18. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  19. package/dist/lib/classes/UiSettings/index.js +82 -0
  20. package/dist/lib/settings/index.d.ts +1 -1
  21. package/dist/lib/settings/index.js +4 -2
  22. package/dist/lib/settings/schema.d.ts +18 -0
  23. package/dist/lib/settings/schema.js +14 -0
  24. package/dist/lib/validation/validators.generated.js +169 -1
  25. package/dist/locales/en-GB.js +9 -2
  26. package/dist/locales/en-US.js +9 -2
  27. package/dist/locales/fr.js +9 -2
  28. package/dist/locales/ja.js +9 -2
  29. package/dist/locales/ko.js +9 -2
  30. package/dist/locales/zh-Hans.js +9 -2
  31. package/dist/locales/zh-Hant.js +9 -2
  32. package/dist/package.js +1 -1
  33. package/dist/testing/recordingCanvas.js +2 -1
  34. package/dist/ui/base/index.d.ts +2 -2
  35. package/dist/ui/dismiss/index.d.ts +21 -0
  36. package/dist/ui/dismiss/index.js +27 -0
  37. package/dist/ui/drag/index.d.ts +51 -0
  38. package/dist/ui/drag/index.js +81 -0
  39. package/dist/ui/elements/PaintCanvas/index.d.ts +17 -12
  40. package/dist/ui/elements/PaintCanvas/index.js +34 -17
  41. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  42. package/dist/ui/elements/PcAppSettings/index.d.ts +1 -1
  43. package/dist/ui/elements/PcAppSettings/index.js +1 -1
  44. package/dist/ui/elements/PcBrushConfig/index.d.ts +9 -2
  45. package/dist/ui/elements/PcBrushConfig/index.js +47 -7
  46. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  47. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  48. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  49. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  50. package/dist/ui/elements/PcBrushTest/index.js +73 -5
  51. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  52. package/dist/ui/elements/PcCanvas/index.d.ts +21 -1
  53. package/dist/ui/elements/PcCanvas/index.js +53 -10
  54. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  55. package/dist/ui/elements/PcClipboardTools/index.d.ts +48 -0
  56. package/dist/ui/elements/PcClipboardTools/index.js +118 -0
  57. package/dist/ui/elements/PcContextMenu/index.d.ts +2 -2
  58. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  59. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  60. package/dist/ui/elements/PcMenu/index.d.ts +48 -3
  61. package/dist/ui/elements/PcMenu/index.js +111 -19
  62. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  63. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  64. package/dist/ui/elements/PcPane/index.d.ts +3 -1
  65. package/dist/ui/elements/PcPane/index.js +44 -7
  66. package/dist/ui/elements/PcPanel/index.d.ts +2 -2
  67. package/dist/ui/elements/PcPanel/index.js +8 -8
  68. package/dist/ui/elements/PcPanelBrushes/index.d.ts +2 -15
  69. package/dist/ui/elements/PcPanelBrushes/index.js +6 -43
  70. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  71. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  72. package/dist/ui/elements/PcPopover/index.js +72 -4
  73. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  74. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  75. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  76. package/dist/ui/elements/PcToolSettings/index.js +382 -318
  77. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  78. package/dist/ui/elements/PcToolbar/index.d.ts +15 -0
  79. package/dist/ui/elements/PcToolbar/index.js +55 -14
  80. package/dist/ui/elements/PcToolbar/style.js +1 -1
  81. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  82. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  83. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  84. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  85. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  86. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  87. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  88. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  89. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  90. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  91. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  92. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  93. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  94. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  95. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  96. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  97. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  98. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  99. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  100. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  101. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  102. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  103. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  104. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  105. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  106. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  107. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  108. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  109. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  110. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  111. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  112. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  113. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  114. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  115. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  116. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  117. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  118. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  119. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  120. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  121. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  122. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  123. package/dist/ui/icons/index.d.ts +1 -1
  124. package/dist/ui/icons/index.js +84 -72
  125. package/dist/ui/index.d.ts +7 -2
  126. package/dist/ui/index.js +10 -4
  127. package/dist/ui/layout/index.d.ts +9 -1
  128. package/dist/ui/layout/index.js +11 -2
  129. package/dist/ui/tokens/index.d.ts +5 -4
  130. package/dist/ui/tokens/index.js +12 -7
  131. package/locales/en-GB.json +9 -2
  132. package/locales/en-US.json +9 -2
  133. package/locales/fr.json +9 -2
  134. package/locales/ja.json +9 -2
  135. package/locales/ko.json +9 -2
  136. package/locales/zh-Hans.json +9 -2
  137. package/locales/zh-Hant.json +9 -2
  138. package/package.json +11 -4
  139. package/dist/ui/icons/images/brush.js +0 -3
  140. package/dist/ui/icons/images/circle-mask.js +0 -3
  141. package/dist/ui/icons/images/clipboard.js +0 -3
  142. package/dist/ui/icons/images/cursor.js +0 -3
  143. package/dist/ui/icons/images/eraser.js +0 -3
  144. package/dist/ui/icons/images/eyedropper.js +0 -3
  145. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  146. package/dist/ui/icons/images/flip-vertical.js +0 -3
  147. package/dist/ui/icons/images/focus-pen.js +0 -3
  148. package/dist/ui/icons/images/freehand-mask.js +0 -3
  149. package/dist/ui/icons/images/hidden.js +0 -3
  150. package/dist/ui/icons/images/layers.js +0 -3
  151. package/dist/ui/icons/images/linked.js +0 -3
  152. package/dist/ui/icons/images/locked.js +0 -3
  153. package/dist/ui/icons/images/moon.js +0 -3
  154. package/dist/ui/icons/images/pan.js +0 -3
  155. package/dist/ui/icons/images/polygon-mask.js +0 -3
  156. package/dist/ui/icons/images/pressure.js +0 -3
  157. package/dist/ui/icons/images/randomise.js +0 -3
  158. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  159. package/dist/ui/icons/images/redo.js +0 -3
  160. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  161. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  162. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  163. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  164. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  165. package/dist/ui/icons/images/star.js +0 -3
  166. package/dist/ui/icons/images/sun.js +0 -3
  167. package/dist/ui/icons/images/tilt.js +0 -3
  168. package/dist/ui/icons/images/undo.js +0 -3
  169. package/dist/ui/icons/images/unlinked.js +0 -3
  170. package/dist/ui/icons/images/unlocked.js +0 -3
  171. package/dist/ui/icons/images/visible.js +0 -3
  172. package/dist/ui/icons/images/zoom-in.js +0 -3
  173. package/dist/ui/icons/images/zoom-out.js +0 -3
  174. package/dist/ui/icons/images/zoom.js +0 -3
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Open-source painting engine and renderer for the browser, with optional UI.
4
4
 
5
+ [Read the docs](https://lvl99.github.io/paintcanvas/), [try PaintCanvas in your browser](https://lvl99.github.io/paintcanvas/app/) or [see the UI elements](https://lvl99.github.io/paintcanvas/app/elements.html).
6
+
5
7
  - **Engine** (`@lvl99dotcom/paintcanvas/lib/*`): layers, brushes and erasers, custom brush presets, selection, transforms, clipboard, undo/redo, viewport, pen/touch/mouse input and rendering. No UI, and nothing is stored unless you ask (e.g. keeping the drawing in the browser).
6
8
  - **Messages**: the engine sends and receives small drawing messages (patches of pixels, strokes, undo and redo), so you can share a drawing over any transport you choose.
7
9
  - **UI** (`@lvl99dotcom/paintcanvas/ui`), optional: Lit web components, from buttons and sliders up to a full `<paint-canvas>` layout with slots for your own parts.
@@ -69,7 +71,7 @@ Importing `@lvl99dotcom/paintcanvas/ui` registers every element. To load less, i
69
71
 
70
72
  ## Documentation
71
73
 
72
- Guides and API pages are in [docs/](docs/), a [Docusaurus](https://docusaurus.io) site (`pnpm docs`). The guides:
74
+ The documentation is published at https://lvl99.github.io/paintcanvas/. Its guides and API pages are in [docs/](docs/), a [Docusaurus](https://docusaurus.io) site (`pnpm docs`). The guides:
73
75
 
74
76
  - [The layout and its slots](docs/docs/layout.md)
75
77
  - [Building your own UI on the engine](docs/docs/engine.md)
@@ -81,13 +83,15 @@ Guides and API pages are in [docs/](docs/), a [Docusaurus](https://docusaurus.io
81
83
 
82
84
  ## Demo
83
85
 
86
+ The demo is published at https://lvl99.github.io/paintcanvas/app/. To run it locally:
87
+
84
88
  ```sh
85
89
  pnpm dev
86
90
  ```
87
91
 
88
92
  Starts a development server and opens the demo in your browser. It uses the source directly, so saving a change to `src/`, `demo/` or `locales/` reloads the page with the change straight away. (The whole page reloads because custom elements can't be redefined in a running page, so the drawing starts again.)
89
93
 
90
- - http://localhost:5190/: a full-page PaintCanvas with a layers panel, the colour palette (from the colours at the top of the toolbar), a menu with input settings, keyboard shortcuts, settings import and export, a dark theme switch and a "New drawing" button. The drawing and the settings are kept in the browser, so they survive reloads.
94
+ - http://localhost:5190/: a full-page PaintCanvas with a layers panel, the colour palette (from the colours at the top of the toolbar), a menu with input settings, keyboard shortcuts, settings import and export, a theme picker (light, dark or system) and a "New drawing" button. The drawing and the settings, including the theme and language, are kept in the browser, so they survive reloads. Below 600px wide, the toolbar and the layers rail sit over the canvas.
91
95
  - http://localhost:5190/elements.html: the base elements.
92
96
 
93
97
  To try it with a pen or touch on another device (e.g. a tablet) on the same network, run `pnpm dev --host` and open the network address it prints. This makes the server reachable by anyone on that network.
@@ -115,18 +119,29 @@ Source layout:
115
119
 
116
120
  Tests sit in a `tests` folder beside the code. Unit tests (`*.test.ts`) run in jsdom; pixel tests (`*.browser.test.ts`) run in headless Chromium.
117
121
 
118
- | Command | Does |
119
- | ----------------------- | --------------------------------------------------------------------------------- |
120
- | `pnpm typecheck` | Checks types in the source, tests, scripts and demo |
121
- | `pnpm lint` | Checks the code with ESLint |
122
- | `pnpm format` | Formats files with Prettier (`pnpm format:check` only checks) |
123
- | `pnpm test` | Runs the unit tests |
124
- | `pnpm test:browser` | Runs the pixel tests in a real browser |
125
- | `pnpm test:all` | Runs both |
126
- | `pnpm coverage` | Runs both, with a coverage report in `coverage/` |
127
- | `pnpm build` | Builds the package into `dist/`: ES modules matching `src/`, with types |
128
- | `pnpm build:validators` | Regenerates the validators after changing the preset, message or document schemas |
129
- | `pnpm dev` | Starts the demo, reloading it when files change (`pnpm demo` does the same) |
130
- | `pnpm docs` | Starts the documentation site (`pnpm docs:build` builds it into `docs/build/`) |
122
+ | Command | Does |
123
+ | ----------------------- | ----------------------------------------------------------------------------------- |
124
+ | `pnpm typecheck` | Checks types in the source, tests, scripts and demo |
125
+ | `pnpm lint` | Checks the code with ESLint |
126
+ | `pnpm format` | Formats files with Prettier (`pnpm format:check` only checks) |
127
+ | `pnpm test` | Runs the unit tests |
128
+ | `pnpm test:browser` | Runs the pixel tests in a real browser |
129
+ | `pnpm test:all` | Runs both |
130
+ | `pnpm coverage` | Runs both, with a coverage report in `coverage/` |
131
+ | `pnpm build` | Builds the package into `dist/`: ES modules matching `src/`, with types |
132
+ | `pnpm build:validators` | Regenerates the validators after changing the preset, message or document schemas |
133
+ | `pnpm icons:build` | Optimises the icons' SVGs and makes their PNGs (`pnpm build` and `pnpm dev` run it) |
134
+ | `pnpm dev` | Starts the demo, reloading it when files change (`pnpm demo` does the same) |
135
+ | `pnpm docs` | Starts the documentation site (`pnpm docs:build` builds it into `docs/build/`) |
136
+ | `pnpm demo:build` | Builds the demo pages as a static site into `.pages/app/` |
137
+ | `pnpm pages:build` | Builds the GitHub Pages site (docs and demo) into `.pages/` |
138
+ | `pnpm pages:preview` | Serves the built site at http://localhost:5191/paintcanvas/ |
139
+ | `pnpm pages:deploy` | Builds the site from `main` and publishes it to the `gh-pages` branch |
140
+
141
+ Icons live in `src/ui/icons/images`, each as `icon-<name>.svg` (the 64 × 64 source artwork) and `icon-<name>@2x.png` (128 × 128, which the UI uses). Edit or add the SVG, then run `pnpm icons:build`: it optimises the SVGs with svgo and makes the PNGs from them. A test fails if a PNG doesn't match its SVG. Register a new icon in `src/ui/icons/index.ts`.
131
142
 
132
143
  Each folder in `src/lib` and `src/ui` is published as its own import path (e.g. `src/lib/classes/PaintCanvas` as `@lvl99dotcom/paintcanvas/lib/classes/PaintCanvas`). `sideEffects` in `package.json` lists the UI modules that register elements when loaded (`src/ui/index.ts` and each `src/ui/elements/*/index.ts`), so bundlers keep them; add any other module that registers elements there.
144
+
145
+ ### GitHub Pages
146
+
147
+ The docs and demo are published on GitHub Pages from the `gh-pages` branch, which holds only the built site. `pnpm pages:deploy` updates it: on a clean `main` that matches `origin/main`, it builds the site and commits and pushes it to `gh-pages`. Run it after each release, or whenever `main` has changes to the docs or demo. GitHub Pages is set to serve the root of the `gh-pages` branch (Settings → Pages).
@@ -1,4 +1,4 @@
1
- import type { BrushConfig, BrushesConfig } from "../../types";
1
+ import { type BrushConfig, type BrushesConfig } from "../../types";
2
2
  /** Hard round brushes */
3
3
  export declare const round: BrushesConfig;
4
4
  /** Soft-edged round brushes, for shading. Stamps are about a quarter of the size apart: closer, the soft edges add up and look hard. */
@@ -1,13 +1,16 @@
1
1
  import { BUILT_IN_AUTHOR } from "../../authors/index.js";
2
+ import { SmoothingType } from "../../types/index.js";
2
3
  import { sizedPresets } from "../sized/index.js";
3
4
  import round_default$1 from "./round.js";
4
5
  import soft_round_default from "./soft-round.js";
5
6
  // src/lib/brushes/presets/round/index.ts
6
7
  /** Settings shared by the round presets */
7
8
  var DEFAULT_CONFIG = {
8
- version: "1.0.0",
9
+ version: "1.1.0",
9
10
  author: BUILT_IN_AUTHOR,
10
- constrainProportions: true
11
+ constrainProportions: true,
12
+ smoothingType: SmoothingType.BezierCubic,
13
+ sampleRate: 5
11
14
  };
12
15
  /** Hard round brushes */
13
16
  var round = sizedPresets("round", "Round", [
@@ -15,13 +18,16 @@ var round = sizedPresets("round", "Round", [
15
18
  5,
16
19
  10,
17
20
  20,
18
- 50
21
+ 30,
22
+ 40,
23
+ 50,
24
+ 100,
25
+ 150
19
26
  ], {
20
27
  ...DEFAULT_CONFIG,
21
28
  image: round_default$1,
22
- sampleRate: 20,
23
29
  spacing: 1
24
- });
30
+ }, (size) => size === 1 ? { snapToPixelGrid: true } : {});
25
31
  /** Soft-edged round brushes, for shading. Stamps are about a quarter of the size apart: closer, the soft edges add up and look hard. */
26
32
  var softRound = sizedPresets("softRound", "Soft round", [
27
33
  5,
@@ -34,8 +40,7 @@ var softRound = sizedPresets("softRound", "Soft round", [
34
40
  150
35
41
  ], {
36
42
  ...DEFAULT_CONFIG,
37
- image: soft_round_default,
38
- sampleRate: 5
43
+ image: soft_round_default
39
44
  }, (size) => ({ spacing: Math.max(1, Math.round(size / 4)) }));
40
45
  var round_default = {
41
46
  ...round,
@@ -1,4 +1,4 @@
1
- import type { BrushConfig, BrushesConfig } from "../../types";
1
+ import { type BrushConfig, type BrushesConfig } from "../../types";
2
2
  /** Hard square brushes, snapped to the pixel grid */
3
3
  export declare const square: BrushesConfig;
4
4
  declare const _default: {
@@ -1,4 +1,5 @@
1
1
  import { BUILT_IN_AUTHOR } from "../../authors/index.js";
2
+ import { SmoothingType } from "../../types/index.js";
2
3
  import { sizedPresets } from "../sized/index.js";
3
4
  import square_default$1 from "./square.js";
4
5
  /** Hard square brushes, snapped to the pixel grid */
@@ -9,13 +10,16 @@ var square = sizedPresets("square", "Square", [
9
10
  20,
10
11
  30,
11
12
  40,
12
- 50
13
+ 50,
14
+ 100,
15
+ 150
13
16
  ], {
14
- version: "1.0.0",
17
+ version: "1.1.0",
15
18
  author: BUILT_IN_AUTHOR,
16
19
  constrainProportions: true,
17
20
  image: square_default$1,
18
- sampleRate: 50,
21
+ smoothingType: SmoothingType.BezierCubic,
22
+ sampleRate: 5,
19
23
  snapToPixelGrid: true
20
24
  }, (size) => ({ spacing: Math.max(1, Math.round(size / 10)) }));
21
25
  var square_default = { ...square };
@@ -34,7 +34,11 @@ export interface ShapeImageDependencies {
34
34
  */
35
35
  export declare const readShapeImage: (blob: Blob, { decode, createCanvas, }?: ShapeImageDependencies) => Promise<ShapeImage>;
36
36
  /**
37
- * Read a brush shape from a PNG image on the clipboard. Throws a `ShapeImageError` if there is none or it can't be
38
- * used.
37
+ * Read a brush shape from a PNG image on the clipboard. Without one (or when the browser won't let it be read), the
38
+ * `fallback` PNG is used if there is one (e.g. image data copied on the canvas, `canvas.copiedPng()`). Throws a
39
+ * `ShapeImageError` if there is none or it can't be used.
40
+ *
41
+ * @example
42
+ * const shape = await readClipboardShape(navigator.clipboard, {}, () => canvas.copiedPng());
39
43
  */
40
- export declare const readClipboardShape: (clipboard?: Pick<Clipboard, "read">, deps?: ShapeImageDependencies) => Promise<ShapeImage>;
44
+ export declare const readClipboardShape: (clipboard?: Pick<Clipboard, "read">, deps?: ShapeImageDependencies, fallback?: () => Promise<Blob | undefined>) => Promise<ShapeImage>;
@@ -44,18 +44,24 @@ var readShapeImage = async (blob, { decode = (file) => createImageBitmap(file),
44
44
  };
45
45
  };
46
46
  /**
47
- * Read a brush shape from a PNG image on the clipboard. Throws a `ShapeImageError` if there is none or it can't be
48
- * used.
47
+ * Read a brush shape from a PNG image on the clipboard. Without one (or when the browser won't let it be read), the
48
+ * `fallback` PNG is used if there is one (e.g. image data copied on the canvas, `canvas.copiedPng()`). Throws a
49
+ * `ShapeImageError` if there is none or it can't be used.
50
+ *
51
+ * @example
52
+ * const shape = await readClipboardShape(navigator.clipboard, {}, () => canvas.copiedPng());
49
53
  */
50
- var readClipboardShape = async (clipboard = navigator.clipboard, deps = {}) => {
51
- let items;
54
+ var readClipboardShape = async (clipboard = navigator.clipboard, deps = {}, fallback = async () => void 0) => {
55
+ const png = await clipboardPng(clipboard) ?? await fallback();
56
+ if (!png) throw new ShapeImageError(SHAPE_ERRORS.noImage);
57
+ return readShapeImage(png, deps);
58
+ };
59
+ /** The PNG image on the clipboard, if there is one and it can be read */
60
+ var clipboardPng = async (clipboard) => {
52
61
  try {
53
- items = await clipboard.read();
62
+ return await (await clipboard.read()).find((entry) => entry.types.includes("image/png"))?.getType("image/png");
54
63
  } catch {
55
- throw new ShapeImageError(SHAPE_ERRORS.noImage);
64
+ return;
56
65
  }
57
- const item = items.find((entry) => entry.types.includes("image/png"));
58
- if (!item) throw new ShapeImageError(SHAPE_ERRORS.noImage);
59
- return readShapeImage(await item.getType("image/png"), deps);
60
66
  };
61
67
  export { MAX_SHAPE_FILE_BYTES, MAX_SHAPE_SIZE, SHAPE_ERRORS, ShapeImageError, readClipboardShape, readShapeImage };
@@ -0,0 +1,18 @@
1
+ /** A brush shape that comes with PaintCanvas (all square) */
2
+ export interface BuiltInShape {
3
+ id: string;
4
+ name: string;
5
+ /** PNG data URL */
6
+ image: string;
7
+ }
8
+ /** The brush shapes that come with PaintCanvas, the ones its presets use */
9
+ export declare const BUILT_IN_SHAPES: readonly BuiltInShape[];
10
+ /**
11
+ * Which built-in shape a brush uses: its id, "round" for a brush without an image (drawn round), or undefined for one
12
+ * of the user's own (uploaded or pasted).
13
+ *
14
+ * @example
15
+ * builtInShapeId(undefined); // "round"
16
+ * builtInShapeId("data:image/png;base64,…"); // undefined, unless it's a built-in shape's image
17
+ */
18
+ export declare const builtInShapeId: (image: unknown) => string | undefined;
@@ -0,0 +1,86 @@
1
+ import round_default from "../presets/round/round.js";
2
+ import soft_round_default from "../presets/round/soft-round.js";
3
+ import square_default from "../presets/square/square.js";
4
+ import pencil_2b_default from "../presets/drawing/pencil-2b.js";
5
+ import pencil_hb_default from "../presets/drawing/pencil-hb.js";
6
+ import felt_pen_default from "../presets/drawing/felt-pen.js";
7
+ import brush_pen_default from "../presets/drawing/brush-pen.js";
8
+ import textured_paint_default from "../presets/painting/textured-paint.js";
9
+ import watery_paint_default from "../presets/painting/watery-paint.js";
10
+ import splatter_default from "../presets/painting/splatter.js";
11
+ import hair_default from "../presets/painting/hair.js";
12
+ import grass_default from "../presets/painting/grass.js";
13
+ // src/lib/brushes/shapes/index.ts
14
+ /** The brush shapes that come with PaintCanvas, the ones its presets use */
15
+ var BUILT_IN_SHAPES = [
16
+ {
17
+ id: "round",
18
+ name: "Round",
19
+ image: round_default
20
+ },
21
+ {
22
+ id: "softRound",
23
+ name: "Soft round",
24
+ image: soft_round_default
25
+ },
26
+ {
27
+ id: "square",
28
+ name: "Square",
29
+ image: square_default
30
+ },
31
+ {
32
+ id: "pencil2b",
33
+ name: "Pencil 2B",
34
+ image: pencil_2b_default
35
+ },
36
+ {
37
+ id: "pencilHb",
38
+ name: "Pencil HB",
39
+ image: pencil_hb_default
40
+ },
41
+ {
42
+ id: "feltPen",
43
+ name: "Felt pen",
44
+ image: felt_pen_default
45
+ },
46
+ {
47
+ id: "brushPen",
48
+ name: "Brush pen",
49
+ image: brush_pen_default
50
+ },
51
+ {
52
+ id: "texturedPaint",
53
+ name: "Textured paint",
54
+ image: textured_paint_default
55
+ },
56
+ {
57
+ id: "wateryPaint",
58
+ name: "Watery paint",
59
+ image: watery_paint_default
60
+ },
61
+ {
62
+ id: "splatter",
63
+ name: "Splatter",
64
+ image: splatter_default
65
+ },
66
+ {
67
+ id: "hair",
68
+ name: "Hair",
69
+ image: hair_default
70
+ },
71
+ {
72
+ id: "grass",
73
+ name: "Grass",
74
+ image: grass_default
75
+ }
76
+ ];
77
+ /**
78
+ * Which built-in shape a brush uses: its id, "round" for a brush without an image (drawn round), or undefined for one
79
+ * of the user's own (uploaded or pasted).
80
+ *
81
+ * @example
82
+ * builtInShapeId(undefined); // "round"
83
+ * builtInShapeId("data:image/png;base64,…"); // undefined, unless it's a built-in shape's image
84
+ */
85
+ var builtInShapeId = (image) => image === void 0 ? "round" : BUILT_IN_SHAPES.find((shape) => shape.image === image)?.id;
86
+ export { BUILT_IN_SHAPES, builtInShapeId };
@@ -26,6 +26,19 @@ export declare const clamp: (value: number, min: number, max: number) => number;
26
26
  * stepOpacity(1, 1); // 10
27
27
  */
28
28
  export declare const stepOpacity: (percent: number, direction: 1 | -1) => number;
29
+ /**
30
+ * A brush size with the given width and proportions (width ÷ height), in whole pixels. The width is kept within the
31
+ * range where the smaller side is at least `MIN_BRUSH_SIZE` and the larger side at most `MAX_BRUSH_SIZE`, so the
32
+ * proportions are kept at either end.
33
+ *
34
+ * @example
35
+ * proportionalSize(0, 3); // { width: 3, height: 1 }
36
+ * proportionalSize(1000, 0.5); // { width: 128, height: 256 }
37
+ */
38
+ export declare const proportionalSize: (width: number, aspect: number) => {
39
+ width: number;
40
+ height: number;
41
+ };
29
42
  /**
30
43
  * The settings of a brush or eraser: the selected preset, an editable copy of its settings (changed from the tool
31
44
  * settings bar or the configuration window, without changing the preset) and the size. The brush and eraser each have
@@ -49,6 +62,11 @@ export default class BrushSettings extends EventTarget {
49
62
  presetId: string;
50
63
  /** The current settings: a copy of the selected preset, with any changes */
51
64
  private current;
65
+ /**
66
+ * The brush's proportions (width ÷ height), kept apart from the rounded size so resizing to either end and back
67
+ * doesn't change them. Set by the preset and by setting the size while proportions aren't constrained.
68
+ */
69
+ private aspect;
52
70
  constructor(presets: BrushesConfig);
53
71
  /** The current settings: the selected preset's, with any changes made since selecting it */
54
72
  get preset(): BrushConfig;
@@ -79,13 +97,18 @@ export default class BrushSettings extends EventTarget {
79
97
  */
80
98
  setHeight(height: number): void;
81
99
  /**
82
- * Set the size, clamped to 1–256 pixels. If the settings constrain proportions, height follows width.
100
+ * Set the size, clamped to 1–256 pixels. If the settings constrain proportions, height follows width, and the width
101
+ * stops where either side would leave that range.
83
102
  */
84
103
  setSize(width: number, height?: number): void;
85
104
  /**
86
- * Change the size by a number of pixels (positive = bigger). Used when dragging up/right (bigger) or down/left (smaller).
105
+ * Change the size by a number of pixels (positive = bigger), keeping the proportions: the larger side changes by
106
+ * `delta`. Stops when the smaller side reaches 1 pixel or the larger 256. Used when dragging up/right (bigger) or
107
+ * down/left (smaller).
87
108
  */
88
109
  resize(delta: number): void;
110
+ /** The larger side (what `resize` changes), which `grow` and `shrink` step from */
111
+ get largerSide(): number;
89
112
  /**
90
113
  * Increase the size by one step (at least 1 pixel).
91
114
  */
@@ -108,6 +131,10 @@ export default class BrushSettings extends EventTarget {
108
131
  * Use a preset's (or configuration's) default size.
109
132
  */
110
133
  private applyPresetSize;
134
+ /**
135
+ * Set the width, with the height following the stored proportions (see `proportionalSize`).
136
+ */
137
+ private setProportionalSize;
111
138
  /**
112
139
  * Dispatch a `change` event.
113
140
  */
@@ -30,6 +30,22 @@ var stepOpacity = (percent, direction) => {
30
30
  return clamp((direction > 0 ? Math.floor(steps + 1e-9) + 1 : Math.ceil(steps - 1e-9) - 1) * 10, 1, 100);
31
31
  };
32
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
+ /**
33
49
  * The settings of a brush or eraser: the selected preset, an editable copy of its settings (changed from the tool
34
50
  * settings bar or the configuration window, without changing the preset) and the size. The brush and eraser each have
35
51
  * their own instance.
@@ -52,6 +68,11 @@ var BrushSettings = class extends EventTarget {
52
68
  presetId;
53
69
  /** The current settings: a copy of the selected preset, with any changes */
54
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;
55
76
  constructor(presets) {
56
77
  super();
57
78
  const first = Object.values(presets)[0];
@@ -118,39 +139,47 @@ var BrushSettings = class extends EventTarget {
118
139
  * Set the height, clamped to 1–256 pixels. If the settings constrain proportions, width follows height.
119
140
  */
120
141
  setHeight(height) {
121
- if (!this.current.constrainProportions) {
122
- this.setSize(this.width, height);
123
- return;
124
- }
125
- const ratio = this.width / this.height;
126
- this.setSize(clamp(height, 1, 256) * ratio);
142
+ if (this.current.constrainProportions) this.setProportionalSize(height * this.aspect);
143
+ else this.setSize(this.width, height);
127
144
  }
128
145
  /**
129
- * 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.
130
148
  */
131
149
  setSize(width, height = width) {
132
- const ratio = this.height / this.width;
150
+ if (this.current.constrainProportions) {
151
+ this.setProportionalSize(width);
152
+ return;
153
+ }
133
154
  this.width = Math.round(clamp(width, 1, 256));
134
- 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;
135
157
  this.notify();
136
158
  }
137
159
  /**
138
- * 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).
139
163
  */
140
164
  resize(delta) {
141
- 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);
142
171
  }
143
172
  /**
144
173
  * Increase the size by one step (at least 1 pixel).
145
174
  */
146
175
  grow() {
147
- 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)));
148
177
  }
149
178
  /**
150
179
  * Decrease the size by one step (at least 1 pixel).
151
180
  */
152
181
  shrink() {
153
- 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)));
154
183
  }
155
184
  /**
156
185
  * Increase the opacity by one step (see `stepOpacity`).
@@ -175,6 +204,14 @@ var BrushSettings = class extends EventTarget {
175
204
  applyPresetSize(preset) {
176
205
  this.width = preset.width ?? 1;
177
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();
178
215
  }
179
216
  /**
180
217
  * Dispatch a `change` event.
@@ -183,4 +220,4 @@ var BrushSettings = class extends EventTarget {
183
220
  this.dispatchEvent(new Event("change"));
184
221
  }
185
222
  };
186
- export { BRUSH_OPACITY_STEP, BRUSH_SIZE_STEP, MAX_BRUSH_SIZE, MIN_BRUSH_OPACITY, MIN_BRUSH_SIZE, clamp, BrushSettings as default, stepOpacity };
223
+ export { BRUSH_OPACITY_STEP, BRUSH_SIZE_STEP, MAX_BRUSH_SIZE, MIN_BRUSH_OPACITY, MIN_BRUSH_SIZE, clamp, BrushSettings as default, proportionalSize, stepOpacity };
@@ -1,5 +1,17 @@
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
  /**
@@ -9,7 +21,10 @@ export declare const SHORTCUT_EVENT = "pc-shortcut";
9
21
  *
10
22
  * Tool selection, brush/eraser size and opacity, interface visibility, select all/invert/deselect, undo/redo,
11
23
  * cut/copy/paste, deleting the masked image data (Delete or Backspace), fills, swapping colours, zooming, layer opacity
12
- * and finishing or cancelling a lasso shape are handled here; other actions (and Escape/Enter for later features)
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)
13
28
  * dispatch an `pc-shortcut` event with the action as `detail`. Holding the hold-to-pan key (Space by default) pans
14
29
  * temporarily; releasing it returns to the selected tool.
15
30
  *
@@ -24,8 +39,15 @@ export default class KeyboardShortcuts {
24
39
  private heldKey?;
25
40
  private readonly onKeyDown;
26
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?;
27
48
  private readonly onBlur;
28
- constructor(app: PaintCanvas);
49
+ private readonly onPaste;
50
+ constructor(app: PaintCanvas, decodeImage?: (blob: Blob) => Promise<ImageBitmap>);
29
51
  /**
30
52
  * Start listening for key presses on a target (usually `window`).
31
53
  */
@@ -38,6 +60,13 @@ export default class KeyboardShortcuts {
38
60
  * Run the shortcut for a key press, if any. Returns the action run.
39
61
  */
40
62
  handle(event: KeyboardEvent): ShortcutAction | undefined;
63
+ /**
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;
41
70
  /**
42
71
  * Return to the selected tool when the hold-to-pan key is released (whatever modifiers are held by then).
43
72
  */