@lvl99dotcom/paintcanvas 0.0.0-stage → 0.3.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/LICENSE +201 -0
- package/README.md +131 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/lib/brushes/BrushTipCache/index.d.ts +32 -0
- package/dist/lib/brushes/BrushTipCache/index.js +60 -0
- package/dist/lib/brushes/StrokeEngine/index.d.ts +171 -0
- package/dist/lib/brushes/StrokeEngine/index.js +324 -0
- package/dist/lib/brushes/StrokePath/index.d.ts +53 -0
- package/dist/lib/brushes/StrokePath/index.js +161 -0
- package/dist/lib/brushes/StrokeSampler/index.d.ts +55 -0
- package/dist/lib/brushes/StrokeSampler/index.js +75 -0
- package/dist/lib/brushes/authors/index.d.ts +55 -0
- package/dist/lib/brushes/authors/index.js +60 -0
- package/dist/lib/brushes/configRanges/index.d.ts +52 -0
- package/dist/lib/brushes/configRanges/index.js +206 -0
- package/dist/lib/brushes/index.d.ts +5 -0
- package/dist/lib/brushes/index.js +19 -0
- package/dist/lib/brushes/params/index.d.ts +108 -0
- package/dist/lib/brushes/params/index.js +139 -0
- package/dist/lib/brushes/presetFile/index.d.ts +32 -0
- package/dist/lib/brushes/presetFile/index.js +55 -0
- package/dist/lib/brushes/presets/drawing/brush-pen.js +3 -0
- package/dist/lib/brushes/presets/drawing/felt-pen.js +3 -0
- package/dist/lib/brushes/presets/drawing/index.d.ts +13 -0
- package/dist/lib/brushes/presets/drawing/index.js +88 -0
- package/dist/lib/brushes/presets/drawing/pencil-2b.js +3 -0
- package/dist/lib/brushes/presets/drawing/pencil-hb.js +3 -0
- package/dist/lib/brushes/presets/painting/grass.js +3 -0
- package/dist/lib/brushes/presets/painting/hair.js +3 -0
- package/dist/lib/brushes/presets/painting/index.d.ts +18 -0
- package/dist/lib/brushes/presets/painting/index.js +118 -0
- package/dist/lib/brushes/presets/painting/splatter.js +3 -0
- package/dist/lib/brushes/presets/painting/textured-paint.js +3 -0
- package/dist/lib/brushes/presets/painting/watery-paint.js +3 -0
- package/dist/lib/brushes/presets/round/index.d.ts +9 -0
- package/dist/lib/brushes/presets/round/index.js +44 -0
- package/dist/lib/brushes/presets/round/round.js +3 -0
- package/dist/lib/brushes/presets/round/soft-round.js +3 -0
- package/dist/lib/brushes/presets/sized/index.d.ts +20 -0
- package/dist/lib/brushes/presets/sized/index.js +30 -0
- package/dist/lib/brushes/presets/sketching/index.d.ts +14 -0
- package/dist/lib/brushes/presets/sketching/index.js +107 -0
- package/dist/lib/brushes/presets/square/index.d.ts +7 -0
- package/dist/lib/brushes/presets/square/index.js +22 -0
- package/dist/lib/brushes/presets/square/square.js +3 -0
- package/dist/lib/brushes/shapeImage/index.d.ts +40 -0
- package/dist/lib/brushes/shapeImage/index.js +61 -0
- package/dist/lib/brushes/types/index.d.ts +243 -0
- package/dist/lib/brushes/types/index.js +20 -0
- package/dist/lib/classes/BackgroundWork/index.d.ts +55 -0
- package/dist/lib/classes/BackgroundWork/index.js +97 -0
- package/dist/lib/classes/BrushSettings/index.d.ts +91 -0
- package/dist/lib/classes/BrushSettings/index.js +152 -0
- package/dist/lib/classes/CustomPresets/index.d.ts +53 -0
- package/dist/lib/classes/CustomPresets/index.js +71 -0
- package/dist/lib/classes/ExternalSheets/index.d.ts +64 -0
- package/dist/lib/classes/ExternalSheets/index.js +112 -0
- package/dist/lib/classes/GestureTracker/index.d.ts +62 -0
- package/dist/lib/classes/GestureTracker/index.js +122 -0
- package/dist/lib/classes/History/index.d.ts +83 -0
- package/dist/lib/classes/History/index.js +126 -0
- package/dist/lib/classes/HistoryGuard/index.d.ts +58 -0
- package/dist/lib/classes/HistoryGuard/index.js +97 -0
- package/dist/lib/classes/InputSettings/index.d.ts +74 -0
- package/dist/lib/classes/InputSettings/index.js +101 -0
- package/dist/lib/classes/KeyboardShortcuts/index.d.ts +48 -0
- package/dist/lib/classes/KeyboardShortcuts/index.js +137 -0
- package/dist/lib/classes/Layer/index.d.ts +86 -0
- package/dist/lib/classes/Layer/index.js +111 -0
- package/dist/lib/classes/LayerAutosave/index.d.ts +180 -0
- package/dist/lib/classes/LayerAutosave/index.js +346 -0
- package/dist/lib/classes/LayerStack/index.d.ts +177 -0
- package/dist/lib/classes/LayerStack/index.js +394 -0
- package/dist/lib/classes/PaintCanvas/index.d.ts +537 -0
- package/dist/lib/classes/PaintCanvas/index.js +965 -0
- package/dist/lib/classes/StrokeLog/index.d.ts +149 -0
- package/dist/lib/classes/StrokeLog/index.js +257 -0
- package/dist/lib/classes/Viewport/index.d.ts +108 -0
- package/dist/lib/classes/Viewport/index.js +194 -0
- package/dist/lib/document/index.d.ts +17 -0
- package/dist/lib/document/index.js +25 -0
- package/dist/lib/document/schema.d.ts +132 -0
- package/dist/lib/document/schema.js +110 -0
- package/dist/lib/i18n/index.d.ts +131 -0
- package/dist/lib/i18n/index.js +277 -0
- package/dist/lib/image/index.d.ts +90 -0
- package/dist/lib/image/index.js +147 -0
- package/dist/lib/messages/PatchSender/index.d.ts +75 -0
- package/dist/lib/messages/PatchSender/index.js +125 -0
- package/dist/lib/messages/index.d.ts +26 -0
- package/dist/lib/messages/index.js +59 -0
- package/dist/lib/messages/schema.d.ts +1775 -0
- package/dist/lib/messages/schema.js +192 -0
- package/dist/lib/presets/index.d.ts +32 -0
- package/dist/lib/presets/index.js +35 -0
- package/dist/lib/presets/schema.d.ts +753 -0
- package/dist/lib/presets/schema.js +181 -0
- package/dist/lib/rendering/BrushTip/index.d.ts +63 -0
- package/dist/lib/rendering/BrushTip/index.js +112 -0
- package/dist/lib/rendering/checkerboard/index.d.ts +22 -0
- package/dist/lib/rendering/checkerboard/index.js +31 -0
- package/dist/lib/rendering/compositing/index.d.ts +64 -0
- package/dist/lib/rendering/compositing/index.js +124 -0
- package/dist/lib/rendering/cursor/index.d.ts +14 -0
- package/dist/lib/rendering/cursor/index.js +39 -0
- package/dist/lib/rendering/cursors/index.d.ts +18 -0
- package/dist/lib/rendering/cursors/index.js +23 -0
- package/dist/lib/rendering/maskOutline/index.d.ts +22 -0
- package/dist/lib/rendering/maskOutline/index.js +78 -0
- package/dist/lib/rendering/outline/index.d.ts +36 -0
- package/dist/lib/rendering/outline/index.js +120 -0
- package/dist/lib/rendering/sample/index.d.ts +55 -0
- package/dist/lib/rendering/sample/index.js +83 -0
- package/dist/lib/rendering/screenshot/index.d.ts +67 -0
- package/dist/lib/rendering/screenshot/index.js +96 -0
- package/dist/lib/rendering/transformBox/index.d.ts +10 -0
- package/dist/lib/rendering/transformBox/index.js +57 -0
- package/dist/lib/selection/Lasso/index.d.ts +70 -0
- package/dist/lib/selection/Lasso/index.js +156 -0
- package/dist/lib/selection/Selection/index.d.ts +86 -0
- package/dist/lib/selection/Selection/index.js +228 -0
- package/dist/lib/selection/geometry/index.d.ts +37 -0
- package/dist/lib/selection/geometry/index.js +57 -0
- package/dist/lib/storage/index.d.ts +63 -0
- package/dist/lib/storage/index.js +114 -0
- package/dist/lib/transform/Transformer/index.d.ts +124 -0
- package/dist/lib/transform/Transformer/index.js +286 -0
- package/dist/lib/transform/geometry/index.d.ts +95 -0
- package/dist/lib/transform/geometry/index.js +258 -0
- package/dist/lib/utils/array/index.d.ts +10 -0
- package/dist/lib/utils/array/index.js +21 -0
- package/dist/lib/utils/gestures/index.d.ts +35 -0
- package/dist/lib/utils/gestures/index.js +81 -0
- package/dist/lib/utils/navigation/index.d.ts +51 -0
- package/dist/lib/utils/navigation/index.js +59 -0
- package/dist/lib/utils/random/index.d.ts +18 -0
- package/dist/lib/utils/random/index.js +27 -0
- package/dist/lib/utils/shortcuts/index.d.ts +74 -0
- package/dist/lib/utils/shortcuts/index.js +189 -0
- package/dist/lib/validation/validators.generated.d.ts +27 -0
- package/dist/lib/validation/validators.generated.js +19826 -0
- package/dist/locales/en-GB.js +181 -0
- package/dist/locales/en-US.js +181 -0
- package/dist/locales/fr.js +181 -0
- package/dist/locales/ja.js +181 -0
- package/dist/locales/ko.js +181 -0
- package/dist/locales/zh-Hans.js +181 -0
- package/dist/locales/zh-Hant.js +181 -0
- package/dist/package.js +3 -0
- package/dist/testing/app.d.ts +7 -0
- package/dist/testing/app.js +16 -0
- package/dist/testing/elements.d.ts +42 -0
- package/dist/testing/elements.js +58 -0
- package/dist/testing/fakeCanvas.d.ts +21 -0
- package/dist/testing/fakeCanvas.js +26 -0
- package/dist/testing/layerStore.d.ts +26 -0
- package/dist/testing/layerStore.js +43 -0
- package/dist/testing/locks.d.ts +15 -0
- package/dist/testing/locks.js +41 -0
- package/dist/testing/recordingCanvas.d.ts +14 -0
- package/dist/testing/recordingCanvas.js +28 -0
- package/dist/testing/recordingContext.d.ts +10 -0
- package/dist/testing/recordingContext.js +19 -0
- package/dist/ui/ChangeController/index.d.ts +40 -0
- package/dist/ui/ChangeController/index.js +62 -0
- package/dist/ui/base/index.d.ts +69 -0
- package/dist/ui/base/index.js +94 -0
- package/dist/ui/elements/PaintCanvas/index.d.ts +249 -0
- package/dist/ui/elements/PaintCanvas/index.js +584 -0
- package/dist/ui/elements/PaintCanvas/style.js +3 -0
- package/dist/ui/elements/PcBrushConfig/index.d.ts +130 -0
- package/dist/ui/elements/PcBrushConfig/index.js +465 -0
- package/dist/ui/elements/PcBrushConfig/style.js +3 -0
- package/dist/ui/elements/PcBrushMenu/index.d.ts +121 -0
- package/dist/ui/elements/PcBrushMenu/index.js +362 -0
- package/dist/ui/elements/PcBrushMenu/style.js +3 -0
- package/dist/ui/elements/PcBrushTest/index.d.ts +71 -0
- package/dist/ui/elements/PcBrushTest/index.js +136 -0
- package/dist/ui/elements/PcBrushTest/style.js +3 -0
- package/dist/ui/elements/PcButton/index.d.ts +57 -0
- package/dist/ui/elements/PcButton/index.js +124 -0
- package/dist/ui/elements/PcButtonGroup/index.d.ts +44 -0
- package/dist/ui/elements/PcButtonGroup/index.js +93 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +356 -0
- package/dist/ui/elements/PcCanvas/index.js +1004 -0
- package/dist/ui/elements/PcCanvas/style.js +3 -0
- package/dist/ui/elements/PcCanvasMap/index.d.ts +96 -0
- package/dist/ui/elements/PcCanvasMap/index.js +187 -0
- package/dist/ui/elements/PcCanvasMap/style.js +3 -0
- package/dist/ui/elements/PcContextMenu/index.d.ts +72 -0
- package/dist/ui/elements/PcContextMenu/index.js +148 -0
- package/dist/ui/elements/PcDialog/index.d.ts +63 -0
- package/dist/ui/elements/PcDialog/index.js +114 -0
- package/dist/ui/elements/PcIcon/index.d.ts +36 -0
- package/dist/ui/elements/PcIcon/index.js +43 -0
- package/dist/ui/elements/PcIconToggle/index.d.ts +46 -0
- package/dist/ui/elements/PcIconToggle/index.js +87 -0
- package/dist/ui/elements/PcInfoTip/index.d.ts +40 -0
- package/dist/ui/elements/PcInfoTip/index.js +95 -0
- package/dist/ui/elements/PcInputSettings/index.d.ts +69 -0
- package/dist/ui/elements/PcInputSettings/index.js +154 -0
- package/dist/ui/elements/PcLayerItem/index.d.ts +221 -0
- package/dist/ui/elements/PcLayerItem/index.js +561 -0
- package/dist/ui/elements/PcLayerItem/style.js +3 -0
- package/dist/ui/elements/PcLayersMenu/index.d.ts +108 -0
- package/dist/ui/elements/PcLayersMenu/index.js +208 -0
- package/dist/ui/elements/PcLayersMenu/style.js +3 -0
- package/dist/ui/elements/PcMatch/index.d.ts +31 -0
- package/dist/ui/elements/PcMatch/index.js +58 -0
- package/dist/ui/elements/PcPanel/index.d.ts +43 -0
- package/dist/ui/elements/PcPanel/index.js +51 -0
- package/dist/ui/elements/PcPopover/index.d.ts +48 -0
- package/dist/ui/elements/PcPopover/index.js +86 -0
- package/dist/ui/elements/PcSlider/index.d.ts +57 -0
- package/dist/ui/elements/PcSlider/index.js +88 -0
- package/dist/ui/elements/PcStats/index.d.ts +54 -0
- package/dist/ui/elements/PcStats/index.js +144 -0
- package/dist/ui/elements/PcThemePicker/index.d.ts +56 -0
- package/dist/ui/elements/PcThemePicker/index.js +134 -0
- package/dist/ui/elements/PcToast/index.d.ts +58 -0
- package/dist/ui/elements/PcToast/index.js +110 -0
- package/dist/ui/elements/PcToggleCheckbox/index.d.ts +37 -0
- package/dist/ui/elements/PcToggleCheckbox/index.js +105 -0
- package/dist/ui/elements/PcToolSettings/index.d.ts +128 -0
- package/dist/ui/elements/PcToolSettings/index.js +549 -0
- package/dist/ui/elements/PcToolSettings/style.js +3 -0
- package/dist/ui/elements/PcToolbar/index.d.ts +71 -0
- package/dist/ui/elements/PcToolbar/index.js +190 -0
- package/dist/ui/elements/PcToolbar/style.js +3 -0
- package/dist/ui/icons/images/brush.js +3 -0
- package/dist/ui/icons/images/circle-mask.js +3 -0
- package/dist/ui/icons/images/clipboard.js +3 -0
- package/dist/ui/icons/images/cursor.js +3 -0
- package/dist/ui/icons/images/eraser.js +3 -0
- package/dist/ui/icons/images/eyedropper.js +3 -0
- package/dist/ui/icons/images/flip-horizontal.js +3 -0
- package/dist/ui/icons/images/flip-vertical.js +3 -0
- package/dist/ui/icons/images/focus-pen.js +3 -0
- package/dist/ui/icons/images/freehand-mask.js +3 -0
- package/dist/ui/icons/images/hidden.js +3 -0
- package/dist/ui/icons/images/layers.js +3 -0
- package/dist/ui/icons/images/linked.js +3 -0
- package/dist/ui/icons/images/locked.js +3 -0
- package/dist/ui/icons/images/moon.js +3 -0
- package/dist/ui/icons/images/pan.js +3 -0
- package/dist/ui/icons/images/polygon-mask.js +3 -0
- package/dist/ui/icons/images/pressure.js +3 -0
- package/dist/ui/icons/images/randomise.js +3 -0
- package/dist/ui/icons/images/rectangle-mask.js +3 -0
- package/dist/ui/icons/images/redo.js +3 -0
- package/dist/ui/icons/images/rotate-90-left.js +3 -0
- package/dist/ui/icons/images/rotate-90-right.js +3 -0
- package/dist/ui/icons/images/smoothing-bezier-cubic.js +3 -0
- package/dist/ui/icons/images/smoothing-bezier-quad.js +3 -0
- package/dist/ui/icons/images/smoothing-linear.js +3 -0
- package/dist/ui/icons/images/star.js +3 -0
- package/dist/ui/icons/images/sun.js +3 -0
- package/dist/ui/icons/images/tilt.js +3 -0
- package/dist/ui/icons/images/undo.js +3 -0
- package/dist/ui/icons/images/unlinked.js +3 -0
- package/dist/ui/icons/images/unlocked.js +3 -0
- package/dist/ui/icons/images/visible.js +3 -0
- package/dist/ui/icons/images/zoom-in.js +3 -0
- package/dist/ui/icons/images/zoom-out.js +3 -0
- package/dist/ui/icons/images/zoom.js +3 -0
- package/dist/ui/icons/index.d.ts +42 -0
- package/dist/ui/icons/index.js +130 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/index.js +22 -0
- package/dist/ui/layout/index.d.ts +88 -0
- package/dist/ui/layout/index.js +96 -0
- package/dist/ui/styles/index.d.ts +13 -0
- package/dist/ui/styles/index.js +195 -0
- package/dist/ui/tokens/index.d.ts +29 -0
- package/dist/ui/tokens/index.js +114 -0
- package/locales/en-GB.json +179 -0
- package/locales/en-US.json +179 -0
- package/locales/fr.json +179 -0
- package/locales/ja.json +179 -0
- package/locales/ko.json +179 -0
- package/locales/zh-Hans.json +179 -0
- package/locales/zh-Hant.json +179 -0
- package/package.json +97 -3
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
import StrokeLog from "../StrokeLog/index.js";
|
|
2
|
+
import BackgroundWork from "../BackgroundWork/index.js";
|
|
3
|
+
import BrushSettings from "../BrushSettings/index.js";
|
|
4
|
+
import LayerStack from "../LayerStack/index.js";
|
|
5
|
+
import Viewport from "../Viewport/index.js";
|
|
6
|
+
import StrokeEngine, { BEFORE_COMMIT_EVENT, STROKE_BEGIN_EVENT, STROKE_END_EVENT } from "../../brushes/StrokeEngine/index.js";
|
|
7
|
+
import BrushTipCache from "../../brushes/BrushTipCache/index.js";
|
|
8
|
+
import Selection from "../../selection/Selection/index.js";
|
|
9
|
+
import Lasso from "../../selection/Lasso/index.js";
|
|
10
|
+
import Transformer, { applyMask, contentBounds, copyArea, drawTransformed } from "../../transform/Transformer/index.js";
|
|
11
|
+
import { boxBounds } from "../../transform/geometry/index.js";
|
|
12
|
+
import Layer, { createOffscreenCanvas } from "../Layer/index.js";
|
|
13
|
+
import LayerAutosave from "../LayerAutosave/index.js";
|
|
14
|
+
import { brushPresets, eraserPresets } from "../../brushes/index.js";
|
|
15
|
+
import { BUILT_IN_AUTHOR, UNKNOWN_AUTHOR, authorHandle } from "../../brushes/authors/index.js";
|
|
16
|
+
import { BRUSH_PRESET_FILE_FORMAT } from "../../presets/schema.js";
|
|
17
|
+
import CustomPresets from "../CustomPresets/index.js";
|
|
18
|
+
import InputSettings from "../InputSettings/index.js";
|
|
19
|
+
import ExternalSheets from "../ExternalSheets/index.js";
|
|
20
|
+
import PatchSender from "../../messages/PatchSender/index.js";
|
|
21
|
+
import { createPatchMessage, validateCanvasMessage } from "../../messages/index.js";
|
|
22
|
+
import { REDO_EVENT, UNDO_EVENT } from "../History/index.js";
|
|
23
|
+
import { CanvasMessageType } from "../../messages/schema.js";
|
|
24
|
+
import { ImageEncoding, base64ToBytes, boundingBoxSize, bytesToBase64, decodeRgbaDeflate, encodeRgbaDeflate } from "../../image/index.js";
|
|
25
|
+
import { drawLayers } from "../../rendering/compositing/index.js";
|
|
26
|
+
import { DOCUMENT_FORMAT } from "../../document/schema.js";
|
|
27
|
+
import { isValidDocument } from "../../document/index.js";
|
|
28
|
+
import { averageColour, flattenArea, pixelRect, readPixels } from "../../rendering/sample/index.js";
|
|
29
|
+
// src/lib/classes/PaintCanvas/index.ts
|
|
30
|
+
/** Drawing and navigation tools */
|
|
31
|
+
var Tool = /* @__PURE__ */ function(Tool) {
|
|
32
|
+
Tool["Brush"] = "brush";
|
|
33
|
+
Tool["Eraser"] = "eraser";
|
|
34
|
+
/** Move, scale and rotate */
|
|
35
|
+
Tool["Move"] = "move";
|
|
36
|
+
Tool["RectangleLasso"] = "rectangle-lasso";
|
|
37
|
+
Tool["CircleLasso"] = "circle-lasso";
|
|
38
|
+
Tool["PolygonLasso"] = "polygon-lasso";
|
|
39
|
+
Tool["FreeformLasso"] = "freeform-lasso";
|
|
40
|
+
Tool["Pan"] = "pan";
|
|
41
|
+
Tool["Zoom"] = "zoom";
|
|
42
|
+
/** Set the current colour from the canvas (eyedropper) */
|
|
43
|
+
Tool["ColourSample"] = "colour-sample";
|
|
44
|
+
return Tool;
|
|
45
|
+
}({});
|
|
46
|
+
/** Toggleable panels */
|
|
47
|
+
var Panel = /* @__PURE__ */ function(Panel) {
|
|
48
|
+
Panel["Brush"] = "brush";
|
|
49
|
+
Panel["Eraser"] = "eraser";
|
|
50
|
+
Panel["Layers"] = "layers";
|
|
51
|
+
/** The implementer's own app menu */
|
|
52
|
+
Panel["Menu"] = "menu";
|
|
53
|
+
return Panel;
|
|
54
|
+
}({});
|
|
55
|
+
/** Panels opened by tools */
|
|
56
|
+
var TOOL_PANELS = {
|
|
57
|
+
["brush"]: "brush",
|
|
58
|
+
["eraser"]: "eraser"
|
|
59
|
+
};
|
|
60
|
+
/** Canvas width and height when none is given */
|
|
61
|
+
var DEFAULT_CANVAS_SIZE = 2e3;
|
|
62
|
+
/** Largest canvas width or height allowed when the `maxCanvasSize` option isn't given */
|
|
63
|
+
var MAX_CANVAS_SIZE = 2e3;
|
|
64
|
+
/** Drawing colour to start with */
|
|
65
|
+
var DEFAULT_COLOUR = "#000000";
|
|
66
|
+
/** Owner of the custom presets when the `ownerId` option isn't given */
|
|
67
|
+
var DEFAULT_OWNER_ID = "local";
|
|
68
|
+
/** Author name of presets made with `newPreset` when the `presetAuthor` option isn't given */
|
|
69
|
+
var DEFAULT_AUTHOR = UNKNOWN_AUTHOR;
|
|
70
|
+
/** PaintCanvas's own names */
|
|
71
|
+
var DEFAULT_NAMES = {
|
|
72
|
+
app: BUILT_IN_AUTHOR,
|
|
73
|
+
filePrefix: "paintcanvas",
|
|
74
|
+
presetFileFormat: BRUSH_PRESET_FILE_FORMAT,
|
|
75
|
+
unknownAuthor: UNKNOWN_AUTHOR
|
|
76
|
+
};
|
|
77
|
+
/** Pattern for colour input values */
|
|
78
|
+
var COLOUR_PATTERN = /^#[0-9a-f]{6}$/i;
|
|
79
|
+
/**
|
|
80
|
+
* A canvas's pixels as a PNG data URL.
|
|
81
|
+
*/
|
|
82
|
+
var toPngDataUrl = async (canvas) => {
|
|
83
|
+
canvas.getContext("2d");
|
|
84
|
+
const blob = await canvas.convertToBlob({ type: "image/png" });
|
|
85
|
+
return `data:image/png;base64,${bytesToBase64(new Uint8Array(await blob.arrayBuffer()))}`;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Decode a PNG data URL into an image.
|
|
89
|
+
*/
|
|
90
|
+
var fromPngDataUrl = (dataUrl) => createImageBitmap(new Blob([base64ToBytes(dataUrl.slice(dataUrl.indexOf(",") + 1))], { type: "image/png" }));
|
|
91
|
+
/**
|
|
92
|
+
* Decode a patch into pixels.
|
|
93
|
+
*/
|
|
94
|
+
var decodePatch = async (patch) => {
|
|
95
|
+
if (patch.encoding === ImageEncoding.Png) return createImageBitmap(new Blob([base64ToBytes(patch.data)], { type: "image/png" }));
|
|
96
|
+
const { width, height } = boundingBoxSize(patch.coords);
|
|
97
|
+
return new ImageData(await decodeRgbaDeflate(patch.data, patch.coords), width, height);
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The painting engine: canvas size, layers, brush and eraser settings, custom presets, the selected tool, colour,
|
|
101
|
+
* selection, transforms, clipboard, undo/redo history, viewport and input settings. Where its data is kept and who it
|
|
102
|
+
* is shared with are up to the implementer (see `onMessage` and `receive`).
|
|
103
|
+
*
|
|
104
|
+
* Dispatches a `change` event whenever its own state changes. Layers, brushes and selection dispatch their own `change`
|
|
105
|
+
* events. Also dispatches `pointer` when the pointer moves over the canvas.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* const canvas = new PaintCanvas({ width: 1000, height: 800 });
|
|
109
|
+
* canvas.selectTool(Tool.Brush, { fromToolbar: true });
|
|
110
|
+
* canvas.setColour("#ff0000");
|
|
111
|
+
*/
|
|
112
|
+
var PaintCanvas = class extends EventTarget {
|
|
113
|
+
/** Brush settings */
|
|
114
|
+
brush = new BrushSettings(brushPresets);
|
|
115
|
+
/** Eraser settings */
|
|
116
|
+
eraser = new BrushSettings(eraserPresets);
|
|
117
|
+
/** The user's own and imported brush presets, listed with the brush and eraser presets */
|
|
118
|
+
customPresets;
|
|
119
|
+
/** Lasso shape in progress */
|
|
120
|
+
lasso = new Lasso();
|
|
121
|
+
/** Brush shapes, loaded per preset */
|
|
122
|
+
tips = new BrushTipCache();
|
|
123
|
+
/** Log of strokes started, ended, cancelled and ignored, with timings, for debugging */
|
|
124
|
+
strokeLog = new StrokeLog();
|
|
125
|
+
/** Whether the debug log shows (see `showDebugLog`) */
|
|
126
|
+
debugLogShown = false;
|
|
127
|
+
/** Slow work that waits for pauses in drawing (syncing, the canvas map and layer thumbnails, saving) */
|
|
128
|
+
background = new BackgroundWork(this.strokeLog);
|
|
129
|
+
/** How the user's input devices control the canvas (scrolling, pinching, touch gestures, focus pen mode) */
|
|
130
|
+
input = new InputSettings();
|
|
131
|
+
/** The user's pointer position over the canvas, in canvas pixels (undefined when off the canvas) */
|
|
132
|
+
pointer;
|
|
133
|
+
/** Selected tool */
|
|
134
|
+
tool = "brush";
|
|
135
|
+
/** Tool used temporarily while a key is held (e.g. Space for pan), overriding the selected tool */
|
|
136
|
+
heldTool = null;
|
|
137
|
+
/** Open panel, if any */
|
|
138
|
+
panel = null;
|
|
139
|
+
/** Whether the interface (toolbar, panels and anything the implementer adds) is visible ("tab" key) */
|
|
140
|
+
interfaceVisible = true;
|
|
141
|
+
/**
|
|
142
|
+
* Whether scaling the Move tool's box keeps its proportions ("Constrain proportions"), from its settings bar and when
|
|
143
|
+
* dragging a handle. Off by default; while drag-scaling, holding Shift turns it on (when off) and Alt turns it off
|
|
144
|
+
* (when on), until released.
|
|
145
|
+
*/
|
|
146
|
+
constrainProportions = false;
|
|
147
|
+
/** How the colour sample tool samples ("point" by default) */
|
|
148
|
+
sampleMode = "point";
|
|
149
|
+
canvasWidth;
|
|
150
|
+
canvasHeight;
|
|
151
|
+
layerStack;
|
|
152
|
+
view;
|
|
153
|
+
strokeEngine;
|
|
154
|
+
currentSelection;
|
|
155
|
+
transformer;
|
|
156
|
+
onLayersOrMaskChange = () => {
|
|
157
|
+
const transformer = this.transformer;
|
|
158
|
+
if (!transformer.pending || transformer.layer !== this.layerStack.active) this.syncTransform();
|
|
159
|
+
};
|
|
160
|
+
onStrokeBegin = () => this.background.strokeStarted();
|
|
161
|
+
onStrokeEnd = () => this.background.strokeEnded();
|
|
162
|
+
onStrokeCommit = (event) => {
|
|
163
|
+
const { layer, area, stroke } = event.detail;
|
|
164
|
+
this.layerStack.recordImage(layer.id, area);
|
|
165
|
+
if (!this.sendingStrokes || this.replaying) return;
|
|
166
|
+
this.emit({
|
|
167
|
+
protocol: 1,
|
|
168
|
+
type: CanvasMessageType.Stroke,
|
|
169
|
+
step: this.history.latestStep ?? crypto.randomUUID(),
|
|
170
|
+
layer: layer.id,
|
|
171
|
+
...stroke
|
|
172
|
+
}).catch(() => void 0);
|
|
173
|
+
};
|
|
174
|
+
onHistoryStep = (event) => {
|
|
175
|
+
if (!this.isSending) return;
|
|
176
|
+
this.emit({
|
|
177
|
+
protocol: 1,
|
|
178
|
+
type: event.type === "undo" ? CanvasMessageType.Undo : CanvasMessageType.Redo,
|
|
179
|
+
step: event.detail.step
|
|
180
|
+
}).catch(() => void 0);
|
|
181
|
+
};
|
|
182
|
+
/** Whether drawing messages are sent (see `startSending`) */
|
|
183
|
+
isSending = false;
|
|
184
|
+
/** Whether finished strokes are sent (see `startSending`) */
|
|
185
|
+
sendingStrokes = false;
|
|
186
|
+
/** Whether a stroke from elsewhere is being drawn (it isn't sent again) */
|
|
187
|
+
replaying = false;
|
|
188
|
+
currentColour = DEFAULT_COLOUR;
|
|
189
|
+
/** Image data copied with cut or copy, and the area it was copied from */
|
|
190
|
+
clipboard;
|
|
191
|
+
/** Drawings from elsewhere */
|
|
192
|
+
externalSheets;
|
|
193
|
+
/** Who receives the drawing messages this canvas sends */
|
|
194
|
+
messageListeners = /* @__PURE__ */ new Set();
|
|
195
|
+
/** Sends patches while sending (see `startSending`) */
|
|
196
|
+
sender;
|
|
197
|
+
/** The layer stack whose changes are being sent */
|
|
198
|
+
sendingLayers;
|
|
199
|
+
onLayersChangeToSend = () => this.sender?.schedule();
|
|
200
|
+
/** Creates the canvases the app draws on (fake ones in tests) */
|
|
201
|
+
createCanvas;
|
|
202
|
+
/** Who the custom presets belong to (passed to the preset store) */
|
|
203
|
+
ownerId;
|
|
204
|
+
/** The largest canvas width or height allowed */
|
|
205
|
+
maxCanvasSize;
|
|
206
|
+
/** Most layers the user can have */
|
|
207
|
+
maxLayers;
|
|
208
|
+
/** Names users see and saved files are called by (see `PaintCanvasNames`) */
|
|
209
|
+
names;
|
|
210
|
+
/** Gives the author of presets made with `newPreset` */
|
|
211
|
+
presetAuthor;
|
|
212
|
+
/** Keeps the user's layers in a store, while one is used */
|
|
213
|
+
layerAutosave;
|
|
214
|
+
constructor(options = {}) {
|
|
215
|
+
super();
|
|
216
|
+
this.strokeLog.enabled = false;
|
|
217
|
+
this.ownerId = options.ownerId ?? "local";
|
|
218
|
+
this.maxCanvasSize = options.maxCanvasSize ?? 2e3;
|
|
219
|
+
this.maxLayers = options.maxLayers ?? 5;
|
|
220
|
+
this.names = {
|
|
221
|
+
...DEFAULT_NAMES,
|
|
222
|
+
...options.names
|
|
223
|
+
};
|
|
224
|
+
this.presetAuthor = options.presetAuthor ?? (() => ({ name: DEFAULT_AUTHOR }));
|
|
225
|
+
this.tips.preload(this.brush.presets);
|
|
226
|
+
this.tips.preload(this.eraser.presets);
|
|
227
|
+
this.customPresets = new CustomPresets(this.ownerId, options.presetStore);
|
|
228
|
+
this.customPresets.addEventListener("change", () => this.listCustomPresets());
|
|
229
|
+
this.customPresets.load().catch(() => void 0);
|
|
230
|
+
this.createCanvas = options.createCanvas ?? createOffscreenCanvas;
|
|
231
|
+
this.canvasWidth = DEFAULT_CANVAS_SIZE;
|
|
232
|
+
this.canvasHeight = DEFAULT_CANVAS_SIZE;
|
|
233
|
+
this.layerStack = new LayerStack(this.canvasWidth, this.canvasHeight, this.createCanvas, this.maxLayers);
|
|
234
|
+
this.view = new Viewport(this.canvasWidth, this.canvasHeight);
|
|
235
|
+
this.strokeEngine = new StrokeEngine(this.canvasWidth, this.canvasHeight, this.createCanvas);
|
|
236
|
+
this.currentSelection = new Selection(this.canvasWidth, this.canvasHeight, this.createCanvas);
|
|
237
|
+
this.transformer = new Transformer(this.canvasWidth, this.canvasHeight, this.createCanvas);
|
|
238
|
+
this.externalSheets = new ExternalSheets(this.canvasWidth, this.canvasHeight, this.createCanvas);
|
|
239
|
+
this.watchLayersAndMask();
|
|
240
|
+
if (options.width !== void 0 || options.height !== void 0) this.setCanvasSize(options.width ?? this.canvasWidth, options.height ?? this.canvasHeight);
|
|
241
|
+
if (options.layerStore) this.useLayerStore(options.layerStore);
|
|
242
|
+
}
|
|
243
|
+
/** Canvas width in pixels */
|
|
244
|
+
get width() {
|
|
245
|
+
return this.canvasWidth;
|
|
246
|
+
}
|
|
247
|
+
/** Canvas height in pixels */
|
|
248
|
+
get height() {
|
|
249
|
+
return this.canvasHeight;
|
|
250
|
+
}
|
|
251
|
+
/** Drawings from elsewhere (e.g. other people's), drawn under the user's own layers (see `receive`) */
|
|
252
|
+
get external() {
|
|
253
|
+
return this.externalSheets;
|
|
254
|
+
}
|
|
255
|
+
/** Everything drawn on the canvas, top first: the user's own layers, then drawings from elsewhere */
|
|
256
|
+
get drawables() {
|
|
257
|
+
return [...this.layerStack.layers, ...this.externalSheets.sheets];
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Listen for the drawing messages this canvas sends, e.g. to pass them to other people. Returns a function that stops
|
|
261
|
+
* listening.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* const stop = canvas.onMessage((message) => transport.send(message));
|
|
265
|
+
* canvas.startSending();
|
|
266
|
+
*/
|
|
267
|
+
onMessage(listener) {
|
|
268
|
+
this.messageListeners.add(listener);
|
|
269
|
+
return () => this.messageListeners.delete(listener);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Give a message to every listener. Rejects if any listener's delivery fails.
|
|
273
|
+
*/
|
|
274
|
+
async emit(message) {
|
|
275
|
+
await Promise.all([...this.messageListeners].map((listener) => listener(message)));
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Start sending drawing messages to `onMessage` listeners:
|
|
279
|
+
* - patches (by default): shortly after the user's layers change (and once drawing pauses), the area of their drawing
|
|
280
|
+
* that changed
|
|
281
|
+
* - strokes (when asked): each finished brush or eraser stroke
|
|
282
|
+
* - undo and redo: the step undone or redone
|
|
283
|
+
*
|
|
284
|
+
* Changing the canvas size stops sending.
|
|
285
|
+
*/
|
|
286
|
+
startSending(options = {}) {
|
|
287
|
+
this.stopSending();
|
|
288
|
+
this.isSending = true;
|
|
289
|
+
this.sendingStrokes = options.strokes ?? false;
|
|
290
|
+
if (options.patches === false) return;
|
|
291
|
+
this.sender = new PatchSender({
|
|
292
|
+
width: this.canvasWidth,
|
|
293
|
+
height: this.canvasHeight,
|
|
294
|
+
layers: () => this.layerStack.layers,
|
|
295
|
+
createCanvas: this.createCanvas,
|
|
296
|
+
delayMs: options.delayMs,
|
|
297
|
+
whenIdle: (run) => this.background.run("sync", run),
|
|
298
|
+
send: (message) => this.emit(message)
|
|
299
|
+
});
|
|
300
|
+
this.sendingLayers = this.layerStack;
|
|
301
|
+
this.layerStack.addEventListener("change", this.onLayersChangeToSend);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Stop sending patches. Changes waiting to be sent are dropped.
|
|
305
|
+
*/
|
|
306
|
+
stopSending() {
|
|
307
|
+
this.isSending = false;
|
|
308
|
+
this.sendingStrokes = false;
|
|
309
|
+
this.sender?.stop();
|
|
310
|
+
this.sender = void 0;
|
|
311
|
+
this.sendingLayers?.removeEventListener("change", this.onLayersChangeToSend);
|
|
312
|
+
this.sendingLayers = void 0;
|
|
313
|
+
}
|
|
314
|
+
/** Whether drawing messages are being sent (see `startSending`) */
|
|
315
|
+
get sending() {
|
|
316
|
+
return this.isSending;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Send a patch of what changed now instead of waiting. Resolves once sent (or straight away when not sending
|
|
320
|
+
* patches).
|
|
321
|
+
*/
|
|
322
|
+
async sendNow() {
|
|
323
|
+
await this.sender?.sync();
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Count pixels of the user's drawing as already sent, so they aren't sent again (e.g. their earlier work, restored
|
|
327
|
+
* from a shared drawing's history).
|
|
328
|
+
*/
|
|
329
|
+
markSent(area, rgba) {
|
|
330
|
+
this.sender?.markSent(area, rgba);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* A patch of the user's whole drawing (all their layers, flattened), e.g. for someone who joins late. Undefined if
|
|
334
|
+
* nothing is drawn.
|
|
335
|
+
*/
|
|
336
|
+
async snapshot() {
|
|
337
|
+
const { canvasWidth: width, canvasHeight: height } = this;
|
|
338
|
+
const context = this.createCanvas(width, height).getContext("2d", { willReadFrequently: true });
|
|
339
|
+
drawLayers(context, this.layerStack.layers);
|
|
340
|
+
const rgba = context.getImageData(0, 0, width, height).data;
|
|
341
|
+
if (!rgba.some((value, i) => i % 4 === 3 && value > 0)) return void 0;
|
|
342
|
+
const coords = [
|
|
343
|
+
0,
|
|
344
|
+
0,
|
|
345
|
+
width,
|
|
346
|
+
height
|
|
347
|
+
];
|
|
348
|
+
return createPatchMessage(coords, ImageEncoding.RgbaDeflate, await encodeRgbaDeflate(rgba, coords));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Apply a drawing message received from a source (an ID the implementer chooses, e.g. a user ID): a patch is drawn
|
|
352
|
+
* onto that source's sheet. Messages are checked first; invalid ones are ignored. Resolves to whether it was applied.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* transport.onMessage((from, message) => canvas.receive(from, message));
|
|
356
|
+
*/
|
|
357
|
+
async receive(source, message) {
|
|
358
|
+
const result = validateCanvasMessage(message);
|
|
359
|
+
if (!result.valid) return false;
|
|
360
|
+
const valid = result.value;
|
|
361
|
+
if (valid.type === CanvasMessageType.Patch) {
|
|
362
|
+
await this.externalSheets.apply(source, valid);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (valid.type === CanvasMessageType.Stroke) {
|
|
366
|
+
const { width, height } = this;
|
|
367
|
+
await this.externalSheets.paint(source, (canvas) => {
|
|
368
|
+
const sheetLayer = new Layer({ name: source }, width, height, () => canvas);
|
|
369
|
+
this.replay(new StrokeEngine(width, height, this.createCanvas), sheetLayer, valid);
|
|
370
|
+
});
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
this.dispatchEvent(new CustomEvent("history", { detail: {
|
|
374
|
+
source,
|
|
375
|
+
message: valid
|
|
376
|
+
} }));
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The drawing as a document: its size, the user's layers and drawings from elsewhere, with PNG images (see
|
|
381
|
+
* `PaintCanvasDocument`). Turn it into JSON to save it as a file.
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* const json = JSON.stringify(await canvas.exportDocument());
|
|
385
|
+
*/
|
|
386
|
+
async exportDocument() {
|
|
387
|
+
const layers = this.layerStack.layers;
|
|
388
|
+
return {
|
|
389
|
+
format: DOCUMENT_FORMAT,
|
|
390
|
+
version: 1,
|
|
391
|
+
width: this.canvasWidth,
|
|
392
|
+
height: this.canvasHeight,
|
|
393
|
+
layers: await Promise.all(layers.map(async (layer) => {
|
|
394
|
+
const { name, visible, opacity, blendMode, alphaLock } = layer.toConfig();
|
|
395
|
+
return {
|
|
396
|
+
name,
|
|
397
|
+
visible,
|
|
398
|
+
opacity,
|
|
399
|
+
blendMode,
|
|
400
|
+
alphaLock,
|
|
401
|
+
image: await toPngDataUrl(layer.canvas)
|
|
402
|
+
};
|
|
403
|
+
})),
|
|
404
|
+
activeLayer: layers.indexOf(this.layerStack.active),
|
|
405
|
+
external: await Promise.all(this.externalSheets.sheets.map(async ({ source, canvas }) => ({
|
|
406
|
+
source,
|
|
407
|
+
image: await toPngDataUrl(canvas)
|
|
408
|
+
})))
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Replace the drawing with a document (e.g. read from a file): its size, layers and drawings from elsewhere. Undo
|
|
413
|
+
* history starts again. Returns whether it was loaded: not when the document is invalid or an image can't be
|
|
414
|
+
* decoded, and then nothing changes.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* await canvas.importDocument(JSON.parse(text));
|
|
418
|
+
*/
|
|
419
|
+
async importDocument(document) {
|
|
420
|
+
const result = isValidDocument(document);
|
|
421
|
+
if (!result.valid || result.value.layers.length > this.maxLayers) return false;
|
|
422
|
+
const doc = result.value;
|
|
423
|
+
const images = await Promise.all([...doc.layers.map((layer) => fromPngDataUrl(layer.image)), ...doc.external.map((sheet) => fromPngDataUrl(sheet.image))]).catch(() => void 0);
|
|
424
|
+
if (!images) return false;
|
|
425
|
+
const configs = doc.layers.map(({ name, visible, opacity, blendMode, alphaLock }) => ({
|
|
426
|
+
id: crypto.randomUUID(),
|
|
427
|
+
name,
|
|
428
|
+
visible,
|
|
429
|
+
opacity,
|
|
430
|
+
blendMode,
|
|
431
|
+
alphaLock
|
|
432
|
+
}));
|
|
433
|
+
this.restoreLayers(doc.width, doc.height, configs, images, configs[doc.activeLayer].id);
|
|
434
|
+
doc.external.forEach((sheet, i) => this.externalSheets.restore(sheet.source, images[configs.length + i]));
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Replace the canvas size and the user's layers (top first, each with its image) and select one by ID. Undo history
|
|
439
|
+
* starts again. Throws if the size or number of layers isn't allowed.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* canvas.restoreLayers(800, 600, [{ id: "a", name: "Sketch" }], [sketchImage], "a");
|
|
443
|
+
*/
|
|
444
|
+
restoreLayers(width, height, layers, images, activeLayer) {
|
|
445
|
+
if (layers.length > this.maxLayers) throw new Error(`Cannot have more than ${this.maxLayers} layers`);
|
|
446
|
+
this.setCanvasSize(width, height);
|
|
447
|
+
this.layerStack.replace(layers).forEach((layer, i) => layer.context.drawImage(images[i], 0, 0));
|
|
448
|
+
this.layerStack.select(activeLayer);
|
|
449
|
+
this.history.clear();
|
|
450
|
+
}
|
|
451
|
+
/** Keeps the user's layers in a store, while one is used (see `useLayerStore`) */
|
|
452
|
+
get autosave() {
|
|
453
|
+
return this.layerAutosave;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Keep the user's layers in a store (or stop, without one), so the drawing survives reloading the page: the saved
|
|
457
|
+
* drawing is restored, then changes are saved. Only one window draws on a drawing at a time (see `LayerAutosave`).
|
|
458
|
+
* Returns the autosave.
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* canvas.useLayerStore(new IndexedDbLayerStore("my-drawing"));
|
|
462
|
+
*/
|
|
463
|
+
useLayerStore(store, options) {
|
|
464
|
+
this.layerAutosave?.stop();
|
|
465
|
+
this.layerAutosave = store ? new LayerAutosave(this, store, options) : void 0;
|
|
466
|
+
this.layerAutosave?.start();
|
|
467
|
+
this.notify();
|
|
468
|
+
return this.layerAutosave;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* How much the canvas holds: its size, layers, drawings from elsewhere, memory and undo steps.
|
|
472
|
+
*/
|
|
473
|
+
stats() {
|
|
474
|
+
const sheets = this.layerStack.layers.length + this.externalSheets.sheets.length;
|
|
475
|
+
return {
|
|
476
|
+
width: this.canvasWidth,
|
|
477
|
+
height: this.canvasHeight,
|
|
478
|
+
layers: this.layerStack.layers.length,
|
|
479
|
+
externalSheets: this.externalSheets.sheets.length,
|
|
480
|
+
memoryBytes: this.canvasWidth * this.canvasHeight * 4 * sheets,
|
|
481
|
+
historySteps: this.history.size,
|
|
482
|
+
historyLimit: this.history.limit
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Draw a recorded stroke (e.g. received from elsewhere) onto one of the user's layers, as an action they can undo.
|
|
487
|
+
* It isn't sent as a stroke message. Does nothing during a stroke, or if there's no such layer. Returns whether it was
|
|
488
|
+
* drawn.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* canvas.drawStroke(canvas.layers.active.id, strokeMessage);
|
|
492
|
+
*/
|
|
493
|
+
drawStroke(layerId, stroke) {
|
|
494
|
+
const layer = this.layerStack.layers.find((existing) => existing.id === layerId);
|
|
495
|
+
if (!layer || this.strokeEngine.drawing || stroke.points.length === 0) return false;
|
|
496
|
+
this.replaying = true;
|
|
497
|
+
try {
|
|
498
|
+
this.replay(this.strokeEngine, layer, stroke);
|
|
499
|
+
} finally {
|
|
500
|
+
this.replaying = false;
|
|
501
|
+
}
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Draw a recorded stroke with a stroke engine, point by point.
|
|
506
|
+
*/
|
|
507
|
+
replay(engine, layer, stroke) {
|
|
508
|
+
const [first, ...rest] = stroke.points;
|
|
509
|
+
engine.begin({
|
|
510
|
+
layer,
|
|
511
|
+
mode: stroke.mode,
|
|
512
|
+
preset: stroke.preset,
|
|
513
|
+
size: stroke.size,
|
|
514
|
+
colour: stroke.colour,
|
|
515
|
+
tip: this.tips.get(stroke.preset),
|
|
516
|
+
isPen: stroke.isPen,
|
|
517
|
+
seed: stroke.seed
|
|
518
|
+
}, first, first.time);
|
|
519
|
+
const last = rest.pop() ?? first;
|
|
520
|
+
for (const point of rest) engine.move(point, point.time);
|
|
521
|
+
engine.end(last, last.time);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Put a patch's pixels onto one of the user's layers (replacing what was there), as an action they can undo. Returns
|
|
525
|
+
* whether it was applied: not for an invalid patch, or if there's no such layer.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* await canvas.applyPatch(canvas.layers.active.id, patch);
|
|
529
|
+
*/
|
|
530
|
+
async applyPatch(layerId, patch) {
|
|
531
|
+
const result = validateCanvasMessage(patch);
|
|
532
|
+
if (!result.valid || result.value.type !== CanvasMessageType.Patch) return false;
|
|
533
|
+
const valid = result.value;
|
|
534
|
+
const image = await decodePatch(valid).catch(() => void 0);
|
|
535
|
+
const layer = this.layerStack.layers.find((existing) => existing.id === layerId);
|
|
536
|
+
if (!image || !layer) return false;
|
|
537
|
+
const [x, y] = valid.coords;
|
|
538
|
+
const { width, height } = boundingBoxSize(valid.coords);
|
|
539
|
+
this.layerStack.changeImage(layer.id, {
|
|
540
|
+
x,
|
|
541
|
+
y,
|
|
542
|
+
width,
|
|
543
|
+
height
|
|
544
|
+
}, (ctx) => {
|
|
545
|
+
ctx.clearRect(x, y, width, height);
|
|
546
|
+
if (image instanceof ImageData) ctx.putImageData(image, x, y);
|
|
547
|
+
else ctx.drawImage(image, x, y);
|
|
548
|
+
});
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The colour shown at a canvas point (every layer mixed as shown, over the white background), or undefined outside the
|
|
553
|
+
* canvas.
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* app.sampleColour({ x: 10.4, y: 20.9 }); // "#ff0000"
|
|
557
|
+
*/
|
|
558
|
+
sampleColour(point) {
|
|
559
|
+
const area = pixelRect(point, point, this.width, this.height);
|
|
560
|
+
if (!area) return void 0;
|
|
561
|
+
return averageColour(readPixels(this.flatten(area)));
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* The average colour shown in the rectangle between two canvas points (cut to the canvas), counting every pixel, or
|
|
565
|
+
* undefined outside the canvas. With `maxPixels`, it's worked out from the area scaled down to at most that many
|
|
566
|
+
* pixels across, which is quicker (for a preview while dragging).
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* app.sampleAreaColour({ x: 0, y: 0 }, { x: 99, y: 99 }); // the average of 100 × 100 pixels
|
|
570
|
+
*/
|
|
571
|
+
sampleAreaColour(a, b, maxPixels = Number.POSITIVE_INFINITY) {
|
|
572
|
+
const area = pixelRect(a, b, this.width, this.height);
|
|
573
|
+
if (!area) return void 0;
|
|
574
|
+
const scale = Math.min(1, maxPixels / Math.max(area.width, area.height));
|
|
575
|
+
return averageColour(readPixels(this.flatten(area, Math.max(1, Math.round(area.width * scale)), Math.max(1, Math.round(area.height * scale)))));
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* The `SAMPLE_PIXELS` × `SAMPLE_PIXELS` canvas pixels around a point as shown (the point's pixel is at the middle,
|
|
579
|
+
* rounding down), for the colour sample tool's enlarged view. Pixels outside the canvas are transparent.
|
|
580
|
+
*/
|
|
581
|
+
samplePixels(point) {
|
|
582
|
+
const half = 5;
|
|
583
|
+
return this.flatten({
|
|
584
|
+
x: Math.floor(point.x) - half,
|
|
585
|
+
y: Math.floor(point.y) - half,
|
|
586
|
+
width: 10,
|
|
587
|
+
height: 10
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** Draw an area of the canvas as shown onto a new canvas (see `flattenArea`) */
|
|
591
|
+
flatten(area, width, height) {
|
|
592
|
+
return flattenArea(this.drawables, this.createCanvas, this.width, this.height, area, width, height);
|
|
593
|
+
}
|
|
594
|
+
/** The user's view of the canvas (zoom and position) */
|
|
595
|
+
get viewport() {
|
|
596
|
+
return this.view;
|
|
597
|
+
}
|
|
598
|
+
/** Whether the debug log shows */
|
|
599
|
+
get debugLog() {
|
|
600
|
+
return this.debugLogShown;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Show or hide the debug log. Strokes and timings are only recorded while it shows.
|
|
604
|
+
*/
|
|
605
|
+
showDebugLog(shown) {
|
|
606
|
+
this.debugLogShown = shown;
|
|
607
|
+
this.strokeLog.enabled = shown;
|
|
608
|
+
this.notify();
|
|
609
|
+
}
|
|
610
|
+
/** Draws brush and eraser strokes */
|
|
611
|
+
get strokes() {
|
|
612
|
+
return this.strokeEngine;
|
|
613
|
+
}
|
|
614
|
+
/** The mask limiting where drawing applies */
|
|
615
|
+
get selection() {
|
|
616
|
+
return this.currentSelection;
|
|
617
|
+
}
|
|
618
|
+
/** Moves, scales and rotates the active layer's pixels with the Move tool */
|
|
619
|
+
get transform() {
|
|
620
|
+
return this.transformer;
|
|
621
|
+
}
|
|
622
|
+
/** The user's undoable actions */
|
|
623
|
+
get history() {
|
|
624
|
+
return this.layerStack.history;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Listen for layer and mask changes on the current layer stack and selection, and for strokes starting and ending
|
|
628
|
+
* (background work waits for pauses in drawing).
|
|
629
|
+
*/
|
|
630
|
+
watchLayersAndMask() {
|
|
631
|
+
this.layerStack.addEventListener("change", this.onLayersOrMaskChange);
|
|
632
|
+
this.currentSelection.addEventListener("change", this.onLayersOrMaskChange);
|
|
633
|
+
this.strokeEngine.addEventListener(BEFORE_COMMIT_EVENT, this.onStrokeCommit);
|
|
634
|
+
this.strokeEngine.addEventListener(STROKE_BEGIN_EVENT, this.onStrokeBegin);
|
|
635
|
+
this.strokeEngine.addEventListener(STROKE_END_EVENT, this.onStrokeEnd);
|
|
636
|
+
this.history.addEventListener(UNDO_EVENT, this.onHistoryStep);
|
|
637
|
+
this.history.addEventListener(REDO_EVENT, this.onHistoryStep);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Show the transform box when the Move tool is selected, and apply any transform when switching away or to another layer.
|
|
641
|
+
*/
|
|
642
|
+
syncTransform() {
|
|
643
|
+
const transformer = this.transformer;
|
|
644
|
+
if (this.tool !== "move") {
|
|
645
|
+
if (transformer.pending) this.commitTransform();
|
|
646
|
+
else if (transformer.active) {
|
|
647
|
+
transformer.reset();
|
|
648
|
+
transformer.dispatchEvent(new Event("change"));
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (transformer.pending && transformer.layer === this.layerStack.active) return;
|
|
653
|
+
if (transformer.pending) this.commitTransform();
|
|
654
|
+
transformer.prepare(this.layerStack.active, this.currentSelection.mask);
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Write the transformed pixels into the layer (Enter, or switching tool). The mask moves with them.
|
|
658
|
+
*/
|
|
659
|
+
commitTransform() {
|
|
660
|
+
const layer = this.transformer.layer;
|
|
661
|
+
const result = this.transformer.commit();
|
|
662
|
+
if (!layer || !result) return;
|
|
663
|
+
const { maskPiece, source, box, area, before } = result;
|
|
664
|
+
this.layerStack.recordImage(layer.id, area, before);
|
|
665
|
+
if (maskPiece) this.currentSelection.moveArea(maskPiece, source, boxBounds(box), (ctx) => drawTransformed(ctx, maskPiece, box));
|
|
666
|
+
this.layerStack.markChanged(layer.id);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Undo the user's latest action. A move or rotate that hasn't been applied is cancelled instead, and nothing happens
|
|
670
|
+
* during a stroke.
|
|
671
|
+
*/
|
|
672
|
+
undo() {
|
|
673
|
+
if (this.canStepHistory()) this.history.undo();
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Redo the user's latest undone action. A move or rotate that hasn't been applied is cancelled instead, and nothing
|
|
677
|
+
* happens during a stroke.
|
|
678
|
+
*/
|
|
679
|
+
redo() {
|
|
680
|
+
if (this.canStepHistory()) this.history.redo();
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Whether undo or redo can move along the history now: not during a stroke, and not while a transform is pending (which
|
|
684
|
+
* is cancelled instead).
|
|
685
|
+
*/
|
|
686
|
+
canStepHistory() {
|
|
687
|
+
if (this.strokeEngine.drawing) return false;
|
|
688
|
+
if (!this.transformer.pending) return true;
|
|
689
|
+
this.cancelTransform();
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Put transformed pixels back where they were (Escape).
|
|
694
|
+
*/
|
|
695
|
+
cancelTransform() {
|
|
696
|
+
const pasting = this.transformer.pasting;
|
|
697
|
+
this.transformer.cancel();
|
|
698
|
+
if (pasting) this.syncTransform();
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Copy the masked image data of the selected layer to the clipboard. Needs a mask with image data inside it; does
|
|
702
|
+
* nothing during a stroke or while a transform is pending. Returns the area copied, if any.
|
|
703
|
+
*/
|
|
704
|
+
copy() {
|
|
705
|
+
const area = this.maskedArea();
|
|
706
|
+
const mask = this.currentSelection.mask;
|
|
707
|
+
if (!area || !mask) return void 0;
|
|
708
|
+
const layer = this.layerStack.active;
|
|
709
|
+
const image = copyArea(this.createCanvas, layer.canvas, area);
|
|
710
|
+
applyMask(image, copyArea(this.createCanvas, mask, area));
|
|
711
|
+
this.clipboard = {
|
|
712
|
+
image,
|
|
713
|
+
area
|
|
714
|
+
};
|
|
715
|
+
return area;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Copy the masked image data of the selected layer to the clipboard, then erase it from the layer as one undoable
|
|
719
|
+
* action.
|
|
720
|
+
*/
|
|
721
|
+
cut() {
|
|
722
|
+
const area = this.copy();
|
|
723
|
+
if (area) this.eraseMasked(area);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Erase the masked image data from the selected layer as one undoable action (Delete or Backspace). Needs a mask with
|
|
727
|
+
* image data inside it; does nothing during a stroke or while a transform is pending.
|
|
728
|
+
*/
|
|
729
|
+
deleteMasked() {
|
|
730
|
+
const area = this.maskedArea();
|
|
731
|
+
if (area) this.eraseMasked(area);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* The bounds of the selected layer's image data inside the mask, if there is a mask with image data inside it. None
|
|
735
|
+
* during a stroke or while a transform is pending.
|
|
736
|
+
*/
|
|
737
|
+
maskedArea() {
|
|
738
|
+
const mask = this.currentSelection.mask;
|
|
739
|
+
if (!mask || this.strokeEngine.drawing || this.transformer.pending) return void 0;
|
|
740
|
+
const { width, height } = this;
|
|
741
|
+
return contentBounds(this.layerStack.active.context.getImageData(0, 0, width, height).data, width, height, mask.getContext("2d").getImageData(0, 0, width, height).data);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Erase the mask's part of an area from the selected layer, as one undoable action.
|
|
745
|
+
*/
|
|
746
|
+
eraseMasked(area) {
|
|
747
|
+
const mask = this.currentSelection.mask;
|
|
748
|
+
const maskPiece = copyArea(this.createCanvas, mask, area);
|
|
749
|
+
this.layerStack.changeImage(this.layerStack.active.id, area, (ctx) => {
|
|
750
|
+
ctx.save();
|
|
751
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
752
|
+
ctx.drawImage(maskPiece, area.x, area.y);
|
|
753
|
+
ctx.restore();
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Show the clipboard's image data over the selected layer where it was copied from, and select the Move tool to place,
|
|
758
|
+
* scale and rotate it. It is written into the layer (and sent) on Enter or when switching to another tool; Escape
|
|
759
|
+
* discards it. A pending transform is applied first.
|
|
760
|
+
*/
|
|
761
|
+
paste() {
|
|
762
|
+
const clipboard = this.clipboard;
|
|
763
|
+
if (!clipboard || this.strokeEngine.drawing) return;
|
|
764
|
+
if (this.transformer.pending) this.commitTransform();
|
|
765
|
+
this.selectTool("move");
|
|
766
|
+
this.transformer.float(this.layerStack.active, clipboard.image, clipboard.area);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Apply a finished lasso action to the mask: a shape is added, subtracted or replaces it; a plain click deselects.
|
|
770
|
+
*/
|
|
771
|
+
applyLasso(result) {
|
|
772
|
+
if (!result) return;
|
|
773
|
+
if ("deselect" in result) this.currentSelection.clear();
|
|
774
|
+
else this.currentSelection.apply(result.shape, result.mode);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Whether a preset comes with the app (it can't be deleted, replaced or exported).
|
|
778
|
+
*/
|
|
779
|
+
isBuiltInPreset(id) {
|
|
780
|
+
return Object.hasOwn(brushPresets, id);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* The handle of the author of presets the user makes (from the `presetAuthor` option), or "" if they have none. Their
|
|
784
|
+
* own presets are the ones with this handle (see `isOwnAuthor`).
|
|
785
|
+
*/
|
|
786
|
+
get authorHandle() {
|
|
787
|
+
return authorHandle(this.presetAuthor()) ?? "";
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Make a new custom preset from settings: a new ID, the given name, and the author from the `presetAuthor` option.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* app.newPreset(app.brush.preset, "My brush"); // { ...settings, id: "0b5c…", name: "My brush", author: { name, handle } }
|
|
794
|
+
*/
|
|
795
|
+
newPreset(settings, name) {
|
|
796
|
+
const copy = structuredClone(settings);
|
|
797
|
+
delete copy.group;
|
|
798
|
+
return {
|
|
799
|
+
...copy,
|
|
800
|
+
id: crypto.randomUUID(),
|
|
801
|
+
name: name.trim(),
|
|
802
|
+
version: "1.0.0",
|
|
803
|
+
author: this.presetAuthor()
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* List the custom presets after the built-in ones, for the brush and the eraser.
|
|
808
|
+
*/
|
|
809
|
+
listCustomPresets() {
|
|
810
|
+
const custom = Object.fromEntries(this.customPresets.presets.map((preset) => [preset.id, preset]));
|
|
811
|
+
this.brush.setPresets({
|
|
812
|
+
...brushPresets,
|
|
813
|
+
...custom
|
|
814
|
+
});
|
|
815
|
+
this.eraser.setPresets({
|
|
816
|
+
...eraserPresets,
|
|
817
|
+
...custom
|
|
818
|
+
});
|
|
819
|
+
this.tips.preload(custom);
|
|
820
|
+
}
|
|
821
|
+
/** The tool currently in effect: the held tool if a key is held, otherwise the selected tool */
|
|
822
|
+
get activeTool() {
|
|
823
|
+
return this.heldTool ?? this.tool;
|
|
824
|
+
}
|
|
825
|
+
/** The user's layers */
|
|
826
|
+
get layers() {
|
|
827
|
+
return this.layerStack;
|
|
828
|
+
}
|
|
829
|
+
/** Current drawing colour as a hex string, e.g. `#000000` */
|
|
830
|
+
get colour() {
|
|
831
|
+
return this.currentColour;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Set the drawing colour. Must be a 6-digit hex colour, e.g. `#ff0000`.
|
|
835
|
+
*/
|
|
836
|
+
setColour(colour) {
|
|
837
|
+
if (!COLOUR_PATTERN.test(colour)) throw new Error(`Invalid colour ${colour}`);
|
|
838
|
+
this.currentColour = colour.toLowerCase();
|
|
839
|
+
this.notify();
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Change the canvas size, from 1 to the maximum canvas size (`maxCanvasSize` option). Replaces all layers.
|
|
843
|
+
*/
|
|
844
|
+
setCanvasSize(width, height) {
|
|
845
|
+
if (!this.isValidCanvasDimension(width) || !this.isValidCanvasDimension(height)) throw new Error(`Canvas size must be whole numbers from 1 to ${this.maxCanvasSize}`);
|
|
846
|
+
this.canvasWidth = width;
|
|
847
|
+
this.canvasHeight = height;
|
|
848
|
+
this.history.removeEventListener(UNDO_EVENT, this.onHistoryStep);
|
|
849
|
+
this.history.removeEventListener(REDO_EVENT, this.onHistoryStep);
|
|
850
|
+
this.layerStack.removeEventListener("change", this.onLayersOrMaskChange);
|
|
851
|
+
this.currentSelection.removeEventListener("change", this.onLayersOrMaskChange);
|
|
852
|
+
this.strokeEngine.removeEventListener(BEFORE_COMMIT_EVENT, this.onStrokeCommit);
|
|
853
|
+
this.strokeEngine.removeEventListener(STROKE_BEGIN_EVENT, this.onStrokeBegin);
|
|
854
|
+
this.strokeEngine.removeEventListener(STROKE_END_EVENT, this.onStrokeEnd);
|
|
855
|
+
this.layerStack = new LayerStack(width, height, this.createCanvas, this.maxLayers);
|
|
856
|
+
this.view = new Viewport(width, height);
|
|
857
|
+
this.strokeEngine = new StrokeEngine(width, height, this.createCanvas);
|
|
858
|
+
this.currentSelection = new Selection(width, height, this.createCanvas);
|
|
859
|
+
this.transformer = new Transformer(width, height, this.createCanvas);
|
|
860
|
+
this.stopSending();
|
|
861
|
+
this.externalSheets.resize(width, height);
|
|
862
|
+
this.canvasResized(width, height);
|
|
863
|
+
this.clipboard = void 0;
|
|
864
|
+
this.watchLayersAndMask();
|
|
865
|
+
this.notify();
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Called when the canvas size changes, before `change` is dispatched. Subclasses resize anything of their own here.
|
|
869
|
+
*/
|
|
870
|
+
canvasResized(width, height) {}
|
|
871
|
+
/**
|
|
872
|
+
* Check a canvas dimension is a whole number from 1 to the maximum canvas size.
|
|
873
|
+
*/
|
|
874
|
+
isValidCanvasDimension(value) {
|
|
875
|
+
return Number.isInteger(value) && value >= 1 && value <= this.maxCanvasSize;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Set whether scaling the Move tool's box keeps its proportions.
|
|
879
|
+
*/
|
|
880
|
+
setConstrainProportions(constrain) {
|
|
881
|
+
this.constrainProportions = constrain;
|
|
882
|
+
this.notify();
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Set how the colour sample tool samples.
|
|
886
|
+
*/
|
|
887
|
+
setSampleMode(mode) {
|
|
888
|
+
this.sampleMode = mode;
|
|
889
|
+
this.notify();
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Set where the user's pointer is over the canvas (undefined when it leaves). Dispatches `pointer` when it moves.
|
|
893
|
+
*/
|
|
894
|
+
setPointer(position) {
|
|
895
|
+
if (position?.x === this.pointer?.x && position?.y === this.pointer?.y) return;
|
|
896
|
+
this.pointer = position ? {
|
|
897
|
+
x: position.x,
|
|
898
|
+
y: position.y
|
|
899
|
+
} : void 0;
|
|
900
|
+
this.dispatchEvent(new Event("pointer"));
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Select a tool.
|
|
904
|
+
*
|
|
905
|
+
* - Clicking a toolbar button (`fromToolbar: true`) only selects the tool: its presets open from the settings bar's
|
|
906
|
+
* preset button.
|
|
907
|
+
* - Pressing a tool's shortcut selects the tool, or toggles its panel if the tool is already selected.
|
|
908
|
+
*
|
|
909
|
+
* Switching tools closes the previous tool's panel (the layers panel and app menu stay open).
|
|
910
|
+
*/
|
|
911
|
+
selectTool(tool, options = {}) {
|
|
912
|
+
const toolPanel = TOOL_PANELS[tool];
|
|
913
|
+
const alreadySelected = this.tool === tool;
|
|
914
|
+
this.tool = tool;
|
|
915
|
+
if (toolPanel && alreadySelected && !options.fromToolbar) this.panel = this.panel === toolPanel ? null : toolPanel;
|
|
916
|
+
else if (!alreadySelected && this.panel !== "layers" && this.panel !== "menu") this.panel = null;
|
|
917
|
+
this.syncTransform();
|
|
918
|
+
this.notify();
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Use a tool temporarily while a key is held (e.g. Space for pan). The selected tool is unchanged.
|
|
922
|
+
*/
|
|
923
|
+
holdTool(tool) {
|
|
924
|
+
if (this.heldTool === tool) return;
|
|
925
|
+
this.heldTool = tool;
|
|
926
|
+
this.notify();
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Stop using the held tool and return to the selected tool.
|
|
930
|
+
*/
|
|
931
|
+
releaseHeldTool() {
|
|
932
|
+
if (this.heldTool === null) return;
|
|
933
|
+
this.heldTool = null;
|
|
934
|
+
this.notify();
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Close the open panel, if any.
|
|
938
|
+
*/
|
|
939
|
+
closePanel() {
|
|
940
|
+
if (this.panel === null) return;
|
|
941
|
+
this.panel = null;
|
|
942
|
+
this.notify();
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Open a panel, or close it if already open.
|
|
946
|
+
*/
|
|
947
|
+
togglePanel(panel) {
|
|
948
|
+
this.panel = this.panel === panel ? null : panel;
|
|
949
|
+
this.notify();
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Show or hide the interface.
|
|
953
|
+
*/
|
|
954
|
+
toggleInterface() {
|
|
955
|
+
this.interfaceVisible = !this.interfaceVisible;
|
|
956
|
+
this.notify();
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Dispatch a `change` event.
|
|
960
|
+
*/
|
|
961
|
+
notify() {
|
|
962
|
+
this.dispatchEvent(new Event("change"));
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
export { DEFAULT_AUTHOR, DEFAULT_CANVAS_SIZE, DEFAULT_COLOUR, DEFAULT_NAMES, DEFAULT_OWNER_ID, MAX_CANVAS_SIZE, Panel, TOOL_PANELS, Tool, PaintCanvas as default };
|