@lvl99dotcom/paintcanvas 0.0.0-stage → 0.2.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 +94 -0
- package/dist/lib/i18n/index.js +182 -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 +188 -0
- package/dist/locales/en-US.js +188 -0
- package/dist/locales/fr.js +188 -0
- package/dist/locales/ja.js +188 -0
- package/dist/locales/ko.js +188 -0
- package/dist/locales/zh-Hans.js +188 -0
- package/dist/locales/zh-Hant.js +188 -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 +463 -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 +150 -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 +141 -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 +543 -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 +186 -0
- package/locales/en-US.json +186 -0
- package/locales/fr.json +186 -0
- package/locales/ja.json +186 -0
- package/locales/ko.json +186 -0
- package/locales/zh-Hans.json +186 -0
- package/locales/zh-Hant.json +186 -0
- package/package.json +97 -3
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Point } from "../../classes/Viewport";
|
|
2
|
+
import type { MaskShape } from "../Selection";
|
|
3
|
+
import { type MaskMode } from "../geometry";
|
|
4
|
+
/** The lasso tools */
|
|
5
|
+
export type LassoKind = "rectangle" | "circle" | "polygon" | "freeform";
|
|
6
|
+
/** Modifier keys held during a lasso action */
|
|
7
|
+
export interface LassoKeys {
|
|
8
|
+
shiftKey: boolean;
|
|
9
|
+
altKey: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** What a finished lasso action does to the mask */
|
|
12
|
+
export type LassoResult = {
|
|
13
|
+
shape: MaskShape;
|
|
14
|
+
mode: MaskMode;
|
|
15
|
+
} | {
|
|
16
|
+
deselect: true;
|
|
17
|
+
} | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Lasso tool behaviour, independent of the screen: builds rectangle, circle, polygon and freeform mask shapes from pointer input.
|
|
20
|
+
*
|
|
21
|
+
* - The keys held when a shape starts choose the mode: Shift adds, Alt subtracts, otherwise it replaces.
|
|
22
|
+
* - Rectangle/circle: drag; Shift while dragging keeps sides equal. A click without dragging deselects (in replace mode).
|
|
23
|
+
* - Polygon: click to add nodes; Shift snaps the next line to 45°; clicking near the first node, double-clicking or `close()` finishes it.
|
|
24
|
+
* - Freeform: drag; releasing finishes it.
|
|
25
|
+
* - `cancel()` (Escape) abandons the shape in progress.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* lasso.down("rectangle", { x: 10, y: 10 }, keys, false);
|
|
29
|
+
* lasso.move({ x: 50, y: 30 }, keys);
|
|
30
|
+
* const result = lasso.up({ x: 50, y: 30 }); // { shape: { type: "rect", ... }, mode: "replace" }
|
|
31
|
+
*/
|
|
32
|
+
export default class Lasso extends EventTarget {
|
|
33
|
+
private drawing?;
|
|
34
|
+
/** Whether a shape is in progress */
|
|
35
|
+
get active(): boolean;
|
|
36
|
+
/** The shape in progress, for drawing its outline: rectangles/ellipses, or the polygon/freeform path including the pointer */
|
|
37
|
+
get preview(): MaskShape | undefined;
|
|
38
|
+
/** Polygon nodes placed so far (for drawing handles) */
|
|
39
|
+
get nodes(): readonly Point[];
|
|
40
|
+
/**
|
|
41
|
+
* Pointer pressed.
|
|
42
|
+
*
|
|
43
|
+
* @param nearFirstNode - whether the pointer is within closing distance of the polygon's first node (on screen)
|
|
44
|
+
*/
|
|
45
|
+
down(kind: LassoKind, point: Point, keys: LassoKeys, nearFirstNode: boolean): LassoResult;
|
|
46
|
+
/**
|
|
47
|
+
* Pointer moved (with or without the button held).
|
|
48
|
+
*/
|
|
49
|
+
move(point: Point, keys: LassoKeys): void;
|
|
50
|
+
/**
|
|
51
|
+
* Pointer released: finishes rectangle, circle and freeform shapes. Polygons continue until closed.
|
|
52
|
+
*/
|
|
53
|
+
up(point: Point, keys?: LassoKeys): LassoResult;
|
|
54
|
+
/**
|
|
55
|
+
* Finish a polygon or freeform shape (double-click, Enter, or clicking the first node). Shapes with fewer than three points are abandoned.
|
|
56
|
+
*/
|
|
57
|
+
close(): LassoResult;
|
|
58
|
+
/**
|
|
59
|
+
* Abandon the shape in progress (Escape).
|
|
60
|
+
*/
|
|
61
|
+
cancel(): void;
|
|
62
|
+
/**
|
|
63
|
+
* Turn a finished rectangle or circle into a shape, or undefined if it has no size.
|
|
64
|
+
*/
|
|
65
|
+
private shapeFrom;
|
|
66
|
+
/**
|
|
67
|
+
* Dispatch a `change` event (so the outline in progress redraws).
|
|
68
|
+
*/
|
|
69
|
+
private notify;
|
|
70
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { isNear, maskModeFor, rectFromCorners, snapAngle } from "../geometry/index.js";
|
|
2
|
+
// src/lib/selection/Lasso/index.ts
|
|
3
|
+
/** Smallest freeform distance between recorded points, in canvas pixels */
|
|
4
|
+
var FREEFORM_STEP = 1;
|
|
5
|
+
/**
|
|
6
|
+
* Lasso tool behaviour, independent of the screen: builds rectangle, circle, polygon and freeform mask shapes from pointer input.
|
|
7
|
+
*
|
|
8
|
+
* - The keys held when a shape starts choose the mode: Shift adds, Alt subtracts, otherwise it replaces.
|
|
9
|
+
* - Rectangle/circle: drag; Shift while dragging keeps sides equal. A click without dragging deselects (in replace mode).
|
|
10
|
+
* - Polygon: click to add nodes; Shift snaps the next line to 45°; clicking near the first node, double-clicking or `close()` finishes it.
|
|
11
|
+
* - Freeform: drag; releasing finishes it.
|
|
12
|
+
* - `cancel()` (Escape) abandons the shape in progress.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* lasso.down("rectangle", { x: 10, y: 10 }, keys, false);
|
|
16
|
+
* lasso.move({ x: 50, y: 30 }, keys);
|
|
17
|
+
* const result = lasso.up({ x: 50, y: 30 }); // { shape: { type: "rect", ... }, mode: "replace" }
|
|
18
|
+
*/
|
|
19
|
+
var Lasso = class extends EventTarget {
|
|
20
|
+
drawing;
|
|
21
|
+
/** Whether a shape is in progress */
|
|
22
|
+
get active() {
|
|
23
|
+
return this.drawing !== void 0;
|
|
24
|
+
}
|
|
25
|
+
/** The shape in progress, for drawing its outline: rectangles/ellipses, or the polygon/freeform path including the pointer */
|
|
26
|
+
get preview() {
|
|
27
|
+
const d = this.drawing;
|
|
28
|
+
if (!d) return void 0;
|
|
29
|
+
switch (d.kind) {
|
|
30
|
+
case "rectangle": return {
|
|
31
|
+
type: "rect",
|
|
32
|
+
rect: rectFromCorners(d.start, d.current, d.constrain)
|
|
33
|
+
};
|
|
34
|
+
case "circle": return {
|
|
35
|
+
type: "ellipse",
|
|
36
|
+
rect: rectFromCorners(d.start, d.current, d.constrain)
|
|
37
|
+
};
|
|
38
|
+
case "polygon": return {
|
|
39
|
+
type: "polygon",
|
|
40
|
+
points: [...d.points, d.current]
|
|
41
|
+
};
|
|
42
|
+
default: return {
|
|
43
|
+
type: "polygon",
|
|
44
|
+
points: d.points
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Polygon nodes placed so far (for drawing handles) */
|
|
49
|
+
get nodes() {
|
|
50
|
+
return this.drawing?.kind === "polygon" ? this.drawing.points : [];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Pointer pressed.
|
|
54
|
+
*
|
|
55
|
+
* @param nearFirstNode - whether the pointer is within closing distance of the polygon's first node (on screen)
|
|
56
|
+
*/
|
|
57
|
+
down(kind, point, keys, nearFirstNode) {
|
|
58
|
+
const d = this.drawing;
|
|
59
|
+
if (d?.kind === "polygon" && kind === "polygon") {
|
|
60
|
+
if (nearFirstNode && d.points.length >= 3) return this.close();
|
|
61
|
+
d.points.push(d.current);
|
|
62
|
+
this.notify();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.drawing = {
|
|
66
|
+
kind,
|
|
67
|
+
mode: maskModeFor(keys),
|
|
68
|
+
start: point,
|
|
69
|
+
points: [point],
|
|
70
|
+
current: point,
|
|
71
|
+
constrain: false
|
|
72
|
+
};
|
|
73
|
+
this.notify();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pointer moved (with or without the button held).
|
|
77
|
+
*/
|
|
78
|
+
move(point, keys) {
|
|
79
|
+
const d = this.drawing;
|
|
80
|
+
if (!d) return;
|
|
81
|
+
if (d.kind === "polygon") d.current = keys.shiftKey ? snapAngle(d.points.at(-1), point) : point;
|
|
82
|
+
else {
|
|
83
|
+
d.current = point;
|
|
84
|
+
d.constrain = keys.shiftKey;
|
|
85
|
+
const last = d.points.at(-1);
|
|
86
|
+
if (d.kind === "freeform" && !isNear(last, point, FREEFORM_STEP)) d.points.push(point);
|
|
87
|
+
}
|
|
88
|
+
this.notify();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Pointer released: finishes rectangle, circle and freeform shapes. Polygons continue until closed.
|
|
92
|
+
*/
|
|
93
|
+
up(point, keys = {
|
|
94
|
+
shiftKey: false,
|
|
95
|
+
altKey: false
|
|
96
|
+
}) {
|
|
97
|
+
const d = this.drawing;
|
|
98
|
+
if (!d || d.kind === "polygon") return void 0;
|
|
99
|
+
this.move(point, keys);
|
|
100
|
+
if (d.kind === "freeform") return this.close();
|
|
101
|
+
this.drawing = void 0;
|
|
102
|
+
this.notify();
|
|
103
|
+
const shape = this.shapeFrom(d);
|
|
104
|
+
if (shape) return {
|
|
105
|
+
shape,
|
|
106
|
+
mode: d.mode
|
|
107
|
+
};
|
|
108
|
+
return d.mode === "replace" ? { deselect: true } : void 0;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Finish a polygon or freeform shape (double-click, Enter, or clicking the first node). Shapes with fewer than three points are abandoned.
|
|
112
|
+
*/
|
|
113
|
+
close() {
|
|
114
|
+
const d = this.drawing;
|
|
115
|
+
if (!d) return void 0;
|
|
116
|
+
this.drawing = void 0;
|
|
117
|
+
this.notify();
|
|
118
|
+
const points = d.points.filter((p, i) => i === 0 || !isNear(p, d.points[i - 1], .5));
|
|
119
|
+
return points.length >= 3 ? {
|
|
120
|
+
shape: {
|
|
121
|
+
type: "polygon",
|
|
122
|
+
points
|
|
123
|
+
},
|
|
124
|
+
mode: d.mode
|
|
125
|
+
} : void 0;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Abandon the shape in progress (Escape).
|
|
129
|
+
*/
|
|
130
|
+
cancel() {
|
|
131
|
+
if (!this.drawing) return;
|
|
132
|
+
this.drawing = void 0;
|
|
133
|
+
this.notify();
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Turn a finished rectangle or circle into a shape, or undefined if it has no size.
|
|
137
|
+
*/
|
|
138
|
+
shapeFrom(d) {
|
|
139
|
+
const rect = rectFromCorners(d.start, d.current, d.constrain);
|
|
140
|
+
if (rect.width < 1 || rect.height < 1) return void 0;
|
|
141
|
+
return d.kind === "circle" ? {
|
|
142
|
+
type: "ellipse",
|
|
143
|
+
rect
|
|
144
|
+
} : {
|
|
145
|
+
type: "rect",
|
|
146
|
+
rect
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Dispatch a `change` event (so the outline in progress redraws).
|
|
151
|
+
*/
|
|
152
|
+
notify() {
|
|
153
|
+
this.dispatchEvent(new Event("change"));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export { Lasso as default };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type Layer from "../../classes/Layer";
|
|
2
|
+
import type { Point, Rect } from "../../classes/Viewport";
|
|
3
|
+
import { type CanvasFactory } from "../../classes/Layer";
|
|
4
|
+
import { type OutlineLoop } from "../../rendering/outline";
|
|
5
|
+
import type { MaskMode } from "../geometry";
|
|
6
|
+
/** A shape drawn with a lasso tool, in canvas pixels */
|
|
7
|
+
export type MaskShape = {
|
|
8
|
+
type: "rect";
|
|
9
|
+
rect: Rect;
|
|
10
|
+
} | {
|
|
11
|
+
type: "ellipse";
|
|
12
|
+
rect: Rect;
|
|
13
|
+
} | {
|
|
14
|
+
type: "polygon";
|
|
15
|
+
points: Point[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Bounding box of a shape.
|
|
19
|
+
*/
|
|
20
|
+
export declare const shapeBounds: (shape: MaskShape) => Rect;
|
|
21
|
+
/**
|
|
22
|
+
* The current mask: a canvas-sized greyscale area (stored as alpha) that limits where drawing applies. Only ever local.
|
|
23
|
+
*
|
|
24
|
+
* Shapes and layer alpha replace, add to or subtract from it. After each change the mask is measured once to find whether it is
|
|
25
|
+
* empty and to trace its outline (for the animated "crawling ants").
|
|
26
|
+
*
|
|
27
|
+
* Dispatches a `change` event whenever the mask changes.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* selection.apply({ type: "rect", rect: { x: 10, y: 10, width: 100, height: 50 } }, "replace");
|
|
31
|
+
* selection.active; // true
|
|
32
|
+
* selection.outline; // closed loops in canvas pixels
|
|
33
|
+
*/
|
|
34
|
+
export default class Selection extends EventTarget {
|
|
35
|
+
/** Canvas width in pixels */
|
|
36
|
+
readonly width: number;
|
|
37
|
+
/** Canvas height in pixels */
|
|
38
|
+
readonly height: number;
|
|
39
|
+
/** Whether a non-empty mask exists */
|
|
40
|
+
active: boolean;
|
|
41
|
+
/** Outline of the mask as closed loops in canvas pixels */
|
|
42
|
+
outline: OutlineLoop[];
|
|
43
|
+
private readonly createCanvas;
|
|
44
|
+
private maskCanvas?;
|
|
45
|
+
/** Area that may contain masked pixels */
|
|
46
|
+
private bounds?;
|
|
47
|
+
constructor(width: number, height: number, createCanvas?: CanvasFactory);
|
|
48
|
+
/** The mask canvas (alpha = mask value) while a mask is active, for limiting drawing */
|
|
49
|
+
get mask(): OffscreenCanvas | undefined;
|
|
50
|
+
/** The mask canvas's drawing context (creating the canvas on first use) */
|
|
51
|
+
private get context();
|
|
52
|
+
/**
|
|
53
|
+
* Replace, add to or subtract from the mask with a lasso shape.
|
|
54
|
+
*/
|
|
55
|
+
apply(shape: MaskShape, mode: MaskMode): void;
|
|
56
|
+
/**
|
|
57
|
+
* Move part of the mask along with transformed pixels: removes the piece from where it was and draws it at its new place.
|
|
58
|
+
*
|
|
59
|
+
* @param draw - draws the piece in its new place (e.g. `drawTransformed`)
|
|
60
|
+
*/
|
|
61
|
+
moveArea(piece: OffscreenCanvas, from: Rect, to: Rect, draw: (ctx: OffscreenCanvasRenderingContext2D) => void): void;
|
|
62
|
+
/**
|
|
63
|
+
* Mask the whole canvas ("cmd + a").
|
|
64
|
+
*/
|
|
65
|
+
selectAll(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Invert the mask: masked parts become unmasked and the rest becomes masked. Without a mask, everything is masked.
|
|
68
|
+
*/
|
|
69
|
+
invert(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Mask a layer's visible pixels, following their alpha (Cmd/Ctrl + click a layer's name).
|
|
72
|
+
*/
|
|
73
|
+
fromAlpha(layer: Layer, mode?: MaskMode): void;
|
|
74
|
+
/**
|
|
75
|
+
* Remove the mask ("cmd + d").
|
|
76
|
+
*/
|
|
77
|
+
clear(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Draw onto the mask with a mode's operation, then re-measure it.
|
|
80
|
+
*/
|
|
81
|
+
private draw;
|
|
82
|
+
/**
|
|
83
|
+
* Check whether the mask has any pixels and trace its outline, within its bounds.
|
|
84
|
+
*/
|
|
85
|
+
private measure;
|
|
86
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createOffscreenCanvas } from "../../classes/Layer/index.js";
|
|
2
|
+
import { chainSegments, traceOutline } from "../../rendering/outline/index.js";
|
|
3
|
+
// src/lib/selection/Selection/index.ts
|
|
4
|
+
/** Canvas composite operation for each mask mode */
|
|
5
|
+
var OPERATIONS = {
|
|
6
|
+
replace: "source-over",
|
|
7
|
+
add: "source-over",
|
|
8
|
+
subtract: "destination-out"
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Bounding box of a shape.
|
|
12
|
+
*/
|
|
13
|
+
var shapeBounds = (shape) => {
|
|
14
|
+
if (shape.type !== "polygon") return shape.rect;
|
|
15
|
+
const xs = shape.points.map((p) => p.x);
|
|
16
|
+
const ys = shape.points.map((p) => p.y);
|
|
17
|
+
const x = Math.min(...xs);
|
|
18
|
+
const y = Math.min(...ys);
|
|
19
|
+
return {
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
width: Math.max(...xs) - x,
|
|
23
|
+
height: Math.max(...ys) - y
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The current mask: a canvas-sized greyscale area (stored as alpha) that limits where drawing applies. Only ever local.
|
|
28
|
+
*
|
|
29
|
+
* Shapes and layer alpha replace, add to or subtract from it. After each change the mask is measured once to find whether it is
|
|
30
|
+
* empty and to trace its outline (for the animated "crawling ants").
|
|
31
|
+
*
|
|
32
|
+
* Dispatches a `change` event whenever the mask changes.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* selection.apply({ type: "rect", rect: { x: 10, y: 10, width: 100, height: 50 } }, "replace");
|
|
36
|
+
* selection.active; // true
|
|
37
|
+
* selection.outline; // closed loops in canvas pixels
|
|
38
|
+
*/
|
|
39
|
+
var Selection = class extends EventTarget {
|
|
40
|
+
/** Canvas width in pixels */
|
|
41
|
+
width;
|
|
42
|
+
/** Canvas height in pixels */
|
|
43
|
+
height;
|
|
44
|
+
/** Whether a non-empty mask exists */
|
|
45
|
+
active = false;
|
|
46
|
+
/** Outline of the mask as closed loops in canvas pixels */
|
|
47
|
+
outline = [];
|
|
48
|
+
createCanvas;
|
|
49
|
+
maskCanvas;
|
|
50
|
+
/** Area that may contain masked pixels */
|
|
51
|
+
bounds;
|
|
52
|
+
constructor(width, height, createCanvas = createOffscreenCanvas) {
|
|
53
|
+
super();
|
|
54
|
+
this.width = width;
|
|
55
|
+
this.height = height;
|
|
56
|
+
this.createCanvas = createCanvas;
|
|
57
|
+
}
|
|
58
|
+
/** The mask canvas (alpha = mask value) while a mask is active, for limiting drawing */
|
|
59
|
+
get mask() {
|
|
60
|
+
return this.active ? this.maskCanvas : void 0;
|
|
61
|
+
}
|
|
62
|
+
/** The mask canvas's drawing context (creating the canvas on first use) */
|
|
63
|
+
get context() {
|
|
64
|
+
this.maskCanvas ??= this.createCanvas(this.width, this.height);
|
|
65
|
+
return this.maskCanvas.getContext("2d");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Replace, add to or subtract from the mask with a lasso shape.
|
|
69
|
+
*/
|
|
70
|
+
apply(shape, mode) {
|
|
71
|
+
this.draw(mode, shapeBounds(shape), (ctx) => {
|
|
72
|
+
ctx.beginPath();
|
|
73
|
+
if (shape.type === "rect") ctx.rect(shape.rect.x, shape.rect.y, shape.rect.width, shape.rect.height);
|
|
74
|
+
else if (shape.type === "ellipse") {
|
|
75
|
+
const { x, y, width, height } = shape.rect;
|
|
76
|
+
ctx.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
|
|
77
|
+
} else {
|
|
78
|
+
shape.points.forEach(({ x, y }, i) => i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y));
|
|
79
|
+
ctx.closePath();
|
|
80
|
+
}
|
|
81
|
+
ctx.fill();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Move part of the mask along with transformed pixels: removes the piece from where it was and draws it at its new place.
|
|
86
|
+
*
|
|
87
|
+
* @param draw - draws the piece in its new place (e.g. `drawTransformed`)
|
|
88
|
+
*/
|
|
89
|
+
moveArea(piece, from, to, draw) {
|
|
90
|
+
if (!this.active) return;
|
|
91
|
+
const ctx = this.context;
|
|
92
|
+
ctx.save();
|
|
93
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
94
|
+
ctx.drawImage(piece, from.x, from.y);
|
|
95
|
+
ctx.globalCompositeOperation = "source-over";
|
|
96
|
+
draw(ctx);
|
|
97
|
+
ctx.restore();
|
|
98
|
+
this.bounds = unionRect(this.bounds, to);
|
|
99
|
+
this.measure();
|
|
100
|
+
this.dispatchEvent(new Event("change"));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Mask the whole canvas ("cmd + a").
|
|
104
|
+
*/
|
|
105
|
+
selectAll() {
|
|
106
|
+
this.apply({
|
|
107
|
+
type: "rect",
|
|
108
|
+
rect: {
|
|
109
|
+
x: 0,
|
|
110
|
+
y: 0,
|
|
111
|
+
width: this.width,
|
|
112
|
+
height: this.height
|
|
113
|
+
}
|
|
114
|
+
}, "replace");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Invert the mask: masked parts become unmasked and the rest becomes masked. Without a mask, everything is masked.
|
|
118
|
+
*/
|
|
119
|
+
invert() {
|
|
120
|
+
if (!this.active) {
|
|
121
|
+
this.selectAll();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const ctx = this.context;
|
|
125
|
+
ctx.save();
|
|
126
|
+
ctx.globalCompositeOperation = "xor";
|
|
127
|
+
ctx.fillStyle = "#000000";
|
|
128
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
129
|
+
ctx.restore();
|
|
130
|
+
this.bounds = {
|
|
131
|
+
x: 0,
|
|
132
|
+
y: 0,
|
|
133
|
+
width: this.width,
|
|
134
|
+
height: this.height
|
|
135
|
+
};
|
|
136
|
+
this.measure();
|
|
137
|
+
this.dispatchEvent(new Event("change"));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Mask a layer's visible pixels, following their alpha (Cmd/Ctrl + click a layer's name).
|
|
141
|
+
*/
|
|
142
|
+
fromAlpha(layer, mode = "replace") {
|
|
143
|
+
this.draw(mode, {
|
|
144
|
+
x: 0,
|
|
145
|
+
y: 0,
|
|
146
|
+
width: this.width,
|
|
147
|
+
height: this.height
|
|
148
|
+
}, (ctx) => {
|
|
149
|
+
ctx.drawImage(layer.canvas, 0, 0);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Remove the mask ("cmd + d").
|
|
154
|
+
*/
|
|
155
|
+
clear() {
|
|
156
|
+
if (this.maskCanvas) this.context.clearRect(0, 0, this.width, this.height);
|
|
157
|
+
this.active = false;
|
|
158
|
+
this.outline = [];
|
|
159
|
+
this.bounds = void 0;
|
|
160
|
+
this.dispatchEvent(new Event("change"));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Draw onto the mask with a mode's operation, then re-measure it.
|
|
164
|
+
*/
|
|
165
|
+
draw(mode, area, paint) {
|
|
166
|
+
if (mode === "subtract" && !this.active) return;
|
|
167
|
+
const ctx = this.context;
|
|
168
|
+
if (mode === "replace") ctx.clearRect(0, 0, this.width, this.height);
|
|
169
|
+
ctx.save();
|
|
170
|
+
ctx.globalCompositeOperation = OPERATIONS[mode];
|
|
171
|
+
ctx.fillStyle = "#000000";
|
|
172
|
+
paint(ctx);
|
|
173
|
+
ctx.restore();
|
|
174
|
+
if (mode !== "subtract") this.bounds = mode === "replace" || !this.bounds ? area : unionRect(this.bounds, area);
|
|
175
|
+
this.measure();
|
|
176
|
+
this.dispatchEvent(new Event("change"));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check whether the mask has any pixels and trace its outline, within its bounds.
|
|
180
|
+
*/
|
|
181
|
+
measure() {
|
|
182
|
+
const area = this.bounds && clipToCanvas(this.bounds, this.width, this.height);
|
|
183
|
+
if (!area) {
|
|
184
|
+
this.active = false;
|
|
185
|
+
this.outline = [];
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const pixels = this.context.getImageData(area.x, area.y, area.width, area.height).data;
|
|
189
|
+
const alpha = new Uint8ClampedArray(area.width * area.height);
|
|
190
|
+
let any = false;
|
|
191
|
+
for (let i = 0; i < alpha.length; i++) {
|
|
192
|
+
const value = pixels[i * 4 + 3];
|
|
193
|
+
alpha[i] = value;
|
|
194
|
+
if (value) any = true;
|
|
195
|
+
}
|
|
196
|
+
this.active = any;
|
|
197
|
+
this.outline = any ? chainSegments(traceOutline(alpha, area.width, area.height, { normalise: false })).map((loop) => loop.map((value, i) => value + (i % 2 === 0 ? area.x : area.y))) : [];
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Smallest rectangle containing both rectangles.
|
|
202
|
+
*/
|
|
203
|
+
var unionRect = (a, b) => {
|
|
204
|
+
const x = Math.min(a.x, b.x);
|
|
205
|
+
const y = Math.min(a.y, b.y);
|
|
206
|
+
return {
|
|
207
|
+
x,
|
|
208
|
+
y,
|
|
209
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
210
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Snap a rectangle outwards to whole pixels (with a 1 px margin for anti-aliasing) and clip it to the canvas.
|
|
215
|
+
*/
|
|
216
|
+
var clipToCanvas = (rect, width, height) => {
|
|
217
|
+
const x = Math.max(0, Math.floor(rect.x) - 1);
|
|
218
|
+
const y = Math.max(0, Math.floor(rect.y) - 1);
|
|
219
|
+
const right = Math.min(width, Math.ceil(rect.x + rect.width) + 1);
|
|
220
|
+
const bottom = Math.min(height, Math.ceil(rect.y + rect.height) + 1);
|
|
221
|
+
return right > x && bottom > y ? {
|
|
222
|
+
x,
|
|
223
|
+
y,
|
|
224
|
+
width: right - x,
|
|
225
|
+
height: bottom - y
|
|
226
|
+
} : void 0;
|
|
227
|
+
};
|
|
228
|
+
export { Selection as default, shapeBounds };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Point, Rect } from "../../classes/Viewport";
|
|
2
|
+
/** How a new shape combines with the existing mask */
|
|
3
|
+
export type MaskMode = "replace" | "add" | "subtract";
|
|
4
|
+
/** Distance (screen pixels) within which clicking the first polygon node closes the polygon */
|
|
5
|
+
export declare const CLOSE_DISTANCE = 8;
|
|
6
|
+
/**
|
|
7
|
+
* Choose how a new shape combines with the mask from the keys held when it starts: Shift adds, Alt subtracts, otherwise it replaces.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* maskModeFor({ shiftKey: true, altKey: false }); // "add"
|
|
11
|
+
*/
|
|
12
|
+
export declare const maskModeFor: ({ shiftKey, altKey, }: {
|
|
13
|
+
shiftKey: boolean;
|
|
14
|
+
altKey: boolean;
|
|
15
|
+
}) => MaskMode;
|
|
16
|
+
/**
|
|
17
|
+
* Rectangle between two corner points, whichever way it was dragged. With `square`, the shorter side grows to match the longer one,
|
|
18
|
+
* keeping the drag direction.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* rectFromCorners({ x: 10, y: 10 }, { x: 0, y: 4 }); // { x: 0, y: 4, width: 10, height: 6 }
|
|
22
|
+
*/
|
|
23
|
+
export declare const rectFromCorners: (start: Point, end: Point, square?: boolean) => Rect;
|
|
24
|
+
/**
|
|
25
|
+
* Snap a point so the line from `from` runs at a multiple of 45°, keeping the line's length along that direction.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* snapAngle({ x: 0, y: 0 }, { x: 10, y: 1 }); // { x: 10, y: 0 }
|
|
29
|
+
*/
|
|
30
|
+
export declare const snapAngle: (from: Point, to: Point) => Point;
|
|
31
|
+
/**
|
|
32
|
+
* Check whether two points are within a distance of each other.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* isNear({ x: 0, y: 0 }, { x: 3, y: 4 }, 5); // true
|
|
36
|
+
*/
|
|
37
|
+
export declare const isNear: (a: Point, b: Point, distance: number) => boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/lib/selection/geometry/index.ts
|
|
2
|
+
/** Distance (screen pixels) within which clicking the first polygon node closes the polygon */
|
|
3
|
+
var CLOSE_DISTANCE = 8;
|
|
4
|
+
/**
|
|
5
|
+
* Choose how a new shape combines with the mask from the keys held when it starts: Shift adds, Alt subtracts, otherwise it replaces.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* maskModeFor({ shiftKey: true, altKey: false }); // "add"
|
|
9
|
+
*/
|
|
10
|
+
var maskModeFor = ({ shiftKey, altKey }) => altKey ? "subtract" : shiftKey ? "add" : "replace";
|
|
11
|
+
/**
|
|
12
|
+
* Rectangle between two corner points, whichever way it was dragged. With `square`, the shorter side grows to match the longer one,
|
|
13
|
+
* keeping the drag direction.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* rectFromCorners({ x: 10, y: 10 }, { x: 0, y: 4 }); // { x: 0, y: 4, width: 10, height: 6 }
|
|
17
|
+
*/
|
|
18
|
+
var rectFromCorners = (start, end, square = false) => {
|
|
19
|
+
let dx = end.x - start.x;
|
|
20
|
+
let dy = end.y - start.y;
|
|
21
|
+
if (square) {
|
|
22
|
+
const size = Math.max(Math.abs(dx), Math.abs(dy));
|
|
23
|
+
dx = Math.sign(dx || 1) * size;
|
|
24
|
+
dy = Math.sign(dy || 1) * size;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
x: Math.min(start.x, start.x + dx),
|
|
28
|
+
y: Math.min(start.y, start.y + dy),
|
|
29
|
+
width: Math.abs(dx),
|
|
30
|
+
height: Math.abs(dy)
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Snap a point so the line from `from` runs at a multiple of 45°, keeping the line's length along that direction.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* snapAngle({ x: 0, y: 0 }, { x: 10, y: 1 }); // { x: 10, y: 0 }
|
|
38
|
+
*/
|
|
39
|
+
var snapAngle = (from, to) => {
|
|
40
|
+
const dx = to.x - from.x;
|
|
41
|
+
const dy = to.y - from.y;
|
|
42
|
+
const length = Math.hypot(dx, dy);
|
|
43
|
+
const angle = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
|
|
44
|
+
const round = (value) => Math.round(value * 1e9) / 1e9;
|
|
45
|
+
return {
|
|
46
|
+
x: round(from.x + Math.cos(angle) * length),
|
|
47
|
+
y: round(from.y + Math.sin(angle) * length)
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Check whether two points are within a distance of each other.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* isNear({ x: 0, y: 0 }, { x: 3, y: 4 }, 5); // true
|
|
55
|
+
*/
|
|
56
|
+
var isNear = (a, b, distance) => Math.hypot(a.x - b.x, a.y - b.y) <= distance;
|
|
57
|
+
export { CLOSE_DISTANCE, isNear, maskModeFor, rectFromCorners, snapAngle };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LayerConfig } from "../classes/Layer";
|
|
2
|
+
/** Version of a saved drawing's shape, so drawings saved by other versions can be recognised */
|
|
3
|
+
export declare const SAVED_DRAWING_VERSION = 1;
|
|
4
|
+
/** Name of the IndexedDB database `IndexedDbLayerStore` keeps drawings in */
|
|
5
|
+
export declare const DATABASE_NAME = "paintcanvas";
|
|
6
|
+
/** A saved layer's settings (its pixels are kept separately, as a PNG) */
|
|
7
|
+
export type SavedLayer = Required<LayerConfig>;
|
|
8
|
+
/** A saved drawing: its size, its layers (top first) without their pixels, and the selected layer */
|
|
9
|
+
export interface SavedDrawing {
|
|
10
|
+
/** The shape's version (see `SAVED_DRAWING_VERSION`) */
|
|
11
|
+
version: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
layers: SavedLayer[];
|
|
15
|
+
/** ID of the selected layer */
|
|
16
|
+
activeLayer: string;
|
|
17
|
+
}
|
|
18
|
+
/** A saved drawing with each layer's pixels as a PNG, by layer ID */
|
|
19
|
+
export interface SavedDrawingWithImages {
|
|
20
|
+
drawing: SavedDrawing;
|
|
21
|
+
images: Map<string, Blob>;
|
|
22
|
+
}
|
|
23
|
+
/** Where a drawing's layers are kept (see `IndexedDbLayerStore`) */
|
|
24
|
+
export interface LayerStoreLike {
|
|
25
|
+
/** The drawing's name. Windows using the same name share the drawing, so only one draws at a time. */
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/** The saved drawing, or undefined if none was saved */
|
|
28
|
+
load(): Promise<SavedDrawingWithImages | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* Save the drawing and the images of the layers that changed (by layer ID). Images of layers no longer in the
|
|
31
|
+
* drawing are deleted.
|
|
32
|
+
*/
|
|
33
|
+
save(drawing: SavedDrawing, images: ReadonlyMap<string, Blob>): Promise<void>;
|
|
34
|
+
/** Delete the saved drawing */
|
|
35
|
+
clear(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Keeps drawings in the browser's IndexedDB database (`DATABASE_NAME`), each under its own name: the drawing in one
|
|
39
|
+
* record, and each layer's pixels as a PNG in its own record, so saving rewrites only the layers that changed.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const canvas = new PaintCanvas({ layerStore: new IndexedDbLayerStore("my-drawing") });
|
|
43
|
+
*/
|
|
44
|
+
export default class IndexedDbLayerStore implements LayerStoreLike {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
private readonly factory;
|
|
47
|
+
private database?;
|
|
48
|
+
/**
|
|
49
|
+
* @param name - the drawing's name
|
|
50
|
+
* @param factory - the IndexedDB to use (the browser's by default)
|
|
51
|
+
*/
|
|
52
|
+
constructor(name: string, factory?: IDBFactory);
|
|
53
|
+
load(): Promise<SavedDrawingWithImages | undefined>;
|
|
54
|
+
save(drawing: SavedDrawing, images: ReadonlyMap<string, Blob>): Promise<void>;
|
|
55
|
+
clear(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Open the database (once), creating its object stores the first time. If it can't be opened, the next call tries
|
|
58
|
+
* again.
|
|
59
|
+
*/
|
|
60
|
+
private open;
|
|
61
|
+
/** The keys of this drawing's layer images: `[name, layerId]` (arrays sort after strings) */
|
|
62
|
+
private imageKeys;
|
|
63
|
+
}
|