@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,181 @@
|
|
|
1
|
+
// src/lib/presets/schema.ts
|
|
2
|
+
/** Matches a lowercase UUID */
|
|
3
|
+
var UUID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
4
|
+
/** The `format` of a brush preset file, unless the implementer names their own (see `PaintCanvasNames`) */
|
|
5
|
+
var BRUSH_PRESET_FILE_FORMAT = "paintcanvas-brush-presets";
|
|
6
|
+
/** Formats of brush preset files that are imported by default */
|
|
7
|
+
var BRUSH_PRESET_FILE_FORMATS = [BRUSH_PRESET_FILE_FORMAT];
|
|
8
|
+
/** Longest brush preset file format name (characters) */
|
|
9
|
+
var MAX_PRESET_FILE_FORMAT_LENGTH = 100;
|
|
10
|
+
/** The version of the brush preset file format */
|
|
11
|
+
var BRUSH_PRESET_FILE_VERSION = 1;
|
|
12
|
+
/** Most presets in one file */
|
|
13
|
+
var MAX_PRESETS_PER_FILE = 1e3;
|
|
14
|
+
/** Longest preset name, author name or handle (characters) */
|
|
15
|
+
var MAX_PRESET_TEXT_LENGTH = 100;
|
|
16
|
+
/** Longest brush shape data URL (characters): a 2 MB PNG as base64 */
|
|
17
|
+
var MAX_SHAPE_DATA_URL_LENGTH = Math.ceil(2097152 / 3) * 4 + 22;
|
|
18
|
+
/** Matches a PNG data URL */
|
|
19
|
+
var PNG_DATA_URL_PATTERN = "^data:image/png;base64,[A-Za-z0-9+/]+={0,2}$";
|
|
20
|
+
/** A 0–1 factor */
|
|
21
|
+
var factor = {
|
|
22
|
+
type: "number",
|
|
23
|
+
minimum: 0,
|
|
24
|
+
maximum: 1
|
|
25
|
+
};
|
|
26
|
+
/** How a brush parameter varies per stamp (see `BrushParamConfig` in the app) */
|
|
27
|
+
var paramConfig = {
|
|
28
|
+
type: "object",
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
useRandomise: { type: "boolean" },
|
|
32
|
+
minRandomise: factor,
|
|
33
|
+
maxRandomise: factor,
|
|
34
|
+
usePressure: { type: "boolean" },
|
|
35
|
+
minPressure: factor,
|
|
36
|
+
maxPressure: factor,
|
|
37
|
+
useTilt: { type: "boolean" },
|
|
38
|
+
minTilt: factor,
|
|
39
|
+
maxTilt: factor
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** A colour value: a number, or a number as text */
|
|
43
|
+
var colourValue = { anyOf: [{ type: "number" }, {
|
|
44
|
+
type: "string",
|
|
45
|
+
maxLength: 20
|
|
46
|
+
}] };
|
|
47
|
+
/** Short text, such as a name */
|
|
48
|
+
var text = {
|
|
49
|
+
type: "string",
|
|
50
|
+
minLength: 1,
|
|
51
|
+
maxLength: 100
|
|
52
|
+
};
|
|
53
|
+
/** JSON schema for a brush preset a user made or imported (see `BrushConfig` in the app) */
|
|
54
|
+
var brushPresetSchema = {
|
|
55
|
+
$id: "paintcanvas/brush-preset",
|
|
56
|
+
type: "object",
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
required: ["id"],
|
|
59
|
+
properties: {
|
|
60
|
+
id: {
|
|
61
|
+
type: "string",
|
|
62
|
+
pattern: UUID_PATTERN
|
|
63
|
+
},
|
|
64
|
+
name: text,
|
|
65
|
+
group: text,
|
|
66
|
+
version: {
|
|
67
|
+
type: "string",
|
|
68
|
+
maxLength: 20
|
|
69
|
+
},
|
|
70
|
+
author: { anyOf: [text, {
|
|
71
|
+
type: "object",
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
required: ["name"],
|
|
74
|
+
properties: {
|
|
75
|
+
name: text,
|
|
76
|
+
handle: text,
|
|
77
|
+
email: {
|
|
78
|
+
type: "string",
|
|
79
|
+
maxLength: 254
|
|
80
|
+
},
|
|
81
|
+
website: {
|
|
82
|
+
type: "string",
|
|
83
|
+
maxLength: 2048
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}] },
|
|
87
|
+
image: {
|
|
88
|
+
type: "string",
|
|
89
|
+
pattern: PNG_DATA_URL_PATTERN,
|
|
90
|
+
maxLength: MAX_SHAPE_DATA_URL_LENGTH
|
|
91
|
+
},
|
|
92
|
+
colourMode: { enum: [0, 1] },
|
|
93
|
+
hue: colourValue,
|
|
94
|
+
hueConfig: paramConfig,
|
|
95
|
+
saturation: colourValue,
|
|
96
|
+
saturationConfig: paramConfig,
|
|
97
|
+
lightness: colourValue,
|
|
98
|
+
lightnessConfig: paramConfig,
|
|
99
|
+
red: colourValue,
|
|
100
|
+
redConfig: paramConfig,
|
|
101
|
+
green: colourValue,
|
|
102
|
+
greenConfig: paramConfig,
|
|
103
|
+
blue: colourValue,
|
|
104
|
+
blueConfig: paramConfig,
|
|
105
|
+
alpha: factor,
|
|
106
|
+
alphaConfig: paramConfig,
|
|
107
|
+
width: {
|
|
108
|
+
type: "number",
|
|
109
|
+
minimum: 1,
|
|
110
|
+
maximum: 256
|
|
111
|
+
},
|
|
112
|
+
widthConfig: paramConfig,
|
|
113
|
+
height: {
|
|
114
|
+
type: "number",
|
|
115
|
+
minimum: 1,
|
|
116
|
+
maximum: 256
|
|
117
|
+
},
|
|
118
|
+
heightConfig: paramConfig,
|
|
119
|
+
constrainProportions: { type: "boolean" },
|
|
120
|
+
rotation: {
|
|
121
|
+
type: "number",
|
|
122
|
+
minimum: -360,
|
|
123
|
+
maximum: 360
|
|
124
|
+
},
|
|
125
|
+
rotationConfig: paramConfig,
|
|
126
|
+
offsetX: {
|
|
127
|
+
type: "number",
|
|
128
|
+
minimum: -256,
|
|
129
|
+
maximum: 256
|
|
130
|
+
},
|
|
131
|
+
offsetXConfig: paramConfig,
|
|
132
|
+
offsetY: {
|
|
133
|
+
type: "number",
|
|
134
|
+
minimum: -256,
|
|
135
|
+
maximum: 256
|
|
136
|
+
},
|
|
137
|
+
offsetYConfig: paramConfig,
|
|
138
|
+
sampleRate: {
|
|
139
|
+
type: "number",
|
|
140
|
+
minimum: 5,
|
|
141
|
+
maximum: 400
|
|
142
|
+
},
|
|
143
|
+
smoothingType: { enum: [
|
|
144
|
+
0,
|
|
145
|
+
1,
|
|
146
|
+
2,
|
|
147
|
+
3
|
|
148
|
+
] },
|
|
149
|
+
spacing: {
|
|
150
|
+
type: "number",
|
|
151
|
+
minimum: 0,
|
|
152
|
+
maximum: 200
|
|
153
|
+
},
|
|
154
|
+
snapToPixelGrid: { type: "boolean" }
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/** JSON schema for a brush preset file */
|
|
158
|
+
var brushPresetFileSchema = {
|
|
159
|
+
$id: "paintcanvas/brush-preset-file",
|
|
160
|
+
type: "object",
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
required: [
|
|
163
|
+
"format",
|
|
164
|
+
"version",
|
|
165
|
+
"presets"
|
|
166
|
+
],
|
|
167
|
+
properties: {
|
|
168
|
+
format: {
|
|
169
|
+
type: "string",
|
|
170
|
+
minLength: 1,
|
|
171
|
+
maxLength: 100
|
|
172
|
+
},
|
|
173
|
+
version: { const: 1 },
|
|
174
|
+
presets: {
|
|
175
|
+
type: "array",
|
|
176
|
+
maxItems: MAX_PRESETS_PER_FILE,
|
|
177
|
+
items: { $ref: "brush-preset" }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
export { BRUSH_PRESET_FILE_FORMAT, BRUSH_PRESET_FILE_FORMATS, BRUSH_PRESET_FILE_VERSION, MAX_PRESETS_PER_FILE, MAX_PRESET_FILE_FORMAT_LENGTH, MAX_PRESET_TEXT_LENGTH, MAX_SHAPE_DATA_URL_LENGTH, PNG_DATA_URL_PATTERN, UUID_PATTERN, brushPresetFileSchema, brushPresetSchema };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BrushConfig } from "../../brushes/types";
|
|
2
|
+
import type { StampParams } from "../../brushes/params";
|
|
3
|
+
import { type CanvasFactory } from "../../classes/Layer";
|
|
4
|
+
import { type OutlineSegment } from "../outline";
|
|
5
|
+
/** The drawing operations a stamp needs */
|
|
6
|
+
export type StampContext = Pick<OffscreenCanvasRenderingContext2D, "save" | "restore" | "translate" | "rotate" | "beginPath" | "ellipse" | "fill" | "drawImage" | "fillStyle" | "globalAlpha">;
|
|
7
|
+
/** A decoded image usable as a brush shape */
|
|
8
|
+
export type TipImage = CanvasImageSource & {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
/** Turns a preset's `image` (data URL or bytes) into a decoded image; replaceable for tests */
|
|
13
|
+
export type ImageDecoder = (image: string | Uint8Array) => Promise<TipImage>;
|
|
14
|
+
/**
|
|
15
|
+
* The bytes and media type in a `data:` URL, read without `fetch()` (which the site's Content Security Policy doesn't
|
|
16
|
+
* allow for `data:` URLs). Throws for anything that isn't a `data:` URL.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* dataUrlBytes("data:image/png;base64,iVBO..."); // { type: "image/png", bytes: Uint8Array [137, 80, 78, 71, ...] }
|
|
20
|
+
* dataUrlBytes("data:text/plain,a%20b"); // { type: "text/plain", bytes: Uint8Array [97, 32, 98] }
|
|
21
|
+
*/
|
|
22
|
+
export declare const dataUrlBytes: (url: string) => {
|
|
23
|
+
type: string;
|
|
24
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Decode a data URL or image file bytes using the browser's image decoder.
|
|
28
|
+
*/
|
|
29
|
+
export declare const decodeImage: ImageDecoder;
|
|
30
|
+
/**
|
|
31
|
+
* A brush shape that can be stamped in any colour, size and rotation.
|
|
32
|
+
*
|
|
33
|
+
* Presets without an image use a hard round shape drawn as a vector ellipse, so it stays crisp at every size.
|
|
34
|
+
* Presets with an image use only its alpha values; tinted copies are cached per colour.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const tip = await BrushTip.load(preset);
|
|
38
|
+
* tip.stamp(ctx, 100, 100, params, "#ff0000");
|
|
39
|
+
*/
|
|
40
|
+
export default class BrushTip {
|
|
41
|
+
/** Alpha mask of the shape image, or undefined for the round shape */
|
|
42
|
+
readonly mask?: OffscreenCanvas;
|
|
43
|
+
/** Outline of the shape in 0–1 coordinates (for the brush cursor), or undefined for the round shape */
|
|
44
|
+
readonly outline?: OutlineSegment[];
|
|
45
|
+
private readonly createCanvas;
|
|
46
|
+
private tintedColour;
|
|
47
|
+
private tinted?;
|
|
48
|
+
constructor(mask?: OffscreenCanvas, outline?: OutlineSegment[], createCanvas?: CanvasFactory);
|
|
49
|
+
/** Whether this is the built-in round shape */
|
|
50
|
+
get isRound(): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Load a preset's shape: decodes its image and keeps only the alpha. Presets without an image get the round shape.
|
|
53
|
+
*/
|
|
54
|
+
static load(preset: BrushConfig, decode?: ImageDecoder, createCanvas?: CanvasFactory): Promise<BrushTip>;
|
|
55
|
+
/**
|
|
56
|
+
* The shape image tinted with a colour (cached until the colour changes).
|
|
57
|
+
*/
|
|
58
|
+
private tint;
|
|
59
|
+
/**
|
|
60
|
+
* Stamp the shape centred on a point, with the stamp's size, rotation, offset and alpha.
|
|
61
|
+
*/
|
|
62
|
+
stamp(ctx: StampContext, x: number, y: number, params: StampParams, colour: string): void;
|
|
63
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createOffscreenCanvas } from "../../classes/Layer/index.js";
|
|
2
|
+
import { traceOutline } from "../outline/index.js";
|
|
3
|
+
// src/lib/rendering/BrushTip/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* The bytes and media type in a `data:` URL, read without `fetch()` (which the site's Content Security Policy doesn't
|
|
6
|
+
* allow for `data:` URLs). Throws for anything that isn't a `data:` URL.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* dataUrlBytes("data:image/png;base64,iVBO..."); // { type: "image/png", bytes: Uint8Array [137, 80, 78, 71, ...] }
|
|
10
|
+
* dataUrlBytes("data:text/plain,a%20b"); // { type: "text/plain", bytes: Uint8Array [97, 32, 98] }
|
|
11
|
+
*/
|
|
12
|
+
var dataUrlBytes = (url) => {
|
|
13
|
+
const match = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(url);
|
|
14
|
+
if (!match) throw new Error("Brush shapes must be data URLs");
|
|
15
|
+
const [, type = "", base64, data = ""] = match;
|
|
16
|
+
const text = base64 ? atob(data) : decodeURIComponent(data);
|
|
17
|
+
return {
|
|
18
|
+
type,
|
|
19
|
+
bytes: base64 ? Uint8Array.from(text, (char) => char.charCodeAt(0)) : new TextEncoder().encode(text)
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Decode a data URL or image file bytes using the browser's image decoder.
|
|
24
|
+
*/
|
|
25
|
+
var decodeImage = async (image) => {
|
|
26
|
+
if (typeof image === "string") {
|
|
27
|
+
const { type, bytes } = dataUrlBytes(image);
|
|
28
|
+
return createImageBitmap(new Blob([bytes], { type }));
|
|
29
|
+
}
|
|
30
|
+
return createImageBitmap(new Blob([image]));
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A brush shape that can be stamped in any colour, size and rotation.
|
|
34
|
+
*
|
|
35
|
+
* Presets without an image use a hard round shape drawn as a vector ellipse, so it stays crisp at every size.
|
|
36
|
+
* Presets with an image use only its alpha values; tinted copies are cached per colour.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const tip = await BrushTip.load(preset);
|
|
40
|
+
* tip.stamp(ctx, 100, 100, params, "#ff0000");
|
|
41
|
+
*/
|
|
42
|
+
var BrushTip = class BrushTip {
|
|
43
|
+
/** Alpha mask of the shape image, or undefined for the round shape */
|
|
44
|
+
mask;
|
|
45
|
+
/** Outline of the shape in 0–1 coordinates (for the brush cursor), or undefined for the round shape */
|
|
46
|
+
outline;
|
|
47
|
+
createCanvas;
|
|
48
|
+
tintedColour = "";
|
|
49
|
+
tinted;
|
|
50
|
+
constructor(mask, outline, createCanvas = createOffscreenCanvas) {
|
|
51
|
+
this.mask = mask;
|
|
52
|
+
this.outline = outline;
|
|
53
|
+
this.createCanvas = createCanvas;
|
|
54
|
+
}
|
|
55
|
+
/** Whether this is the built-in round shape */
|
|
56
|
+
get isRound() {
|
|
57
|
+
return this.mask === void 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load a preset's shape: decodes its image and keeps only the alpha. Presets without an image get the round shape.
|
|
61
|
+
*/
|
|
62
|
+
static async load(preset, decode = decodeImage, createCanvas = createOffscreenCanvas) {
|
|
63
|
+
if (!preset.image) return new BrushTip(void 0, void 0, createCanvas);
|
|
64
|
+
const image = await decode(preset.image);
|
|
65
|
+
const mask = createCanvas(image.width, image.height);
|
|
66
|
+
const ctx = mask.getContext("2d");
|
|
67
|
+
ctx.drawImage(image, 0, 0);
|
|
68
|
+
ctx.globalCompositeOperation = "source-in";
|
|
69
|
+
ctx.fillStyle = "#000000";
|
|
70
|
+
ctx.fillRect(0, 0, image.width, image.height);
|
|
71
|
+
const pixels = ctx.getImageData(0, 0, image.width, image.height).data;
|
|
72
|
+
const alpha = new Uint8ClampedArray(image.width * image.height);
|
|
73
|
+
for (let i = 0; i < alpha.length; i++) alpha[i] = pixels[i * 4 + 3];
|
|
74
|
+
return new BrushTip(mask, traceOutline(alpha, image.width, image.height), createCanvas);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The shape image tinted with a colour (cached until the colour changes).
|
|
78
|
+
*/
|
|
79
|
+
tint(mask, colour) {
|
|
80
|
+
if (this.tinted && this.tintedColour === colour) return this.tinted;
|
|
81
|
+
const canvas = this.tinted ?? this.createCanvas(mask.width, mask.height);
|
|
82
|
+
const ctx = canvas.getContext("2d");
|
|
83
|
+
ctx.globalCompositeOperation = "copy";
|
|
84
|
+
ctx.drawImage(mask, 0, 0);
|
|
85
|
+
ctx.globalCompositeOperation = "source-in";
|
|
86
|
+
ctx.fillStyle = colour;
|
|
87
|
+
ctx.fillRect(0, 0, mask.width, mask.height);
|
|
88
|
+
ctx.globalCompositeOperation = "source-over";
|
|
89
|
+
this.tinted = canvas;
|
|
90
|
+
this.tintedColour = colour;
|
|
91
|
+
return canvas;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Stamp the shape centred on a point, with the stamp's size, rotation, offset and alpha.
|
|
95
|
+
*/
|
|
96
|
+
stamp(ctx, x, y, params, colour) {
|
|
97
|
+
if (params.width <= 0 || params.height <= 0 || params.alpha <= 0) return;
|
|
98
|
+
ctx.save();
|
|
99
|
+
ctx.globalAlpha = params.alpha;
|
|
100
|
+
ctx.translate(x + params.offsetX, y + params.offsetY);
|
|
101
|
+
if (params.rotation) ctx.rotate(params.rotation * Math.PI / 180);
|
|
102
|
+
if (this.mask) ctx.drawImage(this.tint(this.mask, colour), -params.width / 2, -params.height / 2, params.width, params.height);
|
|
103
|
+
else {
|
|
104
|
+
ctx.fillStyle = colour;
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.ellipse(0, 0, params.width / 2, params.height / 2, 0, 0, Math.PI * 2);
|
|
107
|
+
ctx.fill();
|
|
108
|
+
}
|
|
109
|
+
ctx.restore();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
export { dataUrlBytes, decodeImage, BrushTip as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DrawingContext } from "../compositing";
|
|
2
|
+
/** Light square colour of the transparency checkerboard */
|
|
3
|
+
export declare const CHECKER_LIGHT = "#ffffff";
|
|
4
|
+
/** Dark square colour of the transparency checkerboard */
|
|
5
|
+
export declare const CHECKER_DARK = "#cccccc";
|
|
6
|
+
/**
|
|
7
|
+
* Fill an area with a grey and white checkerboard, the usual way of showing transparency.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* drawCheckerboard(ctx, 30, 30, 5);
|
|
11
|
+
*/
|
|
12
|
+
export declare const drawCheckerboard: (ctx: Pick<DrawingContext, "fillStyle" | "fillRect">, width: number, height: number, squareSize: number) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Size of an image scaled to fit inside a box, keeping its proportions.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* fitSize(2000, 1000, 90, 90); // { width: 90, height: 45 }
|
|
18
|
+
*/
|
|
19
|
+
export declare const fitSize: (width: number, height: number, boxWidth: number, boxHeight: number) => {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/lib/rendering/checkerboard/index.ts
|
|
2
|
+
/** Light square colour of the transparency checkerboard */
|
|
3
|
+
var CHECKER_LIGHT = "#ffffff";
|
|
4
|
+
/** Dark square colour of the transparency checkerboard */
|
|
5
|
+
var CHECKER_DARK = "#cccccc";
|
|
6
|
+
/**
|
|
7
|
+
* Fill an area with a grey and white checkerboard, the usual way of showing transparency.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* drawCheckerboard(ctx, 30, 30, 5);
|
|
11
|
+
*/
|
|
12
|
+
var drawCheckerboard = (ctx, width, height, squareSize) => {
|
|
13
|
+
ctx.fillStyle = CHECKER_LIGHT;
|
|
14
|
+
ctx.fillRect(0, 0, width, height);
|
|
15
|
+
ctx.fillStyle = CHECKER_DARK;
|
|
16
|
+
for (let y = 0; y * squareSize < height; y++) for (let x = y % 2; x * squareSize < width; x += 2) ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Size of an image scaled to fit inside a box, keeping its proportions.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* fitSize(2000, 1000, 90, 90); // { width: 90, height: 45 }
|
|
23
|
+
*/
|
|
24
|
+
var fitSize = (width, height, boxWidth, boxHeight) => {
|
|
25
|
+
const scale = Math.min(boxWidth / width, boxHeight / height);
|
|
26
|
+
return {
|
|
27
|
+
width: width * scale,
|
|
28
|
+
height: height * scale
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export { CHECKER_DARK, CHECKER_LIGHT, drawCheckerboard, fitSize };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type Layer from "../../classes/Layer";
|
|
2
|
+
import type Viewport from "../../classes/Viewport";
|
|
3
|
+
import type { Rect } from "../../classes/Viewport";
|
|
4
|
+
/** Colour of the canvas itself (the "paper") */
|
|
5
|
+
export declare const CANVAS_BACKGROUND = "#ffffff";
|
|
6
|
+
/** Colour of the area around the canvas */
|
|
7
|
+
export declare const PASTEBOARD_COLOUR = "#808080";
|
|
8
|
+
/** Anything drawn like a layer: the user's own layers, and drawings from elsewhere (external sheets) */
|
|
9
|
+
export type DrawableLayer = Pick<Layer, "visible" | "opacity" | "blendMode" | "canvas">;
|
|
10
|
+
/** A layer image to draw in place of a layer's own canvas (e.g. the layer with the stroke in progress) */
|
|
11
|
+
export interface LayerOverride {
|
|
12
|
+
layer: DrawableLayer;
|
|
13
|
+
image: CanvasImageSource;
|
|
14
|
+
}
|
|
15
|
+
/** The 2D drawing operations used for compositing; satisfied by both on-screen and offscreen canvas contexts */
|
|
16
|
+
export type DrawingContext = Pick<CanvasRenderingContext2D, "setTransform" | "fillRect" | "drawImage" | "save" | "restore" | "fillStyle" | "globalAlpha" | "globalCompositeOperation" | "imageSmoothingEnabled" | "beginPath" | "rect" | "clip">;
|
|
17
|
+
/**
|
|
18
|
+
* Draw layers onto a context that is already transformed to canvas coordinates, from the bottom layer to the top.
|
|
19
|
+
*
|
|
20
|
+
* Hidden and fully transparent layers are skipped. Each layer uses its opacity and blend mode.
|
|
21
|
+
*
|
|
22
|
+
* @param layers - layers from top (index 0) to bottom, as `LayerStack.layers` lists them
|
|
23
|
+
* @param override - draw this image instead of one layer's canvas (the stroke preview)
|
|
24
|
+
* @param area - draw only this area of each layer (canvas pixels), in place
|
|
25
|
+
*/
|
|
26
|
+
export declare const drawLayers: (ctx: DrawingContext, layers: readonly DrawableLayer[], override?: LayerOverride, area?: Rect) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Draw the canvas onto a context of any size, scaled to fit exactly: white background, then all layers.
|
|
29
|
+
* Used for the canvas map, thumbnails and screenshots.
|
|
30
|
+
*
|
|
31
|
+
* @param override - draw this image instead of one layer's canvas (e.g. a transform in progress)
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* drawFlattened(mapContext, layers, 2000, 2000, 90, 90);
|
|
35
|
+
*/
|
|
36
|
+
export declare const drawFlattened: (ctx: DrawingContext, layers: readonly DrawableLayer[], canvasWidth: number, canvasHeight: number, targetWidth: number, targetHeight: number, override?: LayerOverride) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The screen pixels (device pixels) showing an area of the canvas, rounded outwards with one more pixel on each side.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* screenArea({ x: 10, y: 10, width: 5, height: 5 }, view, 2); // at zoom 1 and no offset: { x: 19, y: 19, width: 12, height: 12 }
|
|
42
|
+
*/
|
|
43
|
+
export declare const screenArea: (area: Rect, view: Pick<Viewport, "zoom" | "offsetX" | "offsetY">, pixelRatio: number) => Rect;
|
|
44
|
+
/**
|
|
45
|
+
* An area of the canvas grown by enough canvas pixels to cover two screen pixels at a scale (screen pixels per canvas
|
|
46
|
+
* pixel), within the canvas, so drawing just that area smooths its edges as drawing everything would.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* sourceArea({ x: 10, y: 10, width: 5, height: 5 }, 0.5, 2000, 2000); // grown by 5: { x: 5, y: 5, width: 15, height: 15 }
|
|
50
|
+
*/
|
|
51
|
+
export declare const sourceArea: (area: Rect, scale: number, canvasWidth: number, canvasHeight: number) => Rect;
|
|
52
|
+
/**
|
|
53
|
+
* Draw the visible part of the canvas onto the screen canvas: grey surround, white canvas, then all layers, positioned by the viewport.
|
|
54
|
+
*
|
|
55
|
+
* Pixels stay sharp when zoomed in past 100% and are smoothed when zoomed out.
|
|
56
|
+
*
|
|
57
|
+
* With an area, only the screen pixels showing that area of the canvas are redrawn (see `screenArea`), blending only
|
|
58
|
+
* that part of each layer, which is much quicker than redrawing everything while a stroke is drawn.
|
|
59
|
+
*
|
|
60
|
+
* @param pixelRatio - device pixels per CSS pixel (`window.devicePixelRatio`), for sharp drawing on high-resolution screens
|
|
61
|
+
* @param override - draw this image instead of one layer's canvas (the stroke preview)
|
|
62
|
+
* @param area - redraw only this area of the canvas (canvas pixels)
|
|
63
|
+
*/
|
|
64
|
+
export declare const drawScene: (ctx: DrawingContext, layers: readonly DrawableLayer[], view: Viewport, pixelRatio: number, override?: LayerOverride, area?: Rect) => void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/lib/rendering/compositing/index.ts
|
|
2
|
+
/** Colour of the canvas itself (the "paper") */
|
|
3
|
+
var CANVAS_BACKGROUND = "#ffffff";
|
|
4
|
+
/** Colour of the area around the canvas */
|
|
5
|
+
var PASTEBOARD_COLOUR = "#808080";
|
|
6
|
+
/**
|
|
7
|
+
* Draw layers onto a context that is already transformed to canvas coordinates, from the bottom layer to the top.
|
|
8
|
+
*
|
|
9
|
+
* Hidden and fully transparent layers are skipped. Each layer uses its opacity and blend mode.
|
|
10
|
+
*
|
|
11
|
+
* @param layers - layers from top (index 0) to bottom, as `LayerStack.layers` lists them
|
|
12
|
+
* @param override - draw this image instead of one layer's canvas (the stroke preview)
|
|
13
|
+
* @param area - draw only this area of each layer (canvas pixels), in place
|
|
14
|
+
*/
|
|
15
|
+
var drawLayers = (ctx, layers, override, area) => {
|
|
16
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
17
|
+
const layer = layers[i];
|
|
18
|
+
if (!layer.visible || layer.opacity === 0) continue;
|
|
19
|
+
ctx.globalAlpha = layer.opacity;
|
|
20
|
+
ctx.globalCompositeOperation = layer.blendMode;
|
|
21
|
+
const image = override?.layer === layer ? override.image : layer.canvas;
|
|
22
|
+
if (area) {
|
|
23
|
+
const { x, y, width, height } = area;
|
|
24
|
+
ctx.drawImage(image, x, y, width, height, x, y, width, height);
|
|
25
|
+
} else ctx.drawImage(image, 0, 0);
|
|
26
|
+
}
|
|
27
|
+
ctx.globalAlpha = 1;
|
|
28
|
+
ctx.globalCompositeOperation = "source-over";
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Draw the canvas onto a context of any size, scaled to fit exactly: white background, then all layers.
|
|
32
|
+
* Used for the canvas map, thumbnails and screenshots.
|
|
33
|
+
*
|
|
34
|
+
* @param override - draw this image instead of one layer's canvas (e.g. a transform in progress)
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* drawFlattened(mapContext, layers, 2000, 2000, 90, 90);
|
|
38
|
+
*/
|
|
39
|
+
var drawFlattened = (ctx, layers, canvasWidth, canvasHeight, targetWidth, targetHeight, override) => {
|
|
40
|
+
ctx.save();
|
|
41
|
+
ctx.setTransform(targetWidth / canvasWidth, 0, 0, targetHeight / canvasHeight, 0, 0);
|
|
42
|
+
ctx.imageSmoothingEnabled = true;
|
|
43
|
+
ctx.fillStyle = CANVAS_BACKGROUND;
|
|
44
|
+
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
45
|
+
drawLayers(ctx, layers, override);
|
|
46
|
+
ctx.restore();
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The screen pixels (device pixels) showing an area of the canvas, rounded outwards with one more pixel on each side.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* screenArea({ x: 10, y: 10, width: 5, height: 5 }, view, 2); // at zoom 1 and no offset: { x: 19, y: 19, width: 12, height: 12 }
|
|
53
|
+
*/
|
|
54
|
+
var screenArea = (area, view, pixelRatio) => {
|
|
55
|
+
const scale = view.zoom * pixelRatio;
|
|
56
|
+
const left = Math.floor(area.x * scale + view.offsetX * pixelRatio) - 1;
|
|
57
|
+
const top = Math.floor(area.y * scale + view.offsetY * pixelRatio) - 1;
|
|
58
|
+
const right = Math.ceil((area.x + area.width) * scale + view.offsetX * pixelRatio) + 1;
|
|
59
|
+
const bottom = Math.ceil((area.y + area.height) * scale + view.offsetY * pixelRatio) + 1;
|
|
60
|
+
return {
|
|
61
|
+
x: left,
|
|
62
|
+
y: top,
|
|
63
|
+
width: right - left,
|
|
64
|
+
height: bottom - top
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* An area of the canvas grown by enough canvas pixels to cover two screen pixels at a scale (screen pixels per canvas
|
|
69
|
+
* pixel), within the canvas, so drawing just that area smooths its edges as drawing everything would.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* sourceArea({ x: 10, y: 10, width: 5, height: 5 }, 0.5, 2000, 2000); // grown by 5: { x: 5, y: 5, width: 15, height: 15 }
|
|
73
|
+
*/
|
|
74
|
+
var sourceArea = (area, scale, canvasWidth, canvasHeight) => {
|
|
75
|
+
const margin = Math.ceil(2 / Math.min(1, scale)) + 1;
|
|
76
|
+
const x = Math.max(0, area.x - margin);
|
|
77
|
+
const y = Math.max(0, area.y - margin);
|
|
78
|
+
return {
|
|
79
|
+
x,
|
|
80
|
+
y,
|
|
81
|
+
width: Math.min(canvasWidth, area.x + area.width + margin) - x,
|
|
82
|
+
height: Math.min(canvasHeight, area.y + area.height + margin) - y
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Draw the visible part of the canvas onto the screen canvas: grey surround, white canvas, then all layers, positioned by the viewport.
|
|
87
|
+
*
|
|
88
|
+
* Pixels stay sharp when zoomed in past 100% and are smoothed when zoomed out.
|
|
89
|
+
*
|
|
90
|
+
* With an area, only the screen pixels showing that area of the canvas are redrawn (see `screenArea`), blending only
|
|
91
|
+
* that part of each layer, which is much quicker than redrawing everything while a stroke is drawn.
|
|
92
|
+
*
|
|
93
|
+
* @param pixelRatio - device pixels per CSS pixel (`window.devicePixelRatio`), for sharp drawing on high-resolution screens
|
|
94
|
+
* @param override - draw this image instead of one layer's canvas (the stroke preview)
|
|
95
|
+
* @param area - redraw only this area of the canvas (canvas pixels)
|
|
96
|
+
*/
|
|
97
|
+
var drawScene = (ctx, layers, view, pixelRatio, override, area) => {
|
|
98
|
+
ctx.save();
|
|
99
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
100
|
+
ctx.fillStyle = PASTEBOARD_COLOUR;
|
|
101
|
+
const scale = view.zoom * pixelRatio;
|
|
102
|
+
let source;
|
|
103
|
+
if (area) {
|
|
104
|
+
const clip = screenArea(area, view, pixelRatio);
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.rect(clip.x, clip.y, clip.width, clip.height);
|
|
107
|
+
ctx.clip();
|
|
108
|
+
ctx.fillRect(clip.x, clip.y, clip.width, clip.height);
|
|
109
|
+
source = sourceArea(area, scale, view.canvasWidth, view.canvasHeight);
|
|
110
|
+
} else ctx.fillRect(0, 0, view.width * pixelRatio, view.height * pixelRatio);
|
|
111
|
+
ctx.setTransform(scale, 0, 0, scale, view.offsetX * pixelRatio, view.offsetY * pixelRatio);
|
|
112
|
+
ctx.imageSmoothingEnabled = view.zoom < 1;
|
|
113
|
+
ctx.fillStyle = CANVAS_BACKGROUND;
|
|
114
|
+
const { x, y, width, height } = source ?? {
|
|
115
|
+
x: 0,
|
|
116
|
+
y: 0,
|
|
117
|
+
width: view.canvasWidth,
|
|
118
|
+
height: view.canvasHeight
|
|
119
|
+
};
|
|
120
|
+
ctx.fillRect(x, y, width, height);
|
|
121
|
+
drawLayers(ctx, layers, override, source);
|
|
122
|
+
ctx.restore();
|
|
123
|
+
};
|
|
124
|
+
export { CANVAS_BACKGROUND, PASTEBOARD_COLOUR, drawFlattened, drawLayers, drawScene, screenArea, sourceArea };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type BrushTip from "../BrushTip";
|
|
2
|
+
/** The drawing operations the brush cursor needs */
|
|
3
|
+
export type CursorContext = Pick<CanvasRenderingContext2D, "save" | "restore" | "translate" | "rotate" | "beginPath" | "ellipse" | "moveTo" | "lineTo" | "stroke" | "strokeStyle" | "lineWidth">;
|
|
4
|
+
/** Below this on-screen size (CSS pixels) a crosshair is shown instead of the outline */
|
|
5
|
+
export declare const MIN_OUTLINE_SIZE = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Draw the brush cursor: a 1:1 white outline of the brush shape at the pointer. The overlay canvas uses the "difference" blend mode,
|
|
8
|
+
* so white inverts whatever is underneath and stays visible on any colour. Very small brushes show a crosshair instead.
|
|
9
|
+
*
|
|
10
|
+
* @param x - pointer position on screen (CSS pixels)
|
|
11
|
+
* @param width - brush width on screen (CSS pixels, already multiplied by zoom)
|
|
12
|
+
* @param pixelRatio - device pixels per CSS pixel
|
|
13
|
+
*/
|
|
14
|
+
export declare const drawBrushCursor: (ctx: CursorContext, tip: BrushTip, x: number, y: number, width: number, height: number, rotation: number, pixelRatio: number) => void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/lib/rendering/cursor/index.ts
|
|
2
|
+
/** Below this on-screen size (CSS pixels) a crosshair is shown instead of the outline */
|
|
3
|
+
var MIN_OUTLINE_SIZE = 4;
|
|
4
|
+
/** Half-length of the small crosshair, in CSS pixels */
|
|
5
|
+
var CROSSHAIR = 4;
|
|
6
|
+
/**
|
|
7
|
+
* Draw the brush cursor: a 1:1 white outline of the brush shape at the pointer. The overlay canvas uses the "difference" blend mode,
|
|
8
|
+
* so white inverts whatever is underneath and stays visible on any colour. Very small brushes show a crosshair instead.
|
|
9
|
+
*
|
|
10
|
+
* @param x - pointer position on screen (CSS pixels)
|
|
11
|
+
* @param width - brush width on screen (CSS pixels, already multiplied by zoom)
|
|
12
|
+
* @param pixelRatio - device pixels per CSS pixel
|
|
13
|
+
*/
|
|
14
|
+
var drawBrushCursor = (ctx, tip, x, y, width, height, rotation, pixelRatio) => {
|
|
15
|
+
ctx.save();
|
|
16
|
+
ctx.strokeStyle = "#ffffff";
|
|
17
|
+
ctx.lineWidth = 1;
|
|
18
|
+
ctx.translate(x * pixelRatio, y * pixelRatio);
|
|
19
|
+
ctx.beginPath();
|
|
20
|
+
if (Math.max(width, height) < 4) {
|
|
21
|
+
const arm = CROSSHAIR * pixelRatio;
|
|
22
|
+
ctx.moveTo(-arm, 0);
|
|
23
|
+
ctx.lineTo(arm, 0);
|
|
24
|
+
ctx.moveTo(0, -arm);
|
|
25
|
+
ctx.lineTo(0, arm);
|
|
26
|
+
} else {
|
|
27
|
+
if (rotation) ctx.rotate(rotation * Math.PI / 180);
|
|
28
|
+
const w = width * pixelRatio;
|
|
29
|
+
const h = height * pixelRatio;
|
|
30
|
+
if (tip.outline) for (const [x1, y1, x2, y2] of tip.outline) {
|
|
31
|
+
ctx.moveTo((x1 - .5) * w, (y1 - .5) * h);
|
|
32
|
+
ctx.lineTo((x2 - .5) * w, (y2 - .5) * h);
|
|
33
|
+
}
|
|
34
|
+
else ctx.ellipse(0, 0, w / 2, h / 2, 0, 0, Math.PI * 2);
|
|
35
|
+
}
|
|
36
|
+
ctx.stroke();
|
|
37
|
+
ctx.restore();
|
|
38
|
+
};
|
|
39
|
+
export { MIN_OUTLINE_SIZE, drawBrushCursor };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a CSS cursor from SVG path data drawn on a 24×24 grid: black lines with a white outline so it shows on any colour.
|
|
3
|
+
*
|
|
4
|
+
* @param hotspot - the point (in the 24×24 grid) that is the cursor's exact position
|
|
5
|
+
* @param fallback - standard cursor used if the image cannot be shown
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* svgCursor("M4 4 L20 20", [4, 4], "crosshair"); // 'url("data:image/svg+xml,...") 4 4, crosshair'
|
|
9
|
+
*/
|
|
10
|
+
export declare const svgCursor: (path: string, hotspot: [number, number], fallback: string) => string;
|
|
11
|
+
/** Colour sample (eyedropper): a pipette pointing down to the left; the point is at its tip */
|
|
12
|
+
export declare const EYEDROPPER_CURSOR: string;
|
|
13
|
+
/** Polygon lasso: a straight-edged loop with a tail; the point is at the tail's end */
|
|
14
|
+
export declare const POLYGON_LASSO_CURSOR: string;
|
|
15
|
+
/** Freeform lasso: an irregular hand-drawn loop with a tail; the point is at the tail's end */
|
|
16
|
+
export declare const FREEFORM_LASSO_CURSOR: string;
|
|
17
|
+
/** Rotate: the browser's grab cursor, as browsers have no standard rotate cursor */
|
|
18
|
+
export declare const ROTATE_CURSOR = "grab";
|