@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
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, a menu with input settings, a dark theme switch and a "New drawing" button. The drawing is kept in the browser, so it survives 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).
@@ -19,7 +19,7 @@ export interface StrokeOptions {
19
19
  width: number;
20
20
  height: number;
21
21
  };
22
- /** Paint colour as a hex string */
22
+ /** Paint colour as a hex string (for the eraser, the colour it paints when the layer's alpha lock is on) */
23
23
  colour: string;
24
24
  tip: BrushTip;
25
25
  /** Whether input comes from a pen (enables pressure and tilt) */
@@ -76,7 +76,7 @@ export interface StrokeCommitDetail {
76
76
  * Stamps are drawn at full strength into a stroke buffer, so overlapping stamps within one stroke do not build up opacity.
77
77
  * The buffer is merged into the layer with the stroke's opacity (the preset's `alpha`):
78
78
  * - Brush: painted over the layer (or only onto existing pixels when the layer's alpha lock is on).
79
- * - Eraser: removes the buffer's shape from the layer (does nothing when alpha lock is on).
79
+ * - Eraser: removes the buffer's shape from the layer (or paints its colour onto existing pixels when alpha lock is on).
80
80
  * - With a mask, the buffer is first cut to the mask, so the stroke only applies inside it.
81
81
  *
82
82
  * While drawing, `preview` provides the layer with the stroke merged in, to draw instead of the layer. The stroke is written into the layer when it ends.
@@ -39,10 +39,13 @@ var clipRect = (rect, width, height) => {
39
39
  height: bottom - y
40
40
  } : void 0;
41
41
  };
42
- /** How the stroke buffer is merged into the layer, or null if the stroke cannot change it */
42
+ /**
43
+ * How the stroke buffer is merged into the layer: with alpha lock on, both brush and eraser paint only onto existing
44
+ * pixels; otherwise the brush paints over the layer and the eraser removes from it.
45
+ */
43
46
  var compositeOperation = (mode, alphaLock) => {
44
- if (mode === "erase") return alphaLock ? null : "destination-out";
45
- return alphaLock ? "source-atop" : "source-over";
47
+ if (alphaLock) return "source-atop";
48
+ return mode === "erase" ? "destination-out" : "source-over";
46
49
  };
47
50
  /** Event dispatched just before a finished stroke is written into its layer */
48
51
  var BEFORE_COMMIT_EVENT = "beforecommit";
@@ -56,7 +59,7 @@ var STROKE_END_EVENT = "strokeend";
56
59
  * Stamps are drawn at full strength into a stroke buffer, so overlapping stamps within one stroke do not build up opacity.
57
60
  * The buffer is merged into the layer with the stroke's opacity (the preset's `alpha`):
