@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,1004 @@
|
|
|
1
|
+
import { unionRect } from "../../../lib/brushes/StrokeEngine/index.js";
|
|
2
|
+
import style_default from "./style.js";
|
|
3
|
+
import { NO_TEXT_SELECTION_CSS } from "../../styles/index.js";
|
|
4
|
+
import { PcElement, defineElement } from "../../base/index.js";
|
|
5
|
+
import ChangeController from "../../ChangeController/index.js";
|
|
6
|
+
import { Tool } from "../../../lib/classes/PaintCanvas/index.js";
|
|
7
|
+
import { PASTEBOARD_COLOUR, drawScene } from "../../../lib/rendering/compositing/index.js";
|
|
8
|
+
import { drawBrushCursor } from "../../../lib/rendering/cursor/index.js";
|
|
9
|
+
import { drawLassoPreview, drawMaskOutline } from "../../../lib/rendering/maskOutline/index.js";
|
|
10
|
+
import { isNear } from "../../../lib/selection/geometry/index.js";
|
|
11
|
+
import { drawTransformBox } from "../../../lib/rendering/transformBox/index.js";
|
|
12
|
+
import { EYEDROPPER_CURSOR, FREEFORM_LASSO_CURSOR, POLYGON_LASSO_CURSOR, ROTATE_CURSOR } from "../../../lib/rendering/cursors/index.js";
|
|
13
|
+
import { SCALE_HANDLES, handleAt, isInsideBox, moveBox, resizeCursor, rotateBox, rotateHandle, scaleBox } from "../../../lib/transform/geometry/index.js";
|
|
14
|
+
import { zoomModifierName } from "../../../lib/classes/InputSettings/index.js";
|
|
15
|
+
import { touchPair, wheelAction } from "../../../lib/utils/navigation/index.js";
|
|
16
|
+
import GestureTracker from "../../../lib/classes/GestureTracker/index.js";
|
|
17
|
+
import "../../../lib/rendering/sample/index.js";
|
|
18
|
+
import { gestureAction } from "../../../lib/utils/gestures/index.js";
|
|
19
|
+
import { snapToPixelGrid } from "../../../lib/brushes/params/index.js";
|
|
20
|
+
import { html, unsafeCSS } from "lit";
|
|
21
|
+
// src/ui/elements/PcCanvas/index.ts
|
|
22
|
+
/** Toolbar width in CSS pixels (kept clear when fitting the canvas) */
|
|
23
|
+
var TOOLBAR_WIDTH = 50;
|
|
24
|
+
/** Share of the screen width taken by the chat panel (kept clear when fitting the canvas) */
|
|
25
|
+
var CHAT_WIDTH_RATIO = .375;
|
|
26
|
+
/** Lasso kind for each lasso tool */
|
|
27
|
+
var LASSO_TOOLS = {
|
|
28
|
+
[Tool.RectangleLasso]: "rectangle",
|
|
29
|
+
[Tool.CircleLasso]: "circle",
|
|
30
|
+
[Tool.PolygonLasso]: "polygon",
|
|
31
|
+
[Tool.FreeformLasso]: "freeform"
|
|
32
|
+
};
|
|
33
|
+
/** Distance (CSS pixels) within which a transform handle can be grabbed */
|
|
34
|
+
var HANDLE_GRAB_DISTANCE = 8;
|
|
35
|
+
/** Milliseconds between steps of the crawling ants animation */
|
|
36
|
+
var ANTS_INTERVAL = 100;
|
|
37
|
+
/** In focus pen mode, how long after a pen lifts touches are still ignored (milliseconds) */
|
|
38
|
+
var PEN_LIFT_GRACE_MS = 500;
|
|
39
|
+
/**
|
|
40
|
+
* Space covered by the toolbar and chat on a screen of the given width.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* interfaceInsets(1000, true); // { top: 0, right: 375, bottom: 0, left: 50 }
|
|
44
|
+
*/
|
|
45
|
+
var interfaceInsets = (width, interfaceVisible) => interfaceVisible ? {
|
|
46
|
+
top: 0,
|
|
47
|
+
right: width * CHAT_WIDTH_RATIO,
|
|
48
|
+
bottom: 0,
|
|
49
|
+
left: 50
|
|
50
|
+
} : {
|
|
51
|
+
top: 0,
|
|
52
|
+
right: 0,
|
|
53
|
+
bottom: 0,
|
|
54
|
+
left: 0
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* CSS cursor for a tool. The brush and eraser hide the cursor because their outline is drawn on the canvas; rectangle and circle lassos
|
|
58
|
+
* use the "+" crosshair; zoom uses the magnifier with "+" or "-".
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* cursorFor(Tool.Zoom, { dragging: false, altKey: true }); // "zoom-out"
|
|
62
|
+
*/
|
|
63
|
+
/** How far above a finger the sampling window sits, so the finger doesn't cover it (screen pixels) */
|
|
64
|
+
var TOUCH_SAMPLER_GAP = 40;
|
|
65
|
+
/** The sampling window's size: the colour on top of the enlarged pixels (screen pixels) */
|
|
66
|
+
var SAMPLER_WIDTH = 50;
|
|
67
|
+
var SAMPLER_HEIGHT = SAMPLER_WIDTH * 2;
|
|
68
|
+
/**
|
|
69
|
+
* Where the colour sample tool's window goes (its top left, in screen pixels): with a mouse or pen, so the outlined
|
|
70
|
+
* pixel in its enlarged view is on the pointer (which is hidden); with touch, centred above the finger. Kept on screen.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* samplerPosition({ x: 100, y: 200 }, false, 1000, 800); // { x: 72.5, y: 122.5 }
|
|
74
|
+
*/
|
|
75
|
+
var samplerPosition = (point, touch, width, height) => {
|
|
76
|
+
const pixelCentre = 27.5;
|
|
77
|
+
const x = point.x - (touch ? SAMPLER_WIDTH / 2 : pixelCentre);
|
|
78
|
+
const y = touch ? point.y - 40 - SAMPLER_HEIGHT : point.y - SAMPLER_WIDTH - pixelCentre;
|
|
79
|
+
return {
|
|
80
|
+
x: Math.min(Math.max(0, x), width - SAMPLER_WIDTH),
|
|
81
|
+
y: Math.min(Math.max(0, y), height - SAMPLER_HEIGHT)
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
var cursorFor = (tool, { dragging, altKey }) => {
|
|
85
|
+
switch (tool) {
|
|
86
|
+
case Tool.Pan: return dragging ? "grabbing" : "grab";
|
|
87
|
+
case Tool.Zoom: return altKey ? "zoom-out" : "zoom-in";
|
|
88
|
+
case Tool.Brush:
|
|
89
|
+
case Tool.Eraser: return "none";
|
|
90
|
+
case Tool.Move: return "move";
|
|
91
|
+
case Tool.PolygonLasso: return POLYGON_LASSO_CURSOR;
|
|
92
|
+
case Tool.FreeformLasso: return FREEFORM_LASSO_CURSOR;
|
|
93
|
+
case Tool.ColourSample: return EYEDROPPER_CURSOR;
|
|
94
|
+
default: return "crosshair";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The drawing area: a full-size canvas showing the composited layers through the viewport.
|
|
99
|
+
*
|
|
100
|
+
* Handles drawing with the brush and eraser (mouse, pen with pressure and tilt, or touch), and navigation: dragging with the pan tool (or while Space is held, or with the middle mouse button), clicking with the zoom tool (Alt to zoom out), scrolling or two-finger dragging to pan, and pinching or Cmd/Ctrl + scrolling to zoom (each as the user's settings allow).
|
|
101
|
+
* Taps and swipes with two or three fingers undo, redo, copy or paste, as the user's touch gesture settings say (see
|
|
102
|
+
* `GestureTracker`); two-finger pan and pinch wait until a finger moves further than a tap allows.
|
|
103
|
+
* In focus pen mode (a user setting), one finger does nothing (only pens draw), and touches are ignored altogether while
|
|
104
|
+
* a pen is down or for `PEN_LIFT_GRACE_MS` after it lifts, so a resting hand can't draw, pan or undo.
|
|
105
|
+
* The Move tool shows the transform box: drag inside it to move, a square handle to scale, or the round handle to rotate.
|
|
106
|
+
* The colour sample tool sets the current colour from what's shown under the pointer, or the average of a dragged area,
|
|
107
|
+
* showing a sampling window while sampling (see `drawSampler`).
|
|
108
|
+
* Lasso tools build mask shapes (see `Lasso`); the mask's outline is drawn as animated black and white "crawling ants".
|
|
109
|
+
* The mask outline and lasso shape are drawn on their own canvas; the brush cursor on an overlay that inverts what is underneath.
|
|
110
|
+
* Redraws at most once per animation frame, only when something changed. The first time it has a size, it fits the canvas beside the toolbar and chat.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* html`<pc-canvas .app=${app}></pc-canvas>`
|
|
114
|
+
*/
|
|
115
|
+
var PcCanvas = class extends PcElement {
|
|
116
|
+
static properties = {
|
|
117
|
+
...PcElement.properties,
|
|
118
|
+
app: { attribute: false },
|
|
119
|
+
insets: { attribute: false }
|
|
120
|
+
};
|
|
121
|
+
static styles = [
|
|
122
|
+
PcElement.styles,
|
|
123
|
+
unsafeCSS(NO_TEXT_SELECTION_CSS),
|
|
124
|
+
unsafeCSS(style_default)
|
|
125
|
+
];
|
|
126
|
+
/** The insets in effect (see `insets`) */
|
|
127
|
+
get activeInsets() {
|
|
128
|
+
const app = this.app;
|
|
129
|
+
return this.insets ?? interfaceInsets(app.viewport.width, app.interfaceVisible);
|
|
130
|
+
}
|
|
131
|
+
/** Device pixels per CSS pixel (replaceable for tests) */
|
|
132
|
+
pixelRatio = () => window.devicePixelRatio || 1;
|
|
133
|
+
changes = new ChangeController(this, () => [
|
|
134
|
+
this.app,
|
|
135
|
+
this.app?.brush,
|
|
136
|
+
this.app?.eraser,
|
|
137
|
+
this.app?.layers,
|
|
138
|
+
this.app?.external,
|
|
139
|
+
this.app?.viewport,
|
|
140
|
+
this.app?.strokes,
|
|
141
|
+
this.app?.tips,
|
|
142
|
+
this.app?.selection,
|
|
143
|
+
this.app?.lasso,
|
|
144
|
+
this.app?.transform
|
|
145
|
+
], (target) => this.sceneChanged(target));
|
|
146
|
+
/** Whether anything but the stroke in progress changed since the last update (so everything is redrawn) */
|
|
147
|
+
fullChange = false;
|
|
148
|
+
/** The area the stroke in progress changed since the last update */
|
|
149
|
+
strokeArea;
|
|
150
|
+
/** Whether a finished stroke is being written into its layer (its layer change needs no redraw) */
|
|
151
|
+
committing = false;
|
|
152
|
+
/** Whether the next frame redraws everything */
|
|
153
|
+
drawAll = false;
|
|
154
|
+
/** The area the next frame redraws, if not everything */
|
|
155
|
+
drawArea;
|
|
156
|
+
/**
|
|
157
|
+
* Transform drag in progress: what is being changed, the box and pointer when it started, the pointer now, and (when
|
|
158
|
+
* scaling) whether "Constrain proportions" was on when it started
|
|
159
|
+
*/
|
|
160
|
+
transformDrag = null;
|
|
161
|
+
/** Crawling ants animation timer while a mask is active */
|
|
162
|
+
antsTimer;
|
|
163
|
+
/** Current crawling ants dash offset */
|
|
164
|
+
antsOffset = 0;
|
|
165
|
+
/** Viewport that has been fitted once (so resizing doesn't refit) */
|
|
166
|
+
fittedViewport;
|
|
167
|
+
frame = 0;
|
|
168
|
+
/** Animation frame drawing only the brush cursor, if requested */
|
|
169
|
+
overlayFrame = 0;
|
|
170
|
+
resizeObserver;
|
|
171
|
+
drag = null;
|
|
172
|
+
/** Pointer drawing the current stroke, and the layer it draws on */
|
|
173
|
+
stroke = null;
|
|
174
|
+
/** Pointer position over the stage (for the brush cursor), or null when outside */
|
|
175
|
+
hover = null;
|
|
176
|
+
/** Name of the key held to zoom with the scroll wheel (replaceable for tests) */
|
|
177
|
+
modifier = zoomModifierName();
|
|
178
|
+
/** Touches on the stage by pointer ID, for two-finger pan and pinch */
|
|
179
|
+
touches = /* @__PURE__ */ new Map();
|
|
180
|
+
/** The two-finger gesture in progress: where its touches' midpoint and spread were last */
|
|
181
|
+
pinch = null;
|
|
182
|
+
/**
|
|
183
|
+
* A colour sample in progress: the pointer, whether it's a finger, where it started and is now (canvas pixels), and
|
|
184
|
+
* where it is on screen
|
|
185
|
+
*/
|
|
186
|
+
sampling = null;
|
|
187
|
+
/** Recognises taps and swipes with two or three fingers, for the actions in the user's touch gesture settings */
|
|
188
|
+
gestureTracker = new GestureTracker();
|
|
189
|
+
/** Pens touching the stage, by pointer ID */
|
|
190
|
+
pens = /* @__PURE__ */ new Set();
|
|
191
|
+
/** When a pen last lifted (event time) */
|
|
192
|
+
penLiftedAt = -Infinity;
|
|
193
|
+
/** Touches ignored altogether in focus pen mode (they touched while a pen was down or had just lifted) */
|
|
194
|
+
ignoredTouches = /* @__PURE__ */ new Set();
|
|
195
|
+
/** Touches that can only pan, pinch and make gestures in focus pen mode (one finger does nothing) */
|
|
196
|
+
navigationTouches = /* @__PURE__ */ new Set();
|
|
197
|
+
/** Zoom when a Safari trackpad pinch started */
|
|
198
|
+
gestureZoom = 1;
|
|
199
|
+
altKey = false;
|
|
200
|
+
onModifierKey = (event) => {
|
|
201
|
+
if (event.altKey !== this.altKey) {
|
|
202
|
+
this.altKey = event.altKey;
|
|
203
|
+
this.updateCursor();
|
|
204
|
+
}
|
|
205
|
+
if (this.transformDrag?.kind === "scale") this.applyTransformDrag(event);
|
|
206
|
+
};
|
|
207
|
+
/** The screen canvas */
|
|
208
|
+
get canvas() {
|
|
209
|
+
return this.renderRoot.querySelector("canvas.scene");
|
|
210
|
+
}
|
|
211
|
+
/** The canvas for the colour sample tool's window and area */
|
|
212
|
+
get sampler() {
|
|
213
|
+
return this.renderRoot.querySelector("canvas.sampler");
|
|
214
|
+
}
|
|
215
|
+
/** The overlay canvas for the brush cursor */
|
|
216
|
+
get overlay() {
|
|
217
|
+
return this.renderRoot.querySelector("canvas.overlay");
|
|
218
|
+
}
|
|
219
|
+
/** The canvas for the mask outline and lasso shape (no blend mode: drawn in black and white) */
|
|
220
|
+
get outlines() {
|
|
221
|
+
return this.renderRoot.querySelector("canvas.outlines");
|
|
222
|
+
}
|
|
223
|
+
connectedCallback() {
|
|
224
|
+
super.connectedCallback();
|
|
225
|
+
window.addEventListener("keydown", this.onModifierKey);
|
|
226
|
+
window.addEventListener("keyup", this.onModifierKey);
|
|
227
|
+
}
|
|
228
|
+
disconnectedCallback() {
|
|
229
|
+
super.disconnectedCallback();
|
|
230
|
+
window.removeEventListener("keydown", this.onModifierKey);
|
|
231
|
+
window.removeEventListener("keyup", this.onModifierKey);
|
|
232
|
+
this.resizeObserver?.disconnect();
|
|
233
|
+
this.animateAnts(false);
|
|
234
|
+
cancelAnimationFrame(this.frame);
|
|
235
|
+
cancelAnimationFrame(this.overlayFrame);
|
|
236
|
+
this.overlayFrame = 0;
|
|
237
|
+
this.frame = 0;
|
|
238
|
+
}
|
|
239
|
+
firstUpdated() {
|
|
240
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
241
|
+
this.resizeObserver = new ResizeObserver(() => this.syncSize());
|
|
242
|
+
this.resizeObserver.observe(this);
|
|
243
|
+
}
|
|
244
|
+
this.syncSize();
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Note what changed: the stroke in progress (and the area it changed), or anything else.
|
|
248
|
+
*/
|
|
249
|
+
sceneChanged(target) {
|
|
250
|
+
const app = this.app;
|
|
251
|
+
if (target === app?.strokes) {
|
|
252
|
+
const area = app.strokes.takeChangedArea();
|
|
253
|
+
if (area) this.strokeArea = unionRect(this.strokeArea, area);
|
|
254
|
+
else this.fullChange = true;
|
|
255
|
+
} else if (!(this.committing && target === app?.layers)) this.fullChange = true;
|
|
256
|
+
}
|
|
257
|
+
updated(changed) {
|
|
258
|
+
const resized = this.syncSize();
|
|
259
|
+
const onlyStroke = changed.size === 0 && !this.fullChange && !resized;
|
|
260
|
+
this.requestDraw(onlyStroke ? this.strokeArea : void 0);
|
|
261
|
+
this.fullChange = false;
|
|
262
|
+
this.strokeArea = void 0;
|
|
263
|
+
this.updateCursor();
|
|
264
|
+
this.animateAnts(this.app?.selection.active ?? false);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Match the canvas's pixel size to its on-screen size and pixel ratio, and update the viewport. Fits the canvas the
|
|
268
|
+
* first time. Returns whether the canvas was resized (which clears it).
|
|
269
|
+
*/
|
|
270
|
+
syncSize() {
|
|
271
|
+
const app = this.app;
|
|
272
|
+
const canvas = this.canvas;
|
|
273
|
+
if (!app || !canvas) return false;
|
|
274
|
+
let resized = false;
|
|
275
|
+
const width = this.clientWidth;
|
|
276
|
+
const height = this.clientHeight;
|
|
277
|
+
const ratio = this.pixelRatio();
|
|
278
|
+
const pixelWidth = Math.round(width * ratio);
|
|
279
|
+
const pixelHeight = Math.round(height * ratio);
|
|
280
|
+
for (const target of [
|
|
281
|
+
canvas,
|
|
282
|
+
this.outlines,
|
|
283
|
+
this.overlay,
|
|
284
|
+
this.sampler
|
|
285
|
+
]) if (target && (target.width !== pixelWidth || target.height !== pixelHeight)) {
|
|
286
|
+
target.width = pixelWidth;
|
|
287
|
+
target.height = pixelHeight;
|
|
288
|
+
resized = true;
|
|
289
|
+
}
|
|
290
|
+
const view = app.viewport;
|
|
291
|
+
if (view.width !== width || view.height !== height) {
|
|
292
|
+
const dx = (width - view.width) / 2;
|
|
293
|
+
const dy = (height - view.height) / 2;
|
|
294
|
+
view.resize(width, height);
|
|
295
|
+
if (this.fittedViewport === view) view.panBy(dx, dy);
|
|
296
|
+
}
|
|
297
|
+
if (this.fittedViewport !== view && width > 0 && height > 0) {
|
|
298
|
+
this.fittedViewport = view;
|
|
299
|
+
view.fit(this.activeInsets);
|
|
300
|
+
}
|
|
301
|
+
return resized;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Schedule a redraw of everything, or of an area of the canvas, on the next animation frame (repeated requests are
|
|
305
|
+
* combined; redrawing everything wins).
|
|
306
|
+
*/
|
|
307
|
+
requestDraw(area) {
|
|
308
|
+
if (area) this.drawArea = unionRect(this.drawArea, area);
|
|
309
|
+
else this.drawAll = true;
|
|
310
|
+
if (this.frame) return;
|
|
311
|
+
this.frame = requestAnimationFrame(() => {
|
|
312
|
+
this.frame = 0;
|
|
313
|
+
const drawArea = this.drawAll ? void 0 : this.drawArea;
|
|
314
|
+
this.drawAll = false;
|
|
315
|
+
this.drawArea = void 0;
|
|
316
|
+
this.draw(drawArea);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Draw the scene now (with the stroke in progress, if any), or only an area of the canvas, then the brush cursor.
|
|
321
|
+
*/
|
|
322
|
+
draw(area) {
|
|
323
|
+
cancelAnimationFrame(this.overlayFrame);
|
|
324
|
+
this.overlayFrame = 0;
|
|
325
|
+
const app = this.app;
|
|
326
|
+
const ctx = this.canvas?.getContext("2d");
|
|
327
|
+
if (app && ctx) {
|
|
328
|
+
const start = app.strokeLog.now();
|
|
329
|
+
drawScene(ctx, app.drawables, app.viewport, this.pixelRatio(), app.transform.preview ?? app.strokes.preview, area);
|
|
330
|
+
app.strokeLog.frame(app.strokeLog.now() - start);
|
|
331
|
+
}
|
|
332
|
+
this.drawOverlay();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Schedule drawing the brush cursor on the next animation frame (repeated requests are combined, and a scene redraw
|
|
336
|
+
* draws it too).
|
|
337
|
+
*/
|
|
338
|
+
requestOverlay() {
|
|
339
|
+
if (this.overlayFrame || this.frame) return;
|
|
340
|
+
this.overlayFrame = requestAnimationFrame(() => {
|
|
341
|
+
this.overlayFrame = 0;
|
|
342
|
+
if (!this.frame) this.drawOverlay();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Draw the brush cursor on the overlay: a 1:1 outline of the brush or eraser shape at the pointer. For presets that
|
|
347
|
+
* snap to the pixel grid, the outline covers the pixels the stamp will.
|
|
348
|
+
*/
|
|
349
|
+
drawOverlay() {
|
|
350
|
+
const app = this.app;
|
|
351
|
+
const overlay = this.overlay;
|
|
352
|
+
const ctx = overlay?.getContext("2d");
|
|
353
|
+
if (!app || !overlay || !ctx) return;
|
|
354
|
+
app.strokeLog.overlay();
|
|
355
|
+
ctx.clearRect(0, 0, overlay.width, overlay.height);
|
|
356
|
+
this.drawOutlines();
|
|
357
|
+
this.drawSampler();
|
|
358
|
+
if (app.strokes.debugEnabled) this.drawDebugTrace(ctx);
|
|
359
|
+
const settings = this.brushSettings();
|
|
360
|
+
if (!settings || !this.hover || this.drag) return;
|
|
361
|
+
const { viewport } = app;
|
|
362
|
+
let { x, y } = this.hover;
|
|
363
|
+
let { width, height } = settings;
|
|
364
|
+
if (settings.preset.snapToPixelGrid) {
|
|
365
|
+
const point = viewport.screenToCanvas(this.hover);
|
|
366
|
+
const area = snapToPixelGrid(point.x, point.y, width, height);
|
|
367
|
+
({x, y} = viewport.canvasToScreen(area));
|
|
368
|
+
({width, height} = area);
|
|
369
|
+
}
|
|
370
|
+
drawBrushCursor(ctx, app.tips.get(settings.preset), x, y, width * viewport.zoom, height * viewport.zoom, settings.preset.rotation ?? 0, this.pixelRatio());
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Draw the mask outline (crawling ants) and the lasso shape in progress in black and white on their own canvas.
|
|
374
|
+
*/
|
|
375
|
+
drawOutlines() {
|
|
376
|
+
const app = this.app;
|
|
377
|
+
const outlines = this.outlines;
|
|
378
|
+
const ctx = outlines?.getContext("2d");
|
|
379
|
+
if (!outlines || !ctx) return;
|
|
380
|
+
ctx.clearRect(0, 0, outlines.width, outlines.height);
|
|
381
|
+
const ratio = this.pixelRatio();
|
|
382
|
+
if (app.selection.active && !app.transform.pending) drawMaskOutline(ctx, app.selection.outline, app.viewport, ratio, this.antsOffset);
|
|
383
|
+
const lassoShape = app.lasso.preview;
|
|
384
|
+
if (lassoShape) drawLassoPreview(ctx, lassoShape, app.lasso.nodes, app.viewport, ratio);
|
|
385
|
+
const handles = this.transformHandles();
|
|
386
|
+
if (handles && app.transform.box) drawTransformBox(ctx, app.transform.box, handles, app.viewport, ratio);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The square handles of the transform box (the 8 scale handles) while the Move tool is in use, or none.
|
|
390
|
+
*/
|
|
391
|
+
transformHandles() {
|
|
392
|
+
return this.app?.activeTool === Tool.Move ? SCALE_HANDLES : void 0;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* What the pointer is over in the transform box: the rotate handle, a scale handle, inside the box, or none of them.
|
|
396
|
+
*/
|
|
397
|
+
transformPart(box, screen) {
|
|
398
|
+
const view = this.app.viewport;
|
|
399
|
+
const toScreen = (p) => view.canvasToScreen(p);
|
|
400
|
+
if (isNear(rotateHandle(box, toScreen).handle, screen, 8)) return { kind: "rotate" };
|
|
401
|
+
const handle = handleAt(box, SCALE_HANDLES, screen, 8, toScreen);
|
|
402
|
+
if (handle) return {
|
|
403
|
+
kind: "scale",
|
|
404
|
+
handle
|
|
405
|
+
};
|
|
406
|
+
return isInsideBox(box, view.screenToCanvas(screen)) ? { kind: "move" } : void 0;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Start rotating, scaling or moving the transform box, depending on where the pointer is: on the round handle, a
|
|
410
|
+
* square handle, or anywhere else (moving).
|
|
411
|
+
*/
|
|
412
|
+
beginTransformDrag(event) {
|
|
413
|
+
const app = this.app;
|
|
414
|
+
const box = app.transform.box;
|
|
415
|
+
if (!box) return;
|
|
416
|
+
const screen = this.pointerPoint(event);
|
|
417
|
+
const part = this.transformPart(box, screen) ?? { kind: "move" };
|
|
418
|
+
this.transformDrag = {
|
|
419
|
+
pointerId: event.pointerId,
|
|
420
|
+
kind: part.kind,
|
|
421
|
+
handle: part.kind === "scale" ? part.handle : void 0,
|
|
422
|
+
startBox: box,
|
|
423
|
+
startPoint: app.viewport.screenToCanvas(screen),
|
|
424
|
+
point: app.viewport.screenToCanvas(screen),
|
|
425
|
+
constrain: app.constrainProportions
|
|
426
|
+
};
|
|
427
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Update the transform box while dragging.
|
|
431
|
+
*/
|
|
432
|
+
continueTransformDrag(event) {
|
|
433
|
+
const app = this.app;
|
|
434
|
+
const drag = this.transformDrag;
|
|
435
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
436
|
+
drag.point = app.viewport.screenToCanvas(this.pointerPoint(event));
|
|
437
|
+
this.applyTransformDrag(event);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Change the transform box for the drag in progress, with the modifier keys held now. While scaling, "Constrain
|
|
441
|
+
* proportions" is turned on while Shift is held (if it was off when the drag started), or off while Alt is held (if
|
|
442
|
+
* it was on), and shows so in the settings bar.
|
|
443
|
+
*/
|
|
444
|
+
applyTransformDrag({ shiftKey, altKey }) {
|
|
445
|
+
const app = this.app;
|
|
446
|
+
const { kind, handle, startBox, startPoint, point, constrain } = this.transformDrag;
|
|
447
|
+
let box;
|
|
448
|
+
if (kind === "scale") {
|
|
449
|
+
const proportional = constrain ? !altKey : shiftKey;
|
|
450
|
+
if (app.constrainProportions !== proportional) app.setConstrainProportions(proportional);
|
|
451
|
+
box = scaleBox(startBox, handle, point, proportional, app.transform.aspect);
|
|
452
|
+
} else if (kind === "rotate") box = rotateBox(startBox, startPoint, point, shiftKey);
|
|
453
|
+
else box = moveBox(startBox, point.x - startPoint.x, point.y - startPoint.y, shiftKey);
|
|
454
|
+
app.transform.setBox(box);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* End the transform drag of a pointer, if it has one: "Constrain proportions" goes back to how it was when a scale
|
|
458
|
+
* drag started.
|
|
459
|
+
*/
|
|
460
|
+
endTransformDrag(pointerId) {
|
|
461
|
+
const drag = this.transformDrag;
|
|
462
|
+
if (drag?.pointerId !== pointerId) return;
|
|
463
|
+
this.transformDrag = null;
|
|
464
|
+
const app = this.app;
|
|
465
|
+
if (drag.kind === "scale" && app.constrainProportions !== drag.constrain) app.setConstrainProportions(drag.constrain);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Cursor while hovering with the Move tool: the grab cursor over the rotate handle, a two-way arrow along a scale
|
|
469
|
+
* handle's direction (turned with the box), four-way arrows inside the box, and the default cursor elsewhere.
|
|
470
|
+
*/
|
|
471
|
+
transformCursor(screen) {
|
|
472
|
+
const app = this.app;
|
|
473
|
+
const box = app.transform.box;
|
|
474
|
+
if (app.activeTool !== Tool.Move || !box) return void 0;
|
|
475
|
+
const part = this.transformPart(box, screen);
|
|
476
|
+
if (part?.kind === "rotate") return ROTATE_CURSOR;
|
|
477
|
+
if (part?.kind === "scale") return resizeCursor(part.handle, box.rotation);
|
|
478
|
+
return part ? "move" : "default";
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Start or stop the crawling ants animation.
|
|
482
|
+
*/
|
|
483
|
+
animateAnts(run) {
|
|
484
|
+
if (run && !this.antsTimer) this.antsTimer = setInterval(() => {
|
|
485
|
+
this.antsOffset = (this.antsOffset + 1) % 1e3;
|
|
486
|
+
this.drawOverlay();
|
|
487
|
+
}, 100);
|
|
488
|
+
else if (!run && this.antsTimer) {
|
|
489
|
+
clearInterval(this.antsTimer);
|
|
490
|
+
this.antsTimer = void 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Handle a lasso tool press: start a shape, add a polygon node, or close a polygon on its first node.
|
|
495
|
+
*/
|
|
496
|
+
lassoDown(event, kind) {
|
|
497
|
+
const app = this.app;
|
|
498
|
+
const point = this.pointerPoint(event);
|
|
499
|
+
const first = app.lasso.nodes[0];
|
|
500
|
+
const nearFirstNode = first !== void 0 && isNear(app.viewport.canvasToScreen(first), point, 8);
|
|
501
|
+
if (kind !== "polygon") event.currentTarget.setPointerCapture(event.pointerId);
|
|
502
|
+
app.applyLasso(app.lasso.down(kind, app.viewport.screenToCanvas(point), event, nearFirstNode));
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Draw the last stroke's recorded points (circles) and stamp positions (dots) for tuning how brushes feel.
|
|
506
|
+
*/
|
|
507
|
+
drawDebugTrace(ctx) {
|
|
508
|
+
const app = this.app;
|
|
509
|
+
const ratio = this.pixelRatio();
|
|
510
|
+
const { points, stamps } = app.strokes.debugTrace;
|
|
511
|
+
ctx.save();
|
|
512
|
+
ctx.fillStyle = "#ffffff";
|
|
513
|
+
ctx.strokeStyle = "#ffffff";
|
|
514
|
+
ctx.lineWidth = ratio;
|
|
515
|
+
for (const stamp of stamps) {
|
|
516
|
+
const { x, y } = app.viewport.canvasToScreen(stamp);
|
|
517
|
+
ctx.fillRect(x * ratio - ratio, y * ratio - ratio, ratio * 2, ratio * 2);
|
|
518
|
+
}
|
|
519
|
+
ctx.beginPath();
|
|
520
|
+
for (const point of points) {
|
|
521
|
+
const { x, y } = app.viewport.canvasToScreen(point);
|
|
522
|
+
ctx.moveTo(x * ratio + 5 * ratio, y * ratio);
|
|
523
|
+
ctx.arc(x * ratio, y * ratio, 5 * ratio, 0, Math.PI * 2);
|
|
524
|
+
}
|
|
525
|
+
ctx.stroke();
|
|
526
|
+
ctx.restore();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* The brush or eraser settings for the tool in use, if it draws.
|
|
530
|
+
*/
|
|
531
|
+
brushSettings() {
|
|
532
|
+
const app = this.app;
|
|
533
|
+
if (app?.activeTool === Tool.Brush) return app.brush;
|
|
534
|
+
if (app?.activeTool === Tool.Eraser) return app.eraser;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Convert a pointer event to a stroke point in canvas pixels, with pen pressure and tilt.
|
|
538
|
+
*/
|
|
539
|
+
strokePoint(event) {
|
|
540
|
+
const point = this.app.viewport.screenToCanvas(this.pointerPoint(event));
|
|
541
|
+
if (event.pointerType === "pen") {
|
|
542
|
+
point.pressure = event.pressure;
|
|
543
|
+
point.tiltX = event.tiltX;
|
|
544
|
+
point.tiltY = event.tiltY;
|
|
545
|
+
}
|
|
546
|
+
return point;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Start a brush or eraser stroke on the active layer (cancelling any other stroke in progress), logging how long it
|
|
550
|
+
* took. Hidden layers cannot be drawn on.
|
|
551
|
+
*/
|
|
552
|
+
beginStroke(event, settings) {
|
|
553
|
+
const app = this.app;
|
|
554
|
+
const layer = app.layers.active;
|
|
555
|
+
if (!layer.visible) {
|
|
556
|
+
app.strokeLog.ignore(event.pointerType, "the layer is hidden");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (this.stroke) app.strokeLog.cancel(`a ${event.pointerType} started another stroke`);
|
|
560
|
+
const start = app.strokeLog.now();
|
|
561
|
+
this.stroke = {
|
|
562
|
+
pointerId: event.pointerId,
|
|
563
|
+
layerId: layer.id
|
|
564
|
+
};
|
|
565
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
566
|
+
app.strokes.begin({
|
|
567
|
+
layer,
|
|
568
|
+
mode: app.activeTool === Tool.Eraser ? "erase" : "paint",
|
|
569
|
+
preset: settings.preset,
|
|
570
|
+
size: {
|
|
571
|
+
width: settings.width,
|
|
572
|
+
height: settings.height
|
|
573
|
+
},
|
|
574
|
+
colour: app.colour,
|
|
575
|
+
tip: app.tips.get(settings.preset),
|
|
576
|
+
isPen: event.pointerType === "pen",
|
|
577
|
+
mask: app.selection.mask
|
|
578
|
+
}, this.strokePoint(event), event.timeStamp);
|
|
579
|
+
app.strokeLog.begin(event.pointerType, event.timeStamp, app.strokeLog.now() - start, {
|
|
580
|
+
layers: app.drawables.filter((l) => l.visible && l.opacity > 0).length,
|
|
581
|
+
zoomPct: app.viewport.zoom * 100
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Get a pointer's position relative to the stage.
|
|
586
|
+
*/
|
|
587
|
+
pointerPoint(event) {
|
|
588
|
+
const rect = this.getBoundingClientRect();
|
|
589
|
+
return {
|
|
590
|
+
x: event.clientX - rect.left,
|
|
591
|
+
y: event.clientY - rect.top
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Start panning, or zoom, depending on the tool and button. In focus pen mode, touches only pan, pinch and make
|
|
596
|
+
* gestures, and are ignored while a pen is down or just lifted (see `focusPenTouch`).
|
|
597
|
+
*/
|
|
598
|
+
onPointerDown(event) {
|
|
599
|
+
const app = this.app;
|
|
600
|
+
if (!app) return;
|
|
601
|
+
if (event.pointerType === "pen") this.penDown(event);
|
|
602
|
+
if (event.pointerType === "touch") {
|
|
603
|
+
const ignored = this.focusPenTouch(event);
|
|
604
|
+
if (ignored) {
|
|
605
|
+
app.strokeLog.ignore("touch", `focus pen mode: ${ignored}`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (this.touchDown(event)) {
|
|
609
|
+
if (this.brushSettings()) app.strokeLog.ignore("touch", `part of a ${this.touches.size}-finger gesture`);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (app.input.data.focusPen) {
|
|
613
|
+
this.navigationTouches.add(event.pointerId);
|
|
614
|
+
app.strokeLog.ignore("touch", "focus pen mode: one finger");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const point = this.pointerPoint(event);
|
|
619
|
+
const middleButton = event.button === 1;
|
|
620
|
+
const settings = this.brushSettings();
|
|
621
|
+
if (settings && event.button > 1) app.strokeLog.ignore(event.pointerType, `button ${event.button}`);
|
|
622
|
+
const lassoKind = LASSO_TOOLS[app.activeTool];
|
|
623
|
+
if (settings && event.button === 0) this.beginStroke(event, settings);
|
|
624
|
+
else if (lassoKind && event.button === 0) this.lassoDown(event, lassoKind);
|
|
625
|
+
else if (this.transformHandles() && event.button === 0) this.beginTransformDrag(event);
|
|
626
|
+
else if (app.activeTool === Tool.ColourSample && event.button === 0) this.beginSample(event);
|
|
627
|
+
else if (app.activeTool === Tool.Pan || middleButton) {
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
this.drag = {
|
|
630
|
+
pointerId: event.pointerId,
|
|
631
|
+
last: point
|
|
632
|
+
};
|
|
633
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
634
|
+
this.updateCursor();
|
|
635
|
+
} else if (app.activeTool === Tool.Zoom && event.button === 0) {
|
|
636
|
+
if (event.altKey) app.viewport.zoomOut(point);
|
|
637
|
+
else app.viewport.zoomIn(point);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Continue a stroke (using every in-between event the browser recorded), pan while dragging, and move the brush cursor.
|
|
642
|
+
*/
|
|
643
|
+
onPointerMove(event) {
|
|
644
|
+
const app = this.app;
|
|
645
|
+
if (!app || this.ignoredTouches.has(event.pointerId)) return;
|
|
646
|
+
const point = this.pointerPoint(event);
|
|
647
|
+
if (this.touches.has(event.pointerId)) {
|
|
648
|
+
this.touches.set(event.pointerId, point);
|
|
649
|
+
this.gestureTracker.move(event.pointerId, point);
|
|
650
|
+
if (this.pinch) {
|
|
651
|
+
if (this.gestureTracker.dragging) this.movePinch();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (this.navigationTouches.has(event.pointerId)) return;
|
|
655
|
+
}
|
|
656
|
+
this.hover = point;
|
|
657
|
+
this.reportPointer(point);
|
|
658
|
+
if (app.lasso.active) app.lasso.move(app.viewport.screenToCanvas(point), event);
|
|
659
|
+
if (this.sampling?.pointerId === event.pointerId) {
|
|
660
|
+
this.sampling.point = app.viewport.screenToCanvas(point);
|
|
661
|
+
this.sampling.screen = point;
|
|
662
|
+
}
|
|
663
|
+
if (this.transformDrag) this.continueTransformDrag(event);
|
|
664
|
+
else {
|
|
665
|
+
const cursor = this.transformCursor(point);
|
|
666
|
+
if (cursor) this.style.cursor = cursor;
|
|
667
|
+
}
|
|
668
|
+
if (this.stroke?.pointerId === event.pointerId) {
|
|
669
|
+
const start = app.strokeLog.now();
|
|
670
|
+
const events = event.getCoalescedEvents?.() ?? [];
|
|
671
|
+
for (const each of events.length > 0 ? events : [event]) app.strokes.move(this.strokePoint(each), each.timeStamp);
|
|
672
|
+
app.strokeLog.move(event.timeStamp, app.strokeLog.now() - start);
|
|
673
|
+
}
|
|
674
|
+
if (this.drag?.pointerId === event.pointerId) {
|
|
675
|
+
app.viewport.panBy(point.x - this.drag.last.x, point.y - this.drag.last.y);
|
|
676
|
+
this.drag.last = point;
|
|
677
|
+
}
|
|
678
|
+
this.requestOverlay();
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Finish a stroke (writing it into the layer) or stop panning.
|
|
682
|
+
*/
|
|
683
|
+
onPointerUp(event) {
|
|
684
|
+
if (this.ignoredTouches.delete(event.pointerId)) return;
|
|
685
|
+
this.penUp(event);
|
|
686
|
+
this.touchUp(event);
|
|
687
|
+
if (this.sampling?.pointerId === event.pointerId) this.finishSample();
|
|
688
|
+
if (event.pointerType === "touch") {
|
|
689
|
+
const gesture = this.gestureTracker.up(event.pointerId, event.timeStamp);
|
|
690
|
+
if (gesture) this.runGesture(gesture);
|
|
691
|
+
}
|
|
692
|
+
if (this.navigationTouches.delete(event.pointerId)) return;
|
|
693
|
+
const app = this.app;
|
|
694
|
+
this.endTransformDrag(event.pointerId);
|
|
695
|
+
if (app?.lasso.active) app.applyLasso(app.lasso.up(app.viewport.screenToCanvas(this.pointerPoint(event)), event));
|
|
696
|
+
if (app && this.stroke?.pointerId === event.pointerId) {
|
|
697
|
+
const { layerId } = this.stroke;
|
|
698
|
+
const start = app.strokeLog.now();
|
|
699
|
+
this.stroke = null;
|
|
700
|
+
app.strokes.end(this.strokePoint(event), event.timeStamp);
|
|
701
|
+
this.committing = true;
|
|
702
|
+
app.layers.markChanged(layerId);
|
|
703
|
+
this.committing = false;
|
|
704
|
+
app.strokeLog.end(event.timeStamp, app.strokeLog.now() - start);
|
|
705
|
+
}
|
|
706
|
+
if (this.drag?.pointerId !== event.pointerId) return;
|
|
707
|
+
this.drag = null;
|
|
708
|
+
this.updateCursor();
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Abandon a stroke or pan when the browser cancels the pointer (e.g. a system gesture).
|
|
712
|
+
*/
|
|
713
|
+
onPointerCancel(event) {
|
|
714
|
+
if (this.ignoredTouches.delete(event.pointerId)) return;
|
|
715
|
+
this.navigationTouches.delete(event.pointerId);
|
|
716
|
+
this.penUp(event);
|
|
717
|
+
this.touchUp(event);
|
|
718
|
+
this.gestureTracker.cancel(event.pointerId);
|
|
719
|
+
if (this.sampling?.pointerId === event.pointerId) this.endSample();
|
|
720
|
+
this.endTransformDrag(event.pointerId);
|
|
721
|
+
if (this.stroke?.pointerId === event.pointerId) {
|
|
722
|
+
this.stroke = null;
|
|
723
|
+
this.app?.strokeLog.cancel("the browser cancelled the pointer");
|
|
724
|
+
this.app?.strokes.cancel();
|
|
725
|
+
}
|
|
726
|
+
if (this.drag?.pointerId !== event.pointerId) return;
|
|
727
|
+
this.drag = null;
|
|
728
|
+
this.updateCursor();
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Keep the browser from handling touches on the canvas itself while the brush or eraser is in use (e.g. Safari on
|
|
732
|
+
* iPad holding back quick Apple Pencil strokes while it checks for its own gestures). Other tools keep the browser's
|
|
733
|
+
* handling, so a double tap still closes a polygon lasso. Pointer events are unaffected.
|
|
734
|
+
*/
|
|
735
|
+
onTouchStart = {
|
|
736
|
+
handleEvent: (event) => {
|
|
737
|
+
if (this.brushSettings()) event.preventDefault();
|
|
738
|
+
},
|
|
739
|
+
passive: false
|
|
740
|
+
};
|
|
741
|
+
/**
|
|
742
|
+
* Note a pen touching down. In focus pen mode, touches already on the stage are ignored from now on, abandoning any
|
|
743
|
+
* pan, pinch or gesture they were making (e.g. a palm resting on the screen).
|
|
744
|
+
*/
|
|
745
|
+
penDown(event) {
|
|
746
|
+
this.pens.add(event.pointerId);
|
|
747
|
+
if (!this.app?.input.data.focusPen) return;
|
|
748
|
+
for (const id of this.touches.keys()) {
|
|
749
|
+
this.gestureTracker.cancel(id);
|
|
750
|
+
this.navigationTouches.delete(id);
|
|
751
|
+
this.ignoredTouches.add(id);
|
|
752
|
+
}
|
|
753
|
+
this.touches.clear();
|
|
754
|
+
this.pinch = null;
|
|
755
|
+
}
|
|
756
|
+
/** Note a pen lifting (or being cancelled), and when */
|
|
757
|
+
penUp(event) {
|
|
758
|
+
if (this.pens.delete(event.pointerId)) this.penLiftedAt = event.timeStamp;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* In focus pen mode, why a touch is ignored altogether: a pen is down, or lifted less than `PEN_LIFT_GRACE_MS` ago
|
|
762
|
+
* (a hand landing as the pen lifts). Undefined if the touch counts.
|
|
763
|
+
*/
|
|
764
|
+
focusPenTouch(event) {
|
|
765
|
+
if (!this.app?.input.data.focusPen) return void 0;
|
|
766
|
+
let reason;
|
|
767
|
+
if (this.pens.size > 0) reason = "the pen is down";
|
|
768
|
+
else if (event.timeStamp - this.penLiftedAt < 500) reason = "the pen just lifted";
|
|
769
|
+
if (reason) this.ignoredTouches.add(event.pointerId);
|
|
770
|
+
return reason;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Note a touch. A second touch stops any stroke or pan and starts a two-finger gesture; a third stops two-finger pan
|
|
774
|
+
* and pinch, for three-finger gestures. Returns whether the touch is part of a gesture (and so does nothing else).
|
|
775
|
+
*/
|
|
776
|
+
touchDown(event) {
|
|
777
|
+
const point = this.pointerPoint(event);
|
|
778
|
+
this.touches.set(event.pointerId, point);
|
|
779
|
+
this.gestureTracker.down(event.pointerId, point, event.timeStamp);
|
|
780
|
+
if (this.touches.size < 2) return false;
|
|
781
|
+
if (this.touches.size > 2) this.pinch = null;
|
|
782
|
+
if (this.touches.size === 2) {
|
|
783
|
+
if (this.stroke) {
|
|
784
|
+
this.stroke = null;
|
|
785
|
+
this.app?.strokeLog.cancel("a second finger touched");
|
|
786
|
+
this.app?.strokes.cancel();
|
|
787
|
+
}
|
|
788
|
+
if (this.sampling) this.endSample();
|
|
789
|
+
this.drag = null;
|
|
790
|
+
const [a, b] = [...this.touches.values()];
|
|
791
|
+
this.pinch = touchPair(a, b);
|
|
792
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Start sampling a colour with the colour sample tool: the pointer is hidden while the sampling window shows.
|
|
798
|
+
*/
|
|
799
|
+
beginSample(event) {
|
|
800
|
+
const app = this.app;
|
|
801
|
+
const screen = this.pointerPoint(event);
|
|
802
|
+
const point = app.viewport.screenToCanvas(screen);
|
|
803
|
+
this.sampling = {
|
|
804
|
+
pointerId: event.pointerId,
|
|
805
|
+
touch: event.pointerType === "touch",
|
|
806
|
+
start: point,
|
|
807
|
+
point,
|
|
808
|
+
screen
|
|
809
|
+
};
|
|
810
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
811
|
+
this.updateCursor();
|
|
812
|
+
this.drawOverlay();
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Set the current colour from the sample: the pixel under the pointer, or the average of every pixel in the area.
|
|
816
|
+
* Nothing changes if it's outside the canvas.
|
|
817
|
+
*/
|
|
818
|
+
finishSample() {
|
|
819
|
+
const app = this.app;
|
|
820
|
+
const { start, point } = this.sampling;
|
|
821
|
+
const colour = app.sampleMode === "area" ? app.sampleAreaColour(start, point) : app.sampleColour(point);
|
|
822
|
+
this.endSample();
|
|
823
|
+
if (colour) app.setColour(colour);
|
|
824
|
+
}
|
|
825
|
+
/** Stop sampling and close the sampling window */
|
|
826
|
+
endSample() {
|
|
827
|
+
this.sampling = null;
|
|
828
|
+
this.updateCursor();
|
|
829
|
+
this.drawOverlay();
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Draw the colour sample tool's window while sampling (see `samplerPosition`): the sampled colour on top (the area's
|
|
833
|
+
* average while dragging an area, worked out from at most `PREVIEW_MAX_PIXELS` across), and the pixels around the
|
|
834
|
+
* pointer enlarged below, with the sampled pixel outlined. An area being sampled is outlined on the canvas.
|
|
835
|
+
*/
|
|
836
|
+
drawSampler() {
|
|
837
|
+
const app = this.app;
|
|
838
|
+
const ctx = this.sampler?.getContext("2d");
|
|
839
|
+
if (!ctx) return;
|
|
840
|
+
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
841
|
+
const sampling = this.sampling;
|
|
842
|
+
if (!sampling) return;
|
|
843
|
+
const ratio = this.pixelRatio();
|
|
844
|
+
ctx.save();
|
|
845
|
+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
846
|
+
let colour;
|
|
847
|
+
if (app.sampleMode === "area") {
|
|
848
|
+
const a = app.viewport.canvasToScreen(sampling.start);
|
|
849
|
+
const b = sampling.screen;
|
|
850
|
+
const [x, y, w, h] = [
|
|
851
|
+
Math.min(a.x, b.x),
|
|
852
|
+
Math.min(a.y, b.y),
|
|
853
|
+
Math.abs(b.x - a.x),
|
|
854
|
+
Math.abs(b.y - a.y)
|
|
855
|
+
];
|
|
856
|
+
ctx.lineWidth = 1;
|
|
857
|
+
ctx.strokeStyle = "#ffffff";
|
|
858
|
+
ctx.setLineDash([]);
|
|
859
|
+
ctx.strokeRect(x, y, w, h);
|
|
860
|
+
ctx.strokeStyle = "#000000";
|
|
861
|
+
ctx.setLineDash([4, 4]);
|
|
862
|
+
ctx.strokeRect(x, y, w, h);
|
|
863
|
+
ctx.setLineDash([]);
|
|
864
|
+
colour = app.sampleAreaColour(sampling.start, sampling.point, 100);
|
|
865
|
+
} else colour = app.sampleColour(sampling.point);
|
|
866
|
+
const { x, y } = samplerPosition(sampling.screen, sampling.touch, this.clientWidth, this.clientHeight);
|
|
867
|
+
ctx.fillStyle = colour ?? "#808080";
|
|
868
|
+
ctx.fillRect(x, y, SAMPLER_WIDTH, SAMPLER_WIDTH);
|
|
869
|
+
ctx.fillStyle = PASTEBOARD_COLOUR;
|
|
870
|
+
ctx.fillRect(x, y + SAMPLER_WIDTH, SAMPLER_WIDTH, SAMPLER_WIDTH);
|
|
871
|
+
ctx.imageSmoothingEnabled = false;
|
|
872
|
+
ctx.drawImage(app.samplePixels(sampling.point), x, y + SAMPLER_WIDTH, SAMPLER_WIDTH, SAMPLER_WIDTH);
|
|
873
|
+
const pixel = 25;
|
|
874
|
+
ctx.lineWidth = 1;
|
|
875
|
+
ctx.strokeStyle = "#000000";
|
|
876
|
+
ctx.strokeRect(x + pixel - .5, y + SAMPLER_WIDTH + pixel - .5, 6, 6);
|
|
877
|
+
ctx.strokeStyle = "#ffffff";
|
|
878
|
+
ctx.strokeRect(x + pixel - 1.5, y + SAMPLER_WIDTH + pixel - 1.5, 8, 8);
|
|
879
|
+
ctx.strokeStyle = "#000000";
|
|
880
|
+
ctx.strokeRect(x - .5, y - .5, SAMPLER_WIDTH + 1, SAMPLER_HEIGHT + 1);
|
|
881
|
+
ctx.restore();
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Do what the user's touch gesture settings say a gesture does: undo, redo, copy or paste.
|
|
885
|
+
*/
|
|
886
|
+
runGesture(gesture) {
|
|
887
|
+
const app = this.app;
|
|
888
|
+
const action = gestureAction(app.input.data.gestures, gesture);
|
|
889
|
+
if (action) app.strokeLog.gesture(gesture, action);
|
|
890
|
+
if (action === "undo") app.undo();
|
|
891
|
+
else if (action === "redo") app.redo();
|
|
892
|
+
else if (action === "copy") app.copy();
|
|
893
|
+
else if (action === "paste") app.paste();
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Pan by how far the two touches' midpoint moved and zoom by how much their spread changed, around the midpoint.
|
|
897
|
+
*/
|
|
898
|
+
movePinch() {
|
|
899
|
+
const app = this.app;
|
|
900
|
+
const pinch = this.pinch;
|
|
901
|
+
const [a, b] = [...this.touches.values()];
|
|
902
|
+
const next = touchPair(a, b);
|
|
903
|
+
const { scrollPan, pinchZoom } = app.input.data;
|
|
904
|
+
if (scrollPan) app.viewport.panBy(next.mid.x - pinch.mid.x, next.mid.y - pinch.mid.y);
|
|
905
|
+
if (pinchZoom && pinch.distance > 0) app.viewport.zoomAt(next.mid, app.viewport.zoom * next.distance / pinch.distance);
|
|
906
|
+
this.pinch = next;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Forget a lifted touch; the gesture ends once fewer than two touches are left.
|
|
910
|
+
*/
|
|
911
|
+
touchUp(event) {
|
|
912
|
+
this.touches.delete(event.pointerId);
|
|
913
|
+
if (this.touches.size < 2) this.pinch = null;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Pan with the wheel or two-finger scroll, and zoom around the pointer with a pinch or Cmd/Ctrl + scroll (see
|
|
917
|
+
* `wheelAction`). The page itself never scrolls or zooms.
|
|
918
|
+
*/
|
|
919
|
+
onWheel(event) {
|
|
920
|
+
event.preventDefault();
|
|
921
|
+
const app = this.app;
|
|
922
|
+
if (!app) return;
|
|
923
|
+
const action = wheelAction(event, app.input.data, this.modifier);
|
|
924
|
+
if (action?.kind === "pan") app.viewport.panBy(action.dx, action.dy);
|
|
925
|
+
else if (action?.kind === "zoom") app.viewport.zoomAt(this.pointerPoint(event), app.viewport.zoom * action.factor);
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Start a Safari trackpad pinch (Safari reports these as gesture events, not Ctrl + wheel). Touch screens use the
|
|
929
|
+
* pointer events instead.
|
|
930
|
+
*/
|
|
931
|
+
onGestureStart(event) {
|
|
932
|
+
event.preventDefault();
|
|
933
|
+
this.gestureZoom = this.app?.viewport.zoom ?? 1;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Zoom with a Safari trackpad pinch, around the pointer.
|
|
937
|
+
*/
|
|
938
|
+
onGestureChange(event) {
|
|
939
|
+
event.preventDefault();
|
|
940
|
+
const app = this.app;
|
|
941
|
+
if (!app || this.touches.size > 0 || !app.input.data.pinchZoom) return;
|
|
942
|
+
const gesture = event;
|
|
943
|
+
app.viewport.zoomAt(this.pointerPoint(gesture), this.gestureZoom * gesture.scale);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Hide the brush cursor (and the user's shared cursor) when the pointer leaves the stage.
|
|
947
|
+
*/
|
|
948
|
+
onPointerLeave() {
|
|
949
|
+
this.hover = null;
|
|
950
|
+
this.app?.setPointer(void 0);
|
|
951
|
+
this.drawOverlay();
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Tell the app where the pointer is on the canvas, in whole canvas pixels (undefined when off the canvas), so it can be
|
|
955
|
+
* shared with others in a session.
|
|
956
|
+
*/
|
|
957
|
+
reportPointer(point) {
|
|
958
|
+
const app = this.app;
|
|
959
|
+
const { x, y } = app.viewport.screenToCanvas(point);
|
|
960
|
+
const inside = x >= 0 && y >= 0 && x <= app.width && y <= app.height;
|
|
961
|
+
app.setPointer(inside ? {
|
|
962
|
+
x: Math.floor(x),
|
|
963
|
+
y: Math.floor(y)
|
|
964
|
+
} : void 0);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Show the cursor for the tool in use.
|
|
968
|
+
*/
|
|
969
|
+
updateCursor() {
|
|
970
|
+
const app = this.app;
|
|
971
|
+
if (!app) return;
|
|
972
|
+
if (this.sampling) {
|
|
973
|
+
this.style.cursor = "none";
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
this.style.cursor = cursorFor(app.activeTool, {
|
|
977
|
+
dragging: this.drag !== null,
|
|
978
|
+
altKey: this.altKey
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
render() {
|
|
982
|
+
return html`<canvas
|
|
983
|
+
class="scene"
|
|
984
|
+
aria-label=${this.t("canvas.label")}
|
|
985
|
+
@pointerdown=${this.onPointerDown}
|
|
986
|
+
@pointermove=${this.onPointerMove}
|
|
987
|
+
@pointerup=${this.onPointerUp}
|
|
988
|
+
@pointercancel=${this.onPointerCancel}
|
|
989
|
+
@touchstart=${this.onTouchStart}
|
|
990
|
+
@pointerleave=${this.onPointerLeave}
|
|
991
|
+
@wheel=${this.onWheel}
|
|
992
|
+
@gesturestart=${this.onGestureStart}
|
|
993
|
+
@gesturechange=${this.onGestureChange}
|
|
994
|
+
@gestureend=${(e) => e.preventDefault()}
|
|
995
|
+
@dblclick=${() => this.app?.applyLasso(this.app.lasso.close())}
|
|
996
|
+
@contextmenu=${(e) => e.preventDefault()}
|
|
997
|
+
></canvas>
|
|
998
|
+
<canvas class="outlines" aria-hidden="true"></canvas>
|
|
999
|
+
<canvas class="overlay" aria-hidden="true"></canvas>
|
|
1000
|
+
<canvas class="sampler" aria-hidden="true"></canvas>`;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
defineElement("pc-canvas", PcCanvas);
|
|
1004
|
+
export { ANTS_INTERVAL, CHAT_WIDTH_RATIO, HANDLE_GRAB_DISTANCE, LASSO_TOOLS, PEN_LIFT_GRACE_MS, PcCanvas, SAMPLER_HEIGHT, SAMPLER_WIDTH, TOOLBAR_WIDTH, TOUCH_SAMPLER_GAP, cursorFor, interfaceInsets, samplerPosition };
|