@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.
- package/README.md +30 -15
- package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
- package/dist/lib/brushes/StrokeEngine/index.js +26 -28
- package/dist/lib/brushes/presets/round/index.d.ts +1 -1
- package/dist/lib/brushes/presets/round/index.js +12 -7
- package/dist/lib/brushes/presets/square/index.d.ts +1 -1
- package/dist/lib/brushes/presets/square/index.js +7 -3
- package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
- package/dist/lib/brushes/shapeImage/index.js +15 -9
- package/dist/lib/brushes/shapes/index.d.ts +18 -0
- package/dist/lib/brushes/shapes/index.js +86 -0
- package/dist/lib/classes/BrushSettings/index.d.ts +53 -2
- package/dist/lib/classes/BrushSettings/index.js +85 -14
- package/dist/lib/classes/KeyboardShortcuts/index.d.ts +47 -7
- package/dist/lib/classes/KeyboardShortcuts/index.js +145 -20
- package/dist/lib/classes/PaintCanvas/index.d.ts +123 -27
- package/dist/lib/classes/PaintCanvas/index.js +208 -41
- package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
- package/dist/lib/classes/PaintCanvas/tool.js +18 -0
- package/dist/lib/classes/Palette/index.d.ts +58 -0
- package/dist/lib/classes/Palette/index.js +92 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +53 -0
- package/dist/lib/classes/SettingsAutosave/index.js +98 -0
- package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
- package/dist/lib/classes/ShortcutSettings/index.js +110 -0
- package/dist/lib/classes/UiSettings/index.d.ts +68 -0
- package/dist/lib/classes/UiSettings/index.js +82 -0
- package/dist/lib/palette/index.d.ts +49 -0
- package/dist/lib/palette/index.js +82 -0
- package/dist/lib/palette/schema.d.ts +45 -0
- package/dist/lib/palette/schema.js +41 -0
- package/dist/lib/rendering/cursors/index.d.ts +2 -0
- package/dist/lib/rendering/cursors/index.js +3 -1
- package/dist/lib/rendering/sample/index.js +3 -2
- package/dist/lib/settings/index.d.ts +63 -0
- package/dist/lib/settings/index.js +93 -0
- package/dist/lib/settings/schema.d.ts +168 -0
- package/dist/lib/settings/schema.js +116 -0
- package/dist/lib/storage/index.d.ts +36 -5
- package/dist/lib/storage/index.js +87 -29
- package/dist/lib/utils/colour/index.d.ts +72 -0
- package/dist/lib/utils/colour/index.js +154 -0
- package/dist/lib/utils/shortcuts/index.d.ts +174 -11
- package/dist/lib/utils/shortcuts/index.js +391 -145
- package/dist/lib/validation/validators.generated.d.ts +5 -0
- package/dist/lib/validation/validators.generated.js +1040 -1
- package/dist/locales/en-GB.js +96 -3
- package/dist/locales/en-US.js +96 -3
- package/dist/locales/fr.js +96 -3
- package/dist/locales/ja.js +96 -3
- package/dist/locales/ko.js +96 -3
- package/dist/locales/zh-Hans.js +96 -3
- package/dist/locales/zh-Hant.js +96 -3
- package/dist/package.js +1 -1
- package/dist/testing/recordingCanvas.js +2 -1
- package/dist/ui/base/index.d.ts +2 -2
- package/dist/ui/dismiss/index.d.ts +21 -0
- package/dist/ui/dismiss/index.js +27 -0
- package/dist/ui/drag/index.d.ts +51 -0
- package/dist/ui/drag/index.js +81 -0
- package/dist/ui/elements/PaintCanvas/index.d.ts +57 -50
- package/dist/ui/elements/PaintCanvas/index.js +141 -174
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
- package/dist/ui/elements/PcAppSettings/index.js +222 -0
- package/dist/ui/elements/PcBrushConfig/index.d.ts +9 -2
- package/dist/ui/elements/PcBrushConfig/index.js +47 -7
- package/dist/ui/elements/PcBrushConfig/style.js +1 -1
- package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
- package/dist/ui/elements/PcBrushSize/index.js +66 -0
- package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
- package/dist/ui/elements/PcBrushTest/index.js +73 -5
- package/dist/ui/elements/PcBrushTest/style.js +1 -1
- package/dist/ui/elements/PcCanvas/index.d.ts +22 -2
- package/dist/ui/elements/PcCanvas/index.js +60 -15
- package/dist/ui/elements/PcCanvasMap/style.js +1 -1
- package/dist/ui/elements/PcClipboardTools/index.d.ts +48 -0
- package/dist/ui/elements/PcClipboardTools/index.js +118 -0
- package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
- package/dist/ui/elements/PcColourPair/index.js +125 -0
- package/dist/ui/elements/PcContextMenu/index.d.ts +71 -34
- package/dist/ui/elements/PcContextMenu/index.js +128 -97
- package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
- package/dist/ui/elements/PcInputSettings/index.js +16 -1
- package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
- package/dist/ui/elements/PcLayerItem/index.js +39 -91
- package/dist/ui/elements/PcLayerItem/style.js +1 -1
- package/dist/ui/elements/PcMenu/index.d.ts +138 -0
- package/dist/ui/elements/PcMenu/index.js +296 -0
- package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
- package/dist/ui/elements/PcNumberSlider/index.js +247 -0
- package/dist/ui/elements/PcPane/index.d.ts +120 -0
- package/dist/ui/elements/PcPane/index.js +394 -0
- package/dist/ui/elements/PcPanel/index.d.ts +26 -8
- package/dist/ui/elements/PcPanel/index.js +76 -8
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +22 -21
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +24 -57
- package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
- package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
- package/dist/ui/elements/PcPanelColours/index.js +650 -0
- package/dist/ui/elements/PcPanelColours/style.js +3 -0
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
- package/dist/ui/elements/PcPanelLayers/style.js +3 -0
- package/dist/ui/elements/PcPopover/index.d.ts +39 -1
- package/dist/ui/elements/PcPopover/index.js +72 -4
- package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
- package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
- package/dist/ui/elements/PcThemePicker/index.js +22 -21
- package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
- package/dist/ui/elements/PcToolSettings/index.js +384 -319
- package/dist/ui/elements/PcToolSettings/style.js +1 -1
- package/dist/ui/elements/PcToolbar/index.d.ts +62 -12
- package/dist/ui/elements/PcToolbar/index.js +119 -57
- package/dist/ui/elements/PcToolbar/style.js +1 -1
- package/dist/ui/icons/images/icon-brush@2x.js +3 -0
- package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
- package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
- package/dist/ui/icons/images/icon-cog@2x.js +3 -0
- package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
- package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
- package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
- package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
- package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
- package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
- package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
- package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
- package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
- package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
- package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
- package/dist/ui/icons/images/icon-layers@2x.js +3 -0
- package/dist/ui/icons/images/icon-linked@2x.js +3 -0
- package/dist/ui/icons/images/icon-locked@2x.js +3 -0
- package/dist/ui/icons/images/icon-moon@2x.js +3 -0
- package/dist/ui/icons/images/icon-pan@2x.js +3 -0
- package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
- package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
- package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
- package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
- package/dist/ui/icons/images/icon-redo@2x.js +3 -0
- package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
- package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
- package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
- package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
- package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
- package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
- package/dist/ui/icons/images/icon-star@2x.js +3 -0
- package/dist/ui/icons/images/icon-sun@2x.js +3 -0
- package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
- package/dist/ui/icons/images/icon-undo@2x.js +3 -0
- package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
- package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
- package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
- package/dist/ui/icons/images/icon-visible@2x.js +3 -0
- package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
- package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
- package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
- package/dist/ui/icons/index.d.ts +1 -1
- package/dist/ui/icons/index.js +84 -72
- package/dist/ui/index.d.ts +13 -2
- package/dist/ui/index.js +16 -4
- package/dist/ui/layout/index.d.ts +9 -1
- package/dist/ui/layout/index.js +11 -2
- package/dist/ui/tokens/index.d.ts +5 -4
- package/dist/ui/tokens/index.js +15 -8
- package/locales/en-GB.json +96 -3
- package/locales/en-US.json +96 -3
- package/locales/fr.json +96 -3
- package/locales/ja.json +96 -3
- package/locales/ko.json +96 -3
- package/locales/zh-Hans.json +96 -3
- package/locales/zh-Hant.json +96 -3
- package/package.json +11 -4
- package/dist/ui/elements/PcBrushMenu/style.js +0 -3
- package/dist/ui/elements/PcLayersMenu/style.js +0 -3
- package/dist/ui/icons/images/brush.js +0 -3
- package/dist/ui/icons/images/circle-mask.js +0 -3
- package/dist/ui/icons/images/clipboard.js +0 -3
- package/dist/ui/icons/images/cursor.js +0 -3
- package/dist/ui/icons/images/eraser.js +0 -3
- package/dist/ui/icons/images/eyedropper.js +0 -3
- package/dist/ui/icons/images/flip-horizontal.js +0 -3
- package/dist/ui/icons/images/flip-vertical.js +0 -3
- package/dist/ui/icons/images/focus-pen.js +0 -3
- package/dist/ui/icons/images/freehand-mask.js +0 -3
- package/dist/ui/icons/images/hidden.js +0 -3
- package/dist/ui/icons/images/layers.js +0 -3
- package/dist/ui/icons/images/linked.js +0 -3
- package/dist/ui/icons/images/locked.js +0 -3
- package/dist/ui/icons/images/moon.js +0 -3
- package/dist/ui/icons/images/pan.js +0 -3
- package/dist/ui/icons/images/polygon-mask.js +0 -3
- package/dist/ui/icons/images/pressure.js +0 -3
- package/dist/ui/icons/images/randomise.js +0 -3
- package/dist/ui/icons/images/rectangle-mask.js +0 -3
- package/dist/ui/icons/images/redo.js +0 -3
- package/dist/ui/icons/images/rotate-90-left.js +0 -3
- package/dist/ui/icons/images/rotate-90-right.js +0 -3
- package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
- package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
- package/dist/ui/icons/images/smoothing-linear.js +0 -3
- package/dist/ui/icons/images/star.js +0 -3
- package/dist/ui/icons/images/sun.js +0 -3
- package/dist/ui/icons/images/tilt.js +0 -3
- package/dist/ui/icons/images/undo.js +0 -3
- package/dist/ui/icons/images/unlinked.js +0 -3
- package/dist/ui/icons/images/unlocked.js +0 -3
- package/dist/ui/icons/images/visible.js +0 -3
- package/dist/ui/icons/images/zoom-in.js +0 -3
- package/dist/ui/icons/images/zoom-out.js +0 -3
- 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
|
-
|
|
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
|
|
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
|
|
130
|
-
| `pnpm
|
|
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 (
|
|
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
|
-
/**
|
|
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 (
|
|
45
|
-
return
|
|
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 (
|
|
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,
|
|
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,
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,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.
|
|
17
|
+
version: "1.1.0",
|
|
15
18
|
author: BUILT_IN_AUTHOR,
|
|
16
19
|
constrainProportions: true,
|
|
17
20
|
image: square_default$1,
|
|
18
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
return await (await clipboard.read()).find((entry) => entry.types.includes("image/png"))?.getType("image/png");
|
|
54
63
|
} catch {
|
|
55
|
-
|
|
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)
|
|
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
|
*/
|