58
61
  * - Brush: painted over the layer (or only onto existing pixels when the layer's alpha lock is on).
59
- * - Eraser: removes the buffer's shape from the layer (does nothing when alpha lock is on).
62
+ * - Eraser: removes the buffer's shape from the layer (or paints its colour onto existing pixels when alpha lock is on).
60
63
  * - With a mask, the buffer is first cut to the mask, so the stroke only applies inside it.
61
64
  *
62
65
  * While drawing, `preview` provides the layer with the stroke merged in, to draw instead of the layer. The stroke is written into the layer when it ends.
@@ -215,7 +218,7 @@ var StrokeEngine = class extends EventTarget {
215
218
  const active = this.active;
216
219
  if (points.length === 0) return;
217
220
  if (this.debugEnabled) this.debugTrace.stamps.push(...points);
218
- const { preset, size, tip, colour, mode, isPen } = active.options;
221
+ const { preset, size, tip, colour, isPen } = active.options;
219
222
  const { random } = active;
220
223
  const ctx = this.buffer.getContext("2d");
221
224
  const stampPreset = {
@@ -230,7 +233,7 @@ var StrokeEngine = class extends EventTarget {
230
233
  y: point.y,
231
234
  params: varied
232
235
  };
233
- tip.stamp(ctx, x, y, params, mode === "erase" ? "#000000" : colour);
236
+ tip.stamp(ctx, x, y, params, colour);
234
237
  const reach = Math.hypot(params.width, params.height) / 2 + STAMP_MARGIN;
235
238
  changed = unionRect(changed, {
236
239
  x: x + params.offsetX - reach,
@@ -258,12 +261,9 @@ var StrokeEngine = class extends EventTarget {
258
261
  ctx.clearRect(x, y, width, height);
259
262
  ctx.drawImage(layer.canvas, x, y, width, height, x, y, width, height);
260
263
  }
261
- const operation = compositeOperation(mode, layer.alphaLock);
262
- if (operation) {
263
- ctx.globalAlpha = Math.max(0, Math.min(1, preset.alpha ?? 1));
264
- ctx.globalCompositeOperation = operation;
265
- ctx.drawImage(this.strokeImage(active, area), x, y, width, height, x, y, width, height);
266
- }
264
+ ctx.globalAlpha = Math.max(0, Math.min(1, preset.alpha ?? 1));
265
+ ctx.globalCompositeOperation = compositeOperation(mode, layer.alphaLock);
266
+ ctx.drawImage(this.strokeImage(active, area), x, y, width, height, x, y, width, height);
267
267
  ctx.restore();
268
268
  }
269
269
  /**
@@ -289,22 +289,20 @@ var StrokeEngine = class extends EventTarget {
289
289
  commit(active) {
290
290
  const { layer, mode, preset, size, colour, isPen } = active.options;
291
291
  if (active.dirty) {
292
- if (compositeOperation(mode, layer.alphaLock)) {
293
- const stroke = {
294
- mode,
295
- preset,
296
- size,
297
- colour,
298
- isPen,
299
- seed: active.seed,
300
- points: active.points
301
- };
302
- this.dispatchEvent(new CustomEvent(BEFORE_COMMIT_EVENT, { detail: {
303
- layer,
304
- area: active.dirty,
305
- stroke
306
- } }));
307
- }
292
+ const stroke = {
293
+ mode,
294
+ preset,
295
+ size,
296
+ colour,
297
+ isPen,
298
+ seed: active.seed,
299
+ points: active.points
300
+ };
301
+ this.dispatchEvent(new CustomEvent(BEFORE_COMMIT_EVENT, { detail: {
302
+ layer,
303
+ area: active.dirty,
304
+ stroke
305
+ } }));
308
306
  this.mergeInto(active.options.layer.canvas, active, active.dirty, false);
309
307
  this.clearBuffer(active.dirty);
310
308
  this.changedArea = unionRect(this.changedArea, active.dirty);
@@ -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 };
@@ -5,6 +5,10 @@ export declare const MIN_BRUSH_SIZE = 1;
5
5
  export declare const MAX_BRUSH_SIZE = 256;
6
6
  /** Fraction of the current size added or removed by `grow` and `shrink` ("]" and "[" keys) */
7
7
  export declare const BRUSH_SIZE_STEP = 0.1;
8
+ /** Opacity step for the opacity keyboard shortcuts, in percent */
9
+ export declare const BRUSH_OPACITY_STEP = 10;
10
+ /** Lowest opacity the opacity keyboard shortcuts go to, in percent */
11
+ export declare const MIN_BRUSH_OPACITY = 1;
8
12
  /**
9
13
  * Clamp a value between min and max.
10
14
  *
@@ -12,6 +16,29 @@ export declare const BRUSH_SIZE_STEP = 0.1;
12
16
  * clamp(300, 1, 256); // 256
13
17
  */
14
18
  export declare const clamp: (value: number, min: number, max: number) => number;
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
+ 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
+ };
15
42
  /**
16
43
  * The settings of a brush or eraser: the selected preset, an editable copy of its settings (changed from the tool
17
44
  * settings bar or the configuration window, without changing the preset) and the size. The brush and eraser each have
@@ -35,6 +62,11 @@ export default class BrushSettings extends EventTarget {
35
62
  presetId: string;
36
63
  /** The current settings: a copy of the selected preset, with any changes */
37
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;
38
70
  constructor(presets: BrushesConfig);
39
71
  /** The current settings: the selected preset's, with any changes made since selecting it */
40
72
  get preset(): BrushConfig;
@@ -65,13 +97,18 @@ export default class BrushSettings extends EventTarget {
65
97
  */
66
98
  setHeight(height: number): void;
67
99
  /**
68
- * 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.
69
102
  */
70
103
  setSize(width: number, height?: number): void;
71
104
  /**
72
- * 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).
73
108
  */
74
109
  resize(delta: number): void;
110
+ /** The larger side (what `resize` changes), which `grow` and `shrink` step from */
111
+ get largerSide(): number;
75
112
  /**
76
113
  * Increase the size by one step (at least 1 pixel).
77
114
  */
@@ -80,10 +117,24 @@ export default class BrushSettings extends EventTarget {
80
117
  * Decrease the size by one step (at least 1 pixel).
81
118
  */
82
119
  shrink(): void;
120
+ /**
121
+ * Increase the opacity by one step (see `stepOpacity`).
122
+ */
123
+ moreOpaque(): void;
124
+ /**
125
+ * Decrease the opacity by one step (see `stepOpacity`).
126
+ */
127
+ lessOpaque(): void;
128
+ /** Change the opacity by one step up or down */
129
+ private stepOpacity;
83
130
  /**
84
131
  * Use a preset's (or configuration's) default size.
85
132
  */
86
133
  private applyPresetSize;
134
+ /**
135
+ * Set the width, with the height following the stored proportions (see `proportionalSize`).
136
+ */
137
+ private setProportionalSize;
87
138
  /**
88
139
  * Dispatch a `change` event.
89
140
  */