@pixerate/editor 0.254.1 → 0.256.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/dist/agent/index.cjs +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/animation-editor/index.cjs +59 -59
- package/dist/animation-editor/index.js +3 -3
- package/dist/brushes/index.cjs +31 -29
- package/dist/brushes/index.d.cts +1 -1
- package/dist/brushes/index.d.ts +1 -1
- package/dist/brushes/index.js +27 -25
- package/dist/brushes/library.d.cts +17 -0
- package/dist/brushes/library.d.ts +17 -0
- package/dist/{brushstrokeRender-5PYSKZIJ.js → brushstrokeRender-HCOQUSWW.js} +53 -18
- package/dist/{brushstrokeRender-7FLLSQEC.cjs → brushstrokeRender-ZZOXI5O5.cjs} +55 -20
- package/dist/canvas/brushTypes.d.cts +22 -3
- package/dist/canvas/brushTypes.d.ts +22 -3
- package/dist/canvas/brushstroke.d.cts +34 -12
- package/dist/canvas/brushstroke.d.ts +34 -12
- package/dist/canvas/brushstrokeRender.d.cts +14 -8
- package/dist/canvas/brushstrokeRender.d.ts +14 -8
- package/dist/canvas/drawController.d.cts +11 -3
- package/dist/canvas/drawController.d.ts +11 -3
- package/dist/canvas/freehand.d.cts +6 -1
- package/dist/canvas/freehand.d.ts +6 -1
- package/dist/canvas/index.cjs +114 -102
- package/dist/canvas/index.js +16 -4
- package/dist/{chunk-XVIK73LS.js → chunk-5P4JEOIV.js} +4 -4
- package/dist/{chunk-JPEHXIB5.cjs → chunk-5YWWQ36H.cjs} +66 -21
- package/dist/{chunk-CHAWTXBE.cjs → chunk-6WCQ35GI.cjs} +4 -4
- package/dist/{chunk-GYMMNCFT.js → chunk-AF23QK5F.js} +56 -11
- package/dist/{chunk-JFHBXNR2.cjs → chunk-B7V34GIH.cjs} +86 -86
- package/dist/{chunk-QC3OXRE4.js → chunk-DMTEQ6SH.js} +1 -1
- package/dist/{chunk-TLOGFIIV.cjs → chunk-GCTUOTCW.cjs} +35 -35
- package/dist/{chunk-34KLWUH4.cjs → chunk-HIMGBZAM.cjs} +1133 -597
- package/dist/{chunk-7P6VF3FG.js → chunk-HYWRU5VX.js} +1 -1
- package/dist/{chunk-QPHNVZHO.js → chunk-JVRLGV3C.js} +1118 -582
- package/dist/{chunk-LFMKWGFL.cjs → chunk-LRNUZKWV.cjs} +22 -8
- package/dist/chunk-PGPFWC35.cjs +517 -0
- package/dist/{chunk-QLSGTCYT.cjs → chunk-TBAVCLEQ.cjs} +23 -23
- package/dist/{chunk-2ZJ4DT3M.js → chunk-VUKUTT2W.js} +1 -1
- package/dist/{chunk-5JNA6LLY.js → chunk-WYGSR3IJ.js} +22 -8
- package/dist/chunk-YHUMGBZD.js +517 -0
- package/dist/commands/builtins.d.cts +1 -1
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/image-editor/brush-presets.d.cts +32 -0
- package/dist/image-editor/brush-presets.d.ts +32 -0
- package/dist/image-editor/controller.d.cts +15 -0
- package/dist/image-editor/controller.d.ts +15 -0
- package/dist/image-editor/index.cjs +848 -775
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +81 -8
- package/dist/image-editor/types.d.cts +7 -0
- package/dist/image-editor/types.d.ts +7 -0
- package/dist/paint-editor/collaboration-tools.d.cts +19 -0
- package/dist/paint-editor/collaboration-tools.d.ts +19 -0
- package/dist/paint-editor/collaboration.d.cts +180 -0
- package/dist/paint-editor/collaboration.d.ts +180 -0
- package/dist/paint-editor/controller.d.cts +54 -0
- package/dist/paint-editor/controller.d.ts +54 -0
- package/dist/paint-editor/index.cjs +816 -283
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +567 -34
- package/dist/vector/brushstroke.d.cts +21 -5
- package/dist/vector/brushstroke.d.ts +21 -5
- package/dist/vector/controller.d.cts +14 -1
- package/dist/vector/controller.d.ts +14 -1
- package/dist/vector/index.cjs +1220 -1199
- package/dist/vector/index.js +34 -13
- package/dist/vector/types.d.cts +7 -0
- package/dist/vector/types.d.ts +7 -0
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +9 -3
- package/skills/pixerate-canvas/SKILL.md +10 -5
- package/skills/pixerate-image-editor/SKILL.md +2 -1
- package/skills/pixerate-paint-editor/SKILL.md +31 -46
- package/skills/pixerate-paint-editor/references/painting-alongside.md +19 -0
- package/skills/pixerate-vector/SKILL.md +1 -1
- package/dist/chunk-HS6QDWDF.cjs +0 -1029
- package/dist/chunk-MTKY6K3Q.js +0 -1029
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BRUSH_ID,
|
|
3
|
-
BRUSH_ROTATIONS,
|
|
4
|
-
DOT_ROTATIONS,
|
|
5
|
-
DOT_TEXTURE_ORDERS,
|
|
6
|
-
IMPASTO_TOOLS,
|
|
7
|
-
MAX_DOT_TEXTURES,
|
|
8
|
-
MAX_STICKER_STAMPS,
|
|
9
|
-
PARTICLE_KINDS,
|
|
10
|
-
PARTICLE_RENDERS,
|
|
11
|
-
RIBBON_PAIRS,
|
|
12
|
-
WET_MEDIA,
|
|
13
|
-
normalizePaintBrush
|
|
14
|
-
} from "./chunk-MTKY6K3Q.js";
|
|
15
1
|
import {
|
|
16
2
|
STAMP_KINDS,
|
|
17
3
|
editorStampOf,
|
|
@@ -21,14 +7,989 @@ import {
|
|
|
21
7
|
validateJsonSchema
|
|
22
8
|
} from "./chunk-V2ZCJRDM.js";
|
|
23
9
|
import {
|
|
10
|
+
DEFAULT_BLENDER,
|
|
11
|
+
DEFAULT_BRUSH,
|
|
12
|
+
DEFAULT_MIXER,
|
|
13
|
+
DEFAULT_SMUDGE,
|
|
24
14
|
MAX_BRUSH_SIZE,
|
|
25
15
|
PAINT_GRADIENT_JSON_SCHEMA,
|
|
16
|
+
normalizeBlender,
|
|
17
|
+
normalizeBrush,
|
|
18
|
+
normalizeMixer,
|
|
19
|
+
normalizeSmudge,
|
|
26
20
|
readPaintSource
|
|
27
21
|
} from "./chunk-GCVFEPJG.js";
|
|
28
22
|
import {
|
|
29
23
|
BLEND_MODES
|
|
30
24
|
} from "./chunk-IRSTHLQQ.js";
|
|
31
25
|
|
|
26
|
+
// src/paint-editor/mark-settings.ts
|
|
27
|
+
var unit = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
|
|
28
|
+
var oneOf = (v, all, d) => all.includes(v) ? v : d;
|
|
29
|
+
var MAX_STICKER_STAMPS = 16;
|
|
30
|
+
var DEFAULT_STICKER = { stamps: ["editor:star"], density: 0.4, sizeJitter: 0.4, rotationJitter: 1, scatter: 0.5, colorJitter: 0.2 };
|
|
31
|
+
function normalizeStickerSettings(raw = {}, base = DEFAULT_STICKER) {
|
|
32
|
+
const refs = (list2) => list2.flatMap((s) => {
|
|
33
|
+
try {
|
|
34
|
+
return [normalizeStickerRef(s)];
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const stamps = Array.isArray(raw.stamps) ? refs(raw.stamps) : refs(base.stamps);
|
|
40
|
+
return {
|
|
41
|
+
stamps: stamps.length ? [...new Set(stamps)].slice(0, MAX_STICKER_STAMPS) : refs(base.stamps.length ? base.stamps : DEFAULT_STICKER.stamps),
|
|
42
|
+
density: unit(raw.density, base.density),
|
|
43
|
+
sizeJitter: unit(raw.sizeJitter, base.sizeJitter),
|
|
44
|
+
rotationJitter: unit(raw.rotationJitter, base.rotationJitter),
|
|
45
|
+
scatter: typeof raw.scatter === "number" && Number.isFinite(raw.scatter) ? Math.min(2, Math.max(0, raw.scatter)) : base.scatter,
|
|
46
|
+
colorJitter: unit(raw.colorJitter, base.colorJitter)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var PARTICLE_KINDS = ["string", "spring", "orbit", "flow", "elastic", "fuzzy"];
|
|
50
|
+
var PARTICLE_RENDERS = ["dot", "line", "sequence", "ribbon"];
|
|
51
|
+
var RIBBON_PAIRS = ["adjacent", "first-last"];
|
|
52
|
+
var DOT_TEXTURE_ORDERS = ["sequence", "random"];
|
|
53
|
+
var DOT_ROTATIONS = ["follow", "random", "fixed"];
|
|
54
|
+
var MAX_DOT_TEXTURES = 4;
|
|
55
|
+
var DEFAULT_PARTICLE = {
|
|
56
|
+
kind: "spring",
|
|
57
|
+
render: "line",
|
|
58
|
+
count: 12,
|
|
59
|
+
size: 0.08,
|
|
60
|
+
spread: 0.5,
|
|
61
|
+
stiffness: 0.4,
|
|
62
|
+
damping: 0.25,
|
|
63
|
+
speed: 0.5,
|
|
64
|
+
gravity: 0,
|
|
65
|
+
life: 0.5,
|
|
66
|
+
interval: 0.3,
|
|
67
|
+
ribbonFill: 0.5,
|
|
68
|
+
ribbonPairs: "adjacent",
|
|
69
|
+
waveAmplitude: 0,
|
|
70
|
+
waveLength: 60,
|
|
71
|
+
waveSpeed: 0,
|
|
72
|
+
wavePhaseJitter: 0,
|
|
73
|
+
dotTextures: [],
|
|
74
|
+
dotTextureOrder: "sequence",
|
|
75
|
+
dotRotation: "random"
|
|
76
|
+
};
|
|
77
|
+
function normalizeParticle(raw = {}, base = DEFAULT_PARTICLE) {
|
|
78
|
+
const textures = Array.isArray(raw.dotTextures) ? raw.dotTextures.filter((t) => STAMP_KINDS.includes(t)).slice(0, MAX_DOT_TEXTURES) : [...base.dotTextures ?? []];
|
|
79
|
+
return {
|
|
80
|
+
kind: oneOf(raw.kind, PARTICLE_KINDS, base.kind),
|
|
81
|
+
render: oneOf(raw.render, PARTICLE_RENDERS, base.render),
|
|
82
|
+
count: typeof raw.count === "number" && Number.isFinite(raw.count) ? Math.min(64, Math.max(1, Math.round(raw.count))) : base.count,
|
|
83
|
+
size: unit(raw.size, base.size),
|
|
84
|
+
spread: unit(raw.spread, base.spread),
|
|
85
|
+
stiffness: unit(raw.stiffness, base.stiffness),
|
|
86
|
+
damping: unit(raw.damping, base.damping),
|
|
87
|
+
speed: unit(raw.speed, base.speed),
|
|
88
|
+
gravity: typeof raw.gravity === "number" && Number.isFinite(raw.gravity) ? Math.min(1, Math.max(-1, raw.gravity)) : base.gravity,
|
|
89
|
+
life: unit(raw.life, base.life),
|
|
90
|
+
interval: unit(raw.interval, base.interval),
|
|
91
|
+
ribbonFill: unit(raw.ribbonFill, base.ribbonFill ?? DEFAULT_PARTICLE.ribbonFill),
|
|
92
|
+
ribbonPairs: oneOf(raw.ribbonPairs, RIBBON_PAIRS, base.ribbonPairs ?? DEFAULT_PARTICLE.ribbonPairs),
|
|
93
|
+
waveAmplitude: unit(raw.waveAmplitude, base.waveAmplitude ?? 0),
|
|
94
|
+
waveLength: typeof raw.waveLength === "number" && Number.isFinite(raw.waveLength) ? Math.min(2e3, Math.max(4, raw.waveLength)) : base.waveLength ?? DEFAULT_PARTICLE.waveLength,
|
|
95
|
+
waveSpeed: unit(raw.waveSpeed, base.waveSpeed ?? 0),
|
|
96
|
+
wavePhaseJitter: unit(raw.wavePhaseJitter, base.wavePhaseJitter ?? 0),
|
|
97
|
+
dotTextures: textures,
|
|
98
|
+
dotTextureOrder: oneOf(raw.dotTextureOrder, DOT_TEXTURE_ORDERS, base.dotTextureOrder ?? DEFAULT_PARTICLE.dotTextureOrder),
|
|
99
|
+
dotRotation: oneOf(raw.dotRotation, DOT_ROTATIONS, base.dotRotation ?? DEFAULT_PARTICLE.dotRotation)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var DEFAULT_BRISTLE = { count: 32, stiffness: 0.5, splay: 0.5, thickness: 0.06, load: 0.6, jitter: 0.4 };
|
|
103
|
+
function normalizeBristle(raw = {}, base = DEFAULT_BRISTLE) {
|
|
104
|
+
return {
|
|
105
|
+
count: typeof raw.count === "number" && Number.isFinite(raw.count) ? Math.min(128, Math.max(3, Math.round(raw.count))) : base.count,
|
|
106
|
+
stiffness: unit(raw.stiffness, base.stiffness),
|
|
107
|
+
splay: unit(raw.splay, base.splay),
|
|
108
|
+
thickness: unit(raw.thickness, base.thickness),
|
|
109
|
+
load: unit(raw.load, base.load),
|
|
110
|
+
jitter: unit(raw.jitter, base.jitter)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/paint-editor/brushes.ts
|
|
115
|
+
var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
|
|
116
|
+
var PAINT_BRUSH_CATEGORIES = ["pencil", "pastel", "charcoal", "airbrush", "marker", "ink", "paint", "eraser", "blender", "smudge", "mixer", "watercolor", "wet-tool", "oil", "impasto", "procedural", "bristle", "sticker", "light", "custom"];
|
|
117
|
+
var WET_TOOLS = ["paint", "water", "dry", "blow", "blend", "smudge", "blot"];
|
|
118
|
+
var WET_MEDIA = ["watercolor", "gouache", "oil"];
|
|
119
|
+
var DEFAULT_WET_BRUSH = { tool: "paint", water: 0.5, pigment: 0.5, body: 0, strength: 0.5, rewet: 0, medium: "watercolor", load: 1, thinner: 0 };
|
|
120
|
+
function normalizeWetBrush(raw = {}, base = DEFAULT_WET_BRUSH) {
|
|
121
|
+
return {
|
|
122
|
+
tool: WET_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
|
|
123
|
+
water: unit2(raw.water, base.water),
|
|
124
|
+
pigment: unit2(raw.pigment, base.pigment),
|
|
125
|
+
body: unit2(raw.body, base.body),
|
|
126
|
+
strength: unit2(raw.strength, base.strength),
|
|
127
|
+
rewet: unit2(raw.rewet, base.rewet),
|
|
128
|
+
medium: WET_MEDIA.includes(raw.medium) ? raw.medium : base.medium ?? "watercolor",
|
|
129
|
+
load: unit2(raw.load, base.load ?? 1),
|
|
130
|
+
thinner: unit2(raw.thinner, base.thinner ?? 0)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
var IMPASTO_TOOLS = ["brush", "tube", "knife", "roller", "gloop"];
|
|
134
|
+
var DEFAULT_IMPASTO = { tool: "brush", thickness: 0.6, gloss: 0.4, metallic: 0, load: 0.9, thinner: 0, drag: 0.4, rim: 0, clarity: 0 };
|
|
135
|
+
var DEFAULT_LIGHT = { amount: 0, pressure: false };
|
|
136
|
+
function normalizeLight(raw = {}, base = DEFAULT_LIGHT) {
|
|
137
|
+
return { amount: unit2(raw.amount, base.amount), pressure: typeof raw.pressure === "boolean" ? raw.pressure : base.pressure };
|
|
138
|
+
}
|
|
139
|
+
function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
|
|
140
|
+
return {
|
|
141
|
+
tool: IMPASTO_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
|
|
142
|
+
thickness: unit2(raw.thickness, base.thickness),
|
|
143
|
+
gloss: unit2(raw.gloss, base.gloss),
|
|
144
|
+
metallic: unit2(raw.metallic, base.metallic),
|
|
145
|
+
load: unit2(raw.load, base.load),
|
|
146
|
+
thinner: unit2(raw.thinner, base.thinner),
|
|
147
|
+
drag: unit2(raw.drag, base.drag),
|
|
148
|
+
rim: unit2(raw.rim, base.rim ?? 0),
|
|
149
|
+
clarity: unit2(raw.clarity, base.clarity ?? 0)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
var MULTI_COLOR_MODES = ["off", "tip", "dab"];
|
|
153
|
+
var DEFAULT_MULTI_COLOR = { mode: "off", colors: [], order: "sequence", length: 12, softness: 1 };
|
|
154
|
+
var DEFAULT_BEHAVIOR = { pickup: 0, mix: 0.5, dirty: false, mixing: "pigment", multicolor: DEFAULT_MULTI_COLOR };
|
|
155
|
+
var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
|
|
156
|
+
var DEFAULT_DYNAMICS = {
|
|
157
|
+
minSize: 0,
|
|
158
|
+
pressureFlow: 0,
|
|
159
|
+
tiltSize: 0,
|
|
160
|
+
tiltRoundness: 0,
|
|
161
|
+
tiltOpacity: 0,
|
|
162
|
+
velocitySize: 0,
|
|
163
|
+
velocityOpacity: 0,
|
|
164
|
+
sizeJitter: 0,
|
|
165
|
+
angleJitter: 0,
|
|
166
|
+
opacityJitter: 0,
|
|
167
|
+
scatter: 0
|
|
168
|
+
};
|
|
169
|
+
var unit2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
|
|
170
|
+
var range = (v, lo, hi, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d;
|
|
171
|
+
function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
|
|
172
|
+
return {
|
|
173
|
+
minSize: unit2(raw.minSize, base.minSize),
|
|
174
|
+
pressureFlow: unit2(raw.pressureFlow, base.pressureFlow),
|
|
175
|
+
tiltSize: unit2(raw.tiltSize, base.tiltSize),
|
|
176
|
+
tiltRoundness: unit2(raw.tiltRoundness, base.tiltRoundness),
|
|
177
|
+
tiltOpacity: unit2(raw.tiltOpacity, base.tiltOpacity),
|
|
178
|
+
velocitySize: range(raw.velocitySize, -1, 1, base.velocitySize),
|
|
179
|
+
velocityOpacity: range(raw.velocityOpacity, -1, 1, base.velocityOpacity),
|
|
180
|
+
sizeJitter: unit2(raw.sizeJitter, base.sizeJitter),
|
|
181
|
+
angleJitter: unit2(raw.angleJitter, base.angleJitter),
|
|
182
|
+
opacityJitter: unit2(raw.opacityJitter, base.opacityJitter),
|
|
183
|
+
scatter: range(raw.scatter, 0, 4, base.scatter)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
var BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
187
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
188
|
+
function normalizeMultiColor(raw = {}, base = DEFAULT_MULTI_COLOR) {
|
|
189
|
+
const colors = Array.isArray(raw.colors) ? raw.colors.filter((c) => typeof c === "string" && HEX.test(c.trim())).map((c) => c.trim().toLowerCase()).slice(0, 8) : base.colors;
|
|
190
|
+
return {
|
|
191
|
+
mode: MULTI_COLOR_MODES.includes(raw.mode) ? raw.mode : base.mode,
|
|
192
|
+
colors: [...colors],
|
|
193
|
+
order: raw.order === "random" || raw.order === "sequence" ? raw.order : base.order,
|
|
194
|
+
length: typeof raw.length === "number" && Number.isFinite(raw.length) ? Math.min(500, Math.max(1, Math.round(raw.length))) : base.length,
|
|
195
|
+
softness: unit2(raw.softness, base.softness)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function normalizeBehavior(raw = {}, base = DEFAULT_BEHAVIOR) {
|
|
199
|
+
return {
|
|
200
|
+
pickup: unit2(raw.pickup, base.pickup),
|
|
201
|
+
mix: unit2(raw.mix, base.mix),
|
|
202
|
+
dirty: typeof raw.dirty === "boolean" ? raw.dirty : base.dirty,
|
|
203
|
+
mixing: raw.mixing === "rgb" || raw.mixing === "pigment" ? raw.mixing : base.mixing,
|
|
204
|
+
multicolor: normalizeMultiColor(raw.multicolor ?? {}, base.multicolor)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function isWetCapable(brush) {
|
|
208
|
+
return brush.engine === "dab" || brush.engine === "mixer" || brush.engine === "wet" || brush.engine === "impasto";
|
|
209
|
+
}
|
|
210
|
+
function isWetBrush(brush) {
|
|
211
|
+
return brush.engine === "wet";
|
|
212
|
+
}
|
|
213
|
+
function usesPaintExchange(brush, gradient = brush.paint !== null) {
|
|
214
|
+
return brush.engine === "dab" && (brush.behavior.pickup > 0 || gradient);
|
|
215
|
+
}
|
|
216
|
+
function brushTakesPaint(brush) {
|
|
217
|
+
if (brush.engine === "eraser" || brush.engine === "blender" || brush.engine === "smudge") return false;
|
|
218
|
+
if (brush.engine === "wet") return brush.wet.tool === "paint";
|
|
219
|
+
if (brush.engine === "mixer") return brush.loadsColor;
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
function paintToMultiColor(g) {
|
|
223
|
+
if (!g || g.space === "oklab" || g.stops.some((s) => s.opacity !== void 0 || s.midpoint !== void 0)) return null;
|
|
224
|
+
const colors = g.stops.map((s) => s.color);
|
|
225
|
+
const own = (list2) => list2.length === 2 && list2[0] === "paint" && list2[1] === "secondary" ? [] : list2.some((c) => c === "paint" || c === "secondary") ? null : list2;
|
|
226
|
+
const uniform = (n) => g.stops.every((s, k) => s.offset === k / n);
|
|
227
|
+
const softness = g.softness ?? 1;
|
|
228
|
+
if (g.map.by === "across-tip" && colors.length <= 8 && uniform(colors.length - 1)) {
|
|
229
|
+
const c = own(colors);
|
|
230
|
+
return c ? { mode: "tip", colors: c, order: "sequence", length: DEFAULT_MULTI_COLOR.length, softness } : null;
|
|
231
|
+
}
|
|
232
|
+
if (g.map.by === "random" && colors.length % 2 === 0 && colors.length <= 16) {
|
|
233
|
+
const n = colors.length / 2;
|
|
234
|
+
const pairs = Array.from({ length: n }, (_, k) => colors[2 * k]);
|
|
235
|
+
const hard = g.stops.every((s, i) => s.offset === (Math.floor(i / 2) + i % 2) / n && s.color === pairs[Math.floor(i / 2)]);
|
|
236
|
+
const c = hard ? own(pairs) : null;
|
|
237
|
+
return c ? { mode: "dab", colors: c, order: "random", length: g.map.every ?? 1, softness } : null;
|
|
238
|
+
}
|
|
239
|
+
if (g.map.by === "distance" && g.map.unit === "dabs" && g.map.repeat === "repeat" && colors.length >= 3 && colors.length <= 9 && colors[0] === colors[colors.length - 1] && uniform(colors.length - 1)) {
|
|
240
|
+
const n = colors.length - 1;
|
|
241
|
+
const c = own(colors.slice(0, n));
|
|
242
|
+
const length = g.map.length / n;
|
|
243
|
+
return c && Number.isInteger(length) ? { mode: "dab", colors: c, order: "sequence", length, softness } : null;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function multiColorToPaint(mc, mixing) {
|
|
248
|
+
if (mc.mode === "off") return null;
|
|
249
|
+
const colors = mc.colors.length >= 2 ? mc.colors : ["paint", "secondary"];
|
|
250
|
+
const n = colors.length;
|
|
251
|
+
const space = mixing === "rgb" ? "srgb" : "pigment";
|
|
252
|
+
const soft = mc.softness < 1 ? { softness: mc.softness } : {};
|
|
253
|
+
if (mc.mode === "tip") return { type: "gradient", stops: colors.map((color, k) => ({ offset: k / (n - 1), color })), space, map: { by: "across-tip" }, ...soft };
|
|
254
|
+
if (mc.order === "random") {
|
|
255
|
+
const stops = colors.flatMap((color, k) => [
|
|
256
|
+
{ offset: k / n, color },
|
|
257
|
+
{ offset: (k + 1) / n, color }
|
|
258
|
+
]);
|
|
259
|
+
return { type: "gradient", stops, space, map: { by: "random", every: mc.length } };
|
|
260
|
+
}
|
|
261
|
+
return { type: "gradient", stops: [...colors, colors[0]].map((color, k) => ({ offset: k / n, color })), space, map: { by: "distance", length: mc.length * n, repeat: "repeat", unit: "dabs" }, ...soft };
|
|
262
|
+
}
|
|
263
|
+
function normalizePaintBrush(raw = {}, base) {
|
|
264
|
+
const b = base ?? BASE_BRUSH;
|
|
265
|
+
const id = typeof raw.id === "string" && BRUSH_ID.test(raw.id) ? raw.id : b.id;
|
|
266
|
+
const engine = PAINT_BRUSH_ENGINES.includes(raw.engine) ? raw.engine : b.engine;
|
|
267
|
+
const rb = raw.behavior ?? {};
|
|
268
|
+
const mixerIn = { ...b.mixer, ...raw.mixer ?? {} };
|
|
269
|
+
if (engine === "mixer") {
|
|
270
|
+
if (rb.pickup !== void 0) mixerIn.wet = rb.pickup;
|
|
271
|
+
if (rb.mix !== void 0) mixerIn.mix = rb.mix;
|
|
272
|
+
if (rb.dirty !== void 0) mixerIn.cleanEachStroke = !rb.dirty;
|
|
273
|
+
if (rb.mixing !== void 0) mixerIn.mixing = rb.mixing;
|
|
274
|
+
}
|
|
275
|
+
const mixer = normalizeMixer(mixerIn);
|
|
276
|
+
let behavior = normalizeBehavior(rb, b.behavior);
|
|
277
|
+
if (engine === "mixer") behavior = { ...behavior, pickup: mixer.wet, mix: mixer.mix, dirty: !mixer.cleanEachStroke, mixing: mixer.mixing };
|
|
278
|
+
let paint = b.paint ?? null;
|
|
279
|
+
if (raw.paint !== void 0) {
|
|
280
|
+
const p = readPaintSource(raw.paint);
|
|
281
|
+
paint = p && p.type === "gradient" ? p : null;
|
|
282
|
+
} else if (rb.multicolor !== void 0) {
|
|
283
|
+
const mc = normalizeMultiColor(rb.multicolor, paintToMultiColor(paint) ?? DEFAULT_MULTI_COLOR);
|
|
284
|
+
paint = mc.mode === "off" ? paintToMultiColor(paint) ? null : paint : multiColorToPaint(mc, behavior.mixing);
|
|
285
|
+
}
|
|
286
|
+
behavior = { ...behavior, multicolor: paintToMultiColor(paint) ?? { ...DEFAULT_MULTI_COLOR, colors: [] } };
|
|
287
|
+
return {
|
|
288
|
+
id,
|
|
289
|
+
name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
|
|
290
|
+
category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
|
|
291
|
+
engine,
|
|
292
|
+
tip: normalizeBrush({ ...b.tip, ...raw.tip ?? {} }),
|
|
293
|
+
grain: unit2(raw.grain, b.grain),
|
|
294
|
+
pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
|
|
295
|
+
rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
|
|
296
|
+
dynamics: normalizeDynamics(raw.dynamics ?? {}, b.dynamics),
|
|
297
|
+
mixer,
|
|
298
|
+
loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
|
|
299
|
+
behavior,
|
|
300
|
+
blender: normalizeBlender(raw.blender ?? {}, b.blender),
|
|
301
|
+
smudge: normalizeSmudge(raw.smudge ?? {}, b.smudge),
|
|
302
|
+
wet: normalizeWetBrush(raw.wet ?? {}, b.wet ?? DEFAULT_WET_BRUSH),
|
|
303
|
+
impasto: normalizeImpasto(raw.impasto ?? {}, b.impasto ?? DEFAULT_IMPASTO),
|
|
304
|
+
particle: normalizeParticle(raw.particle ?? {}, b.particle ?? DEFAULT_PARTICLE),
|
|
305
|
+
bristle: normalizeBristle(raw.bristle ?? {}, b.bristle ?? DEFAULT_BRISTLE),
|
|
306
|
+
sticker: normalizeStickerSettings(raw.sticker ?? {}, b.sticker ?? DEFAULT_STICKER),
|
|
307
|
+
light: normalizeLight(raw.light ?? {}, b.light ?? DEFAULT_LIGHT),
|
|
308
|
+
paint,
|
|
309
|
+
...raw.builtin ? { builtin: true } : {}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
var BASE_BRUSH = {
|
|
313
|
+
id: "round",
|
|
314
|
+
name: "Round",
|
|
315
|
+
category: "custom",
|
|
316
|
+
engine: "dab",
|
|
317
|
+
tip: { ...DEFAULT_BRUSH },
|
|
318
|
+
grain: 0,
|
|
319
|
+
pressureSmoothing: 0,
|
|
320
|
+
rotation: "fixed",
|
|
321
|
+
dynamics: { ...DEFAULT_DYNAMICS },
|
|
322
|
+
mixer: { ...DEFAULT_MIXER },
|
|
323
|
+
loadsColor: true,
|
|
324
|
+
behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
|
|
325
|
+
blender: { ...DEFAULT_BLENDER },
|
|
326
|
+
smudge: { ...DEFAULT_SMUDGE },
|
|
327
|
+
wet: { ...DEFAULT_WET_BRUSH },
|
|
328
|
+
impasto: { ...DEFAULT_IMPASTO },
|
|
329
|
+
particle: { ...DEFAULT_PARTICLE },
|
|
330
|
+
bristle: { ...DEFAULT_BRISTLE },
|
|
331
|
+
sticker: { ...DEFAULT_STICKER, stamps: [...DEFAULT_STICKER.stamps] },
|
|
332
|
+
light: { ...DEFAULT_LIGHT },
|
|
333
|
+
paint: null
|
|
334
|
+
};
|
|
335
|
+
var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
|
|
336
|
+
var BUILTIN_BRUSHES = [
|
|
337
|
+
builtin({
|
|
338
|
+
id: "pencil-hb",
|
|
339
|
+
name: "HB pencil",
|
|
340
|
+
category: "pencil",
|
|
341
|
+
tip: { size: 4, hardness: 0.7, spacing: 0.08, opacity: 0.85, flow: 0.55, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
|
|
342
|
+
grain: 0.85,
|
|
343
|
+
pressureSmoothing: 0.3,
|
|
344
|
+
dynamics: { minSize: 0.55, tiltSize: 0.9, tiltRoundness: 0.7, tiltOpacity: 0.5 }
|
|
345
|
+
}),
|
|
346
|
+
builtin({
|
|
347
|
+
id: "pencil-6b",
|
|
348
|
+
name: "6B pencil",
|
|
349
|
+
category: "pencil",
|
|
350
|
+
tip: { size: 7, hardness: 0.55, spacing: 0.07, opacity: 1, flow: 0.75, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
|
|
351
|
+
grain: 0.7,
|
|
352
|
+
pressureSmoothing: 0.3,
|
|
353
|
+
dynamics: { minSize: 0.5, tiltSize: 1, tiltRoundness: 0.75, tiltOpacity: 0.35 }
|
|
354
|
+
}),
|
|
355
|
+
builtin({
|
|
356
|
+
id: "soft-pastel",
|
|
357
|
+
name: "Soft pastel",
|
|
358
|
+
category: "pastel",
|
|
359
|
+
rotation: "stroke",
|
|
360
|
+
tip: { size: 32, hardness: 0.55, spacing: 0.05, angle: 90, roundness: 0.55, opacity: 1, flow: 0.7, smoothing: 0.15, pressureSize: false, pressureOpacity: true },
|
|
361
|
+
grain: 1,
|
|
362
|
+
pressureSmoothing: 0.25,
|
|
363
|
+
dynamics: { sizeJitter: 0.06, angleJitter: 0.02 }
|
|
364
|
+
}),
|
|
365
|
+
builtin({
|
|
366
|
+
id: "oil-pastel",
|
|
367
|
+
name: "Oil pastel",
|
|
368
|
+
category: "pastel",
|
|
369
|
+
rotation: "stroke",
|
|
370
|
+
tip: { size: 26, hardness: 0.8, spacing: 0.05, angle: 90, roundness: 0.7, opacity: 1, flow: 0.9, smoothing: 0.15, pressureSize: true, pressureOpacity: false, blend: "pigment" },
|
|
371
|
+
grain: 0.5,
|
|
372
|
+
pressureSmoothing: 0.25,
|
|
373
|
+
dynamics: { minSize: 0.6 }
|
|
374
|
+
}),
|
|
375
|
+
builtin({
|
|
376
|
+
id: "charcoal-stick",
|
|
377
|
+
name: "Charcoal stick",
|
|
378
|
+
category: "charcoal",
|
|
379
|
+
rotation: "stroke",
|
|
380
|
+
tip: { size: 26, hardness: 0.4, spacing: 0.05, angle: 90, roundness: 0.35, opacity: 0.95, flow: 0.5, smoothing: 0.15, pressureSize: false, pressureOpacity: true },
|
|
381
|
+
grain: 0.95,
|
|
382
|
+
pressureSmoothing: 0.2,
|
|
383
|
+
dynamics: { sizeJitter: 0.1, angleJitter: 0.03, tiltSize: 0.6 }
|
|
384
|
+
}),
|
|
385
|
+
builtin({
|
|
386
|
+
id: "vine-charcoal",
|
|
387
|
+
name: "Vine charcoal",
|
|
388
|
+
category: "charcoal",
|
|
389
|
+
tip: { size: 10, hardness: 0.3, spacing: 0.06, opacity: 0.75, flow: 0.45, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
|
|
390
|
+
grain: 0.9,
|
|
391
|
+
pressureSmoothing: 0.2,
|
|
392
|
+
dynamics: { minSize: 0.4, tiltSize: 0.8, tiltRoundness: 0.5, tiltOpacity: 0.3, sizeJitter: 0.08 }
|
|
393
|
+
}),
|
|
394
|
+
builtin({
|
|
395
|
+
id: "airbrush",
|
|
396
|
+
name: "Airbrush",
|
|
397
|
+
category: "airbrush",
|
|
398
|
+
tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
|
|
399
|
+
grain: 0,
|
|
400
|
+
pressureSmoothing: 0.3,
|
|
401
|
+
dynamics: { pressureFlow: 1, tiltSize: 0.5, tiltRoundness: 0.6 }
|
|
402
|
+
}),
|
|
403
|
+
builtin({
|
|
404
|
+
id: "marker",
|
|
405
|
+
name: "Chisel marker",
|
|
406
|
+
category: "marker",
|
|
407
|
+
tip: { size: 24, hardness: 0.85, spacing: 0.04, angle: 45, roundness: 0.35, opacity: 0.6, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: false, blend: "multiply" },
|
|
408
|
+
grain: 0.1
|
|
409
|
+
}),
|
|
410
|
+
builtin({
|
|
411
|
+
id: "ink-pen",
|
|
412
|
+
name: "Ink pen",
|
|
413
|
+
category: "ink",
|
|
414
|
+
tip: { size: 4, hardness: 1, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
|
|
415
|
+
pressureSmoothing: 0.4,
|
|
416
|
+
dynamics: { minSize: 0.25, velocitySize: -0.45 }
|
|
417
|
+
}),
|
|
418
|
+
builtin({
|
|
419
|
+
id: "brush-pen",
|
|
420
|
+
name: "Brush pen",
|
|
421
|
+
category: "ink",
|
|
422
|
+
rotation: "stroke",
|
|
423
|
+
tip: { size: 14, hardness: 0.9, spacing: 0.04, roundness: 0.8, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
|
|
424
|
+
pressureSmoothing: 0.45,
|
|
425
|
+
dynamics: { minSize: 0.05, velocitySize: -0.3 }
|
|
426
|
+
}),
|
|
427
|
+
builtin({
|
|
428
|
+
id: "eraser-soft",
|
|
429
|
+
name: "Soft eraser",
|
|
430
|
+
category: "eraser",
|
|
431
|
+
engine: "eraser",
|
|
432
|
+
tip: { size: 40, hardness: 0.2, spacing: 0.08, opacity: 1, flow: 0.6, pressureSize: false, pressureOpacity: true }
|
|
433
|
+
}),
|
|
434
|
+
builtin({
|
|
435
|
+
id: "eraser-hard",
|
|
436
|
+
name: "Hard eraser",
|
|
437
|
+
category: "eraser",
|
|
438
|
+
engine: "eraser",
|
|
439
|
+
tip: { size: 16, hardness: 0.9, spacing: 0.06, opacity: 1, flow: 1, pressureSize: true, pressureOpacity: false },
|
|
440
|
+
grain: 0.3,
|
|
441
|
+
dynamics: { minSize: 0.5 }
|
|
442
|
+
}),
|
|
443
|
+
builtin({
|
|
444
|
+
id: "blender",
|
|
445
|
+
name: "Blender",
|
|
446
|
+
category: "blender",
|
|
447
|
+
engine: "blender",
|
|
448
|
+
loadsColor: false,
|
|
449
|
+
tip: { size: 34, hardness: 0.4, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
|
|
450
|
+
blender: { strength: 0.5, distance: 0.35, mixing: "pigment" }
|
|
451
|
+
}),
|
|
452
|
+
builtin({
|
|
453
|
+
id: "smudge",
|
|
454
|
+
name: "Smudge",
|
|
455
|
+
category: "smudge",
|
|
456
|
+
engine: "smudge",
|
|
457
|
+
loadsColor: false,
|
|
458
|
+
tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
|
|
459
|
+
smudge: { strength: 0.75, length: 0.85, mixing: "rgb" }
|
|
460
|
+
}),
|
|
461
|
+
builtin({
|
|
462
|
+
id: "wet-smudge",
|
|
463
|
+
name: "Pigment smudge",
|
|
464
|
+
category: "smudge",
|
|
465
|
+
engine: "smudge",
|
|
466
|
+
loadsColor: false,
|
|
467
|
+
tip: { size: 30, hardness: 0.5, spacing: 0.06, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
|
|
468
|
+
smudge: { strength: 0.7, length: 0.9, mixing: "pigment" },
|
|
469
|
+
dynamics: { minSize: 0.6 }
|
|
470
|
+
}),
|
|
471
|
+
builtin({
|
|
472
|
+
id: "dirty-round",
|
|
473
|
+
name: "Dirty round",
|
|
474
|
+
category: "paint",
|
|
475
|
+
tip: { size: 22, hardness: 0.65, spacing: 0.06, opacity: 1, flow: 0.7, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
476
|
+
behavior: { pickup: 0.55, mix: 0.6, dirty: true, mixing: "pigment" },
|
|
477
|
+
dynamics: { minSize: 0.5 }
|
|
478
|
+
}),
|
|
479
|
+
builtin({
|
|
480
|
+
id: "multicolor-flat",
|
|
481
|
+
name: "Multi-colour flat",
|
|
482
|
+
category: "paint",
|
|
483
|
+
rotation: "stroke",
|
|
484
|
+
tip: { size: 30, hardness: 0.75, spacing: 0.05, angle: 90, roundness: 0.3, opacity: 1, flow: 0.85, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
485
|
+
behavior: { pickup: 0.15, mix: 0.4, mixing: "pigment" },
|
|
486
|
+
// The paint and second colours side by side across the tip.
|
|
487
|
+
paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "across-tip" }, softness: 0.7 },
|
|
488
|
+
dynamics: { minSize: 0.6 }
|
|
489
|
+
}),
|
|
490
|
+
builtin({
|
|
491
|
+
id: "palette-dabber",
|
|
492
|
+
name: "Palette dabber",
|
|
493
|
+
category: "paint",
|
|
494
|
+
tip: { size: 18, hardness: 0.7, spacing: 0.35, opacity: 1, flow: 0.9, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
|
|
495
|
+
// From the paint colour to the second colour and back, every 6 dabs.
|
|
496
|
+
paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 0.5, color: "secondary" }, { offset: 1, color: "paint" }], space: "pigment", map: { by: "distance", length: 12, repeat: "repeat", unit: "dabs" }, softness: 0.4 },
|
|
497
|
+
dynamics: { minSize: 0.5, sizeJitter: 0.15, scatter: 0.3 }
|
|
498
|
+
}),
|
|
499
|
+
builtin({
|
|
500
|
+
id: "gradient-round",
|
|
501
|
+
name: "Gradient round",
|
|
502
|
+
category: "paint",
|
|
503
|
+
tip: { size: 24, hardness: 0.8, spacing: 0.05, opacity: 1, flow: 0.9, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
504
|
+
// Changes colour as it is dragged out: paint → second colour over 300 px, and back.
|
|
505
|
+
paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 300, repeat: "mirror" } },
|
|
506
|
+
dynamics: { minSize: 0.5 }
|
|
507
|
+
}),
|
|
508
|
+
builtin({
|
|
509
|
+
id: "pressure-ombre",
|
|
510
|
+
name: "Pressure ombr\xE9",
|
|
511
|
+
category: "paint",
|
|
512
|
+
tip: { size: 26, hardness: 0.6, spacing: 0.05, opacity: 1, flow: 0.8, smoothing: 0.2, pressureSize: false, pressureOpacity: false },
|
|
513
|
+
// Light pressure lays the second colour, full pressure the paint colour.
|
|
514
|
+
paint: { type: "gradient", stops: [{ offset: 0, color: "secondary" }, { offset: 1, color: "paint" }], space: "pigment", map: { by: "pressure" } }
|
|
515
|
+
}),
|
|
516
|
+
builtin({
|
|
517
|
+
id: "rainbow-marker",
|
|
518
|
+
name: "Rainbow marker",
|
|
519
|
+
category: "marker",
|
|
520
|
+
rotation: "stroke",
|
|
521
|
+
tip: { size: 18, hardness: 0.9, spacing: 0.04, angle: 90, roundness: 0.4, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: false, pressureOpacity: false },
|
|
522
|
+
paint: {
|
|
523
|
+
type: "gradient",
|
|
524
|
+
stops: [
|
|
525
|
+
{ offset: 0, color: "#e11d48" },
|
|
526
|
+
{ offset: 0.2, color: "#f59e0b" },
|
|
527
|
+
{ offset: 0.4, color: "#facc15" },
|
|
528
|
+
{ offset: 0.6, color: "#22c55e" },
|
|
529
|
+
{ offset: 0.8, color: "#3b82f6" },
|
|
530
|
+
{ offset: 1, color: "#8b5cf6" }
|
|
531
|
+
],
|
|
532
|
+
space: "oklab",
|
|
533
|
+
map: { by: "distance", length: 600, repeat: "repeat" }
|
|
534
|
+
}
|
|
535
|
+
}),
|
|
536
|
+
builtin({
|
|
537
|
+
id: "mixer-brush",
|
|
538
|
+
name: "Mixer Brush",
|
|
539
|
+
category: "mixer",
|
|
540
|
+
engine: "mixer",
|
|
541
|
+
tip: { size: 30, hardness: 0.7, spacing: 0.1, opacity: 1, flow: 0.5, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
542
|
+
mixer: { wet: 0.5, load: 0.95, mix: 0.5, loadEachStroke: true, cleanEachStroke: true, mixing: "pigment" },
|
|
543
|
+
dynamics: { minSize: 0.5 }
|
|
544
|
+
}),
|
|
545
|
+
builtin({
|
|
546
|
+
id: "watercolor-round",
|
|
547
|
+
name: "Watercolour round",
|
|
548
|
+
category: "watercolor",
|
|
549
|
+
engine: "wet",
|
|
550
|
+
tip: { size: 28, hardness: 0.6, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
|
|
551
|
+
dynamics: { minSize: 0.35 },
|
|
552
|
+
behavior: { pickup: 0.55, mix: 0.5, mixing: "pigment" },
|
|
553
|
+
wet: { tool: "paint", water: 0.55, pigment: 0.5, body: 0, rewet: 0.2 }
|
|
554
|
+
}),
|
|
555
|
+
builtin({
|
|
556
|
+
id: "watercolor-wash",
|
|
557
|
+
name: "Wash brush",
|
|
558
|
+
category: "watercolor",
|
|
559
|
+
engine: "wet",
|
|
560
|
+
rotation: "stroke",
|
|
561
|
+
tip: { size: 90, hardness: 0.35, spacing: 0.1, angle: 90, roundness: 0.45, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: true },
|
|
562
|
+
behavior: { pickup: 0.45, mix: 0.45, mixing: "pigment" },
|
|
563
|
+
wet: { tool: "paint", water: 0.8, pigment: 0.45, body: 0, rewet: 0.2 }
|
|
564
|
+
}),
|
|
565
|
+
builtin({
|
|
566
|
+
id: "gradient-wash",
|
|
567
|
+
name: "Gradient wash",
|
|
568
|
+
category: "watercolor",
|
|
569
|
+
engine: "wet",
|
|
570
|
+
rotation: "stroke",
|
|
571
|
+
tip: { size: 80, hardness: 0.35, spacing: 0.1, angle: 90, roundness: 0.5, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: true },
|
|
572
|
+
behavior: { pickup: 0.4, mix: 0.4, mixing: "pigment" },
|
|
573
|
+
wet: { tool: "paint", water: 0.8, pigment: 0.45, body: 0, rewet: 0.2 },
|
|
574
|
+
// A graded wash: the paint colour runs into the second colour as the brush travels.
|
|
575
|
+
paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 500, repeat: "mirror" } }
|
|
576
|
+
}),
|
|
577
|
+
builtin({
|
|
578
|
+
id: "watercolor-detail",
|
|
579
|
+
name: "Watercolour rigger",
|
|
580
|
+
category: "watercolor",
|
|
581
|
+
engine: "wet",
|
|
582
|
+
tip: { size: 7, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.4, pressureSize: true, pressureOpacity: false },
|
|
583
|
+
dynamics: { minSize: 0.2 },
|
|
584
|
+
behavior: { pickup: 0.4, mix: 0.45, mixing: "pigment" },
|
|
585
|
+
wet: { tool: "paint", water: 0.4, pigment: 0.7, body: 0, rewet: 0.2 }
|
|
586
|
+
}),
|
|
587
|
+
builtin({
|
|
588
|
+
id: "gouache",
|
|
589
|
+
name: "Gouache",
|
|
590
|
+
category: "watercolor",
|
|
591
|
+
engine: "wet",
|
|
592
|
+
tip: { size: 26, hardness: 0.7, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
593
|
+
dynamics: { minSize: 0.5 },
|
|
594
|
+
behavior: { pickup: 0.65, mix: 0.6, mixing: "pigment" },
|
|
595
|
+
wet: { tool: "paint", water: 0.35, pigment: 0.8, body: 1, medium: "gouache", rewet: 0.3 }
|
|
596
|
+
}),
|
|
597
|
+
builtin({
|
|
598
|
+
id: "oil-round",
|
|
599
|
+
name: "Wet oil round",
|
|
600
|
+
category: "oil",
|
|
601
|
+
engine: "wet",
|
|
602
|
+
tip: { size: 24, hardness: 0.75, spacing: 0.08, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
603
|
+
dynamics: { minSize: 0.5 },
|
|
604
|
+
behavior: { pickup: 0.6, mix: 0.55, mixing: "pigment" },
|
|
605
|
+
wet: { tool: "paint", water: 0.4, pigment: 0.9, body: 1, medium: "oil", load: 0.97 }
|
|
606
|
+
}),
|
|
607
|
+
builtin({
|
|
608
|
+
id: "oil-flat",
|
|
609
|
+
name: "Wet oil flat",
|
|
610
|
+
category: "oil",
|
|
611
|
+
engine: "wet",
|
|
612
|
+
rotation: "stroke",
|
|
613
|
+
tip: { size: 40, hardness: 0.85, spacing: 0.06, angle: 90, roundness: 0.35, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
614
|
+
dynamics: { minSize: 0.6 },
|
|
615
|
+
behavior: { pickup: 0.65, mix: 0.6, mixing: "pigment" },
|
|
616
|
+
wet: { tool: "paint", water: 0.45, pigment: 0.9, body: 1, medium: "oil", load: 0.95 }
|
|
617
|
+
}),
|
|
618
|
+
builtin({
|
|
619
|
+
id: "impasto-brush",
|
|
620
|
+
name: "Impasto brush",
|
|
621
|
+
category: "impasto",
|
|
622
|
+
engine: "impasto",
|
|
623
|
+
rotation: "stroke",
|
|
624
|
+
tip: { size: 36, hardness: 0.8, spacing: 0.06, angle: 90, roundness: 0.55, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
625
|
+
dynamics: { minSize: 0.55 },
|
|
626
|
+
behavior: { pickup: 0.4, mix: 0.5, mixing: "pigment" },
|
|
627
|
+
impasto: { tool: "brush", thickness: 0.6, gloss: 0.45, load: 0.92, drag: 0.4 }
|
|
628
|
+
}),
|
|
629
|
+
builtin({
|
|
630
|
+
id: "paint-tube",
|
|
631
|
+
name: "Paint tube",
|
|
632
|
+
category: "impasto",
|
|
633
|
+
engine: "impasto",
|
|
634
|
+
tip: { size: 22, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.35, pressureSize: true, pressureOpacity: false },
|
|
635
|
+
dynamics: { minSize: 0.4 },
|
|
636
|
+
impasto: { tool: "tube", thickness: 0.9, gloss: 0.5, load: 1, drag: 0.1 }
|
|
637
|
+
}),
|
|
638
|
+
builtin({
|
|
639
|
+
id: "palette-knife",
|
|
640
|
+
name: "Palette knife",
|
|
641
|
+
category: "impasto",
|
|
642
|
+
engine: "impasto",
|
|
643
|
+
rotation: "stroke",
|
|
644
|
+
tip: { size: 48, hardness: 1, spacing: 0.05, angle: 90, roundness: 0.18, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
|
|
645
|
+
impasto: { tool: "knife", thickness: 0.3, gloss: 0.55, load: 1, drag: 0.85 }
|
|
646
|
+
}),
|
|
647
|
+
builtin({
|
|
648
|
+
id: "paint-roller",
|
|
649
|
+
name: "Roller",
|
|
650
|
+
category: "impasto",
|
|
651
|
+
engine: "impasto",
|
|
652
|
+
rotation: "stroke",
|
|
653
|
+
tip: { size: 70, hardness: 0.95, spacing: 0.04, angle: 90, roundness: 0.12, opacity: 1, flow: 1, smoothing: 0.4, pressureSize: false, pressureOpacity: false },
|
|
654
|
+
impasto: { tool: "roller", thickness: 0.25, gloss: 0.3, load: 0.9, drag: 0.15 }
|
|
655
|
+
}),
|
|
656
|
+
builtin({
|
|
657
|
+
id: "gloop-pen",
|
|
658
|
+
name: "Gloop pen",
|
|
659
|
+
category: "impasto",
|
|
660
|
+
engine: "impasto",
|
|
661
|
+
tip: { size: 14, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
|
|
662
|
+
dynamics: { minSize: 0.5 },
|
|
663
|
+
impasto: { tool: "gloop", thickness: 1, gloss: 0.9, load: 1, drag: 0 }
|
|
664
|
+
}),
|
|
665
|
+
builtin({
|
|
666
|
+
id: "metallic-paint",
|
|
667
|
+
name: "Metallic paint",
|
|
668
|
+
category: "impasto",
|
|
669
|
+
engine: "impasto",
|
|
670
|
+
rotation: "stroke",
|
|
671
|
+
tip: { size: 30, hardness: 0.8, spacing: 0.06, angle: 90, roundness: 0.6, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
672
|
+
dynamics: { minSize: 0.5 },
|
|
673
|
+
behavior: { pickup: 0.2, mix: 0.4, mixing: "pigment" },
|
|
674
|
+
impasto: { tool: "brush", thickness: 0.45, gloss: 0.8, metallic: 1, load: 0.95, drag: 0.3 }
|
|
675
|
+
}),
|
|
676
|
+
builtin({
|
|
677
|
+
id: "hair-strands",
|
|
678
|
+
name: "Hair strands",
|
|
679
|
+
category: "procedural",
|
|
680
|
+
engine: "particle",
|
|
681
|
+
tip: { size: 40, hardness: 0.8, opacity: 0.9, flow: 0.6, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
|
|
682
|
+
particle: { kind: "string", render: "line", count: 22, size: 0.025, spread: 1, stiffness: 0.08, damping: 0.3 }
|
|
683
|
+
}),
|
|
684
|
+
builtin({
|
|
685
|
+
id: "spring-ink",
|
|
686
|
+
name: "Spring ink",
|
|
687
|
+
category: "procedural",
|
|
688
|
+
engine: "particle",
|
|
689
|
+
tip: { size: 30, hardness: 0.9, opacity: 1, flow: 0.8, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
|
|
690
|
+
particle: { kind: "spring", render: "line", count: 5, size: 0.08, spread: 0.6, stiffness: 0.2, damping: 0.1 }
|
|
691
|
+
}),
|
|
692
|
+
builtin({
|
|
693
|
+
id: "orbit-curls",
|
|
694
|
+
name: "Orbit curls",
|
|
695
|
+
category: "procedural",
|
|
696
|
+
engine: "particle",
|
|
697
|
+
tip: { size: 36, hardness: 0.85, opacity: 0.9, flow: 0.7, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
698
|
+
particle: { kind: "orbit", render: "line", count: 4, size: 0.05, spread: 0.6, speed: 0.45 }
|
|
699
|
+
}),
|
|
700
|
+
builtin({
|
|
701
|
+
id: "flow-fur",
|
|
702
|
+
name: "Flow fur",
|
|
703
|
+
category: "procedural",
|
|
704
|
+
engine: "particle",
|
|
705
|
+
tip: { size: 50, hardness: 0.7, opacity: 0.8, flow: 0.5, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
|
|
706
|
+
particle: { kind: "flow", render: "line", count: 10, size: 0.03, spread: 0.6, speed: 0.5, life: 0.25 }
|
|
707
|
+
}),
|
|
708
|
+
builtin({
|
|
709
|
+
id: "bead-trail",
|
|
710
|
+
name: "Bead trail",
|
|
711
|
+
category: "procedural",
|
|
712
|
+
engine: "particle",
|
|
713
|
+
tip: { size: 34, hardness: 0.9, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
714
|
+
particle: { kind: "spring", render: "sequence", count: 3, size: 0.18, spread: 0.5, stiffness: 0.3, damping: 0.3, interval: 0.4 }
|
|
715
|
+
}),
|
|
716
|
+
builtin({
|
|
717
|
+
id: "sparkle-dust",
|
|
718
|
+
name: "Sparkle dust",
|
|
719
|
+
category: "procedural",
|
|
720
|
+
engine: "particle",
|
|
721
|
+
tip: { size: 60, hardness: 0.6, opacity: 1, flow: 0.9, smoothing: 0.1, pressureSize: true, pressureOpacity: true },
|
|
722
|
+
particle: { kind: "flow", render: "dot", count: 6, size: 0.05, spread: 0.9, speed: 0.7, life: 0.4 }
|
|
723
|
+
}),
|
|
724
|
+
builtin({
|
|
725
|
+
id: "bristle-round",
|
|
726
|
+
name: "Bristle round",
|
|
727
|
+
category: "bristle",
|
|
728
|
+
engine: "bristle",
|
|
729
|
+
rotation: "stroke",
|
|
730
|
+
tip: { size: 30, hardness: 0.85, opacity: 1, flow: 0.9, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
731
|
+
dynamics: { minSize: 0.4, tiltSize: 0.6, tiltRoundness: 0.6 },
|
|
732
|
+
bristle: { count: 28, stiffness: 0.45, splay: 0.6, thickness: 0.035, load: 0.7, jitter: 0.5 }
|
|
733
|
+
}),
|
|
734
|
+
builtin({
|
|
735
|
+
id: "bristle-flat",
|
|
736
|
+
name: "Bristle flat",
|
|
737
|
+
category: "bristle",
|
|
738
|
+
engine: "bristle",
|
|
739
|
+
rotation: "stroke",
|
|
740
|
+
tip: { size: 46, hardness: 0.85, angle: 90, roundness: 0.3, opacity: 1, flow: 0.9, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
741
|
+
dynamics: { minSize: 0.5, tiltSize: 0.5 },
|
|
742
|
+
bristle: { count: 40, stiffness: 0.6, splay: 0.4, thickness: 0.025, load: 0.65, jitter: 0.45 }
|
|
743
|
+
}),
|
|
744
|
+
builtin({
|
|
745
|
+
id: "dry-brush",
|
|
746
|
+
name: "Dry brush",
|
|
747
|
+
category: "bristle",
|
|
748
|
+
engine: "bristle",
|
|
749
|
+
rotation: "stroke",
|
|
750
|
+
tip: { size: 44, hardness: 0.9, angle: 90, roundness: 0.45, opacity: 1, flow: 0.8, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
751
|
+
grain: 0.6,
|
|
752
|
+
bristle: { count: 48, stiffness: 0.35, splay: 0.7, thickness: 0.05, load: 0.15, jitter: 0.8 }
|
|
753
|
+
}),
|
|
754
|
+
// Light paint: brushes that lay light with their colour (glow and spill; see light.ts).
|
|
755
|
+
builtin({
|
|
756
|
+
id: "glow-gel",
|
|
757
|
+
name: "Glow gel",
|
|
758
|
+
category: "light",
|
|
759
|
+
engine: "impasto",
|
|
760
|
+
tip: { size: 16, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
|
|
761
|
+
dynamics: { minSize: 0.6 },
|
|
762
|
+
impasto: { tool: "gloop", thickness: 1, gloss: 0.95, load: 1, drag: 0, rim: 0.8, clarity: 0.5 },
|
|
763
|
+
light: { amount: 0.85 }
|
|
764
|
+
}),
|
|
765
|
+
builtin({
|
|
766
|
+
id: "neon-marker",
|
|
767
|
+
name: "Neon marker",
|
|
768
|
+
category: "light",
|
|
769
|
+
tip: { size: 10, hardness: 0.9, spacing: 0.04, opacity: 1, flow: 1, smoothing: 0.45, pressureSize: false, pressureOpacity: false },
|
|
770
|
+
light: { amount: 1 }
|
|
771
|
+
}),
|
|
772
|
+
builtin({
|
|
773
|
+
id: "light-airbrush",
|
|
774
|
+
name: "Light airbrush",
|
|
775
|
+
category: "light",
|
|
776
|
+
tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
|
|
777
|
+
pressureSmoothing: 0.3,
|
|
778
|
+
dynamics: { pressureFlow: 1 },
|
|
779
|
+
light: { amount: 0.7, pressure: true }
|
|
780
|
+
}),
|
|
781
|
+
builtin({
|
|
782
|
+
id: "spark-trail",
|
|
783
|
+
name: "Spark trail",
|
|
784
|
+
category: "light",
|
|
785
|
+
engine: "particle",
|
|
786
|
+
tip: { size: 50, hardness: 0.7, opacity: 1, flow: 1, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
|
|
787
|
+
particle: { kind: "flow", render: "dot", count: 8, size: 0.06, spread: 0.7, speed: 0.9, life: 0.3 },
|
|
788
|
+
light: { amount: 1 }
|
|
789
|
+
}),
|
|
790
|
+
builtin({
|
|
791
|
+
// Silk of light: strands dragged behind the pen on slow waves, the bands between them filled.
|
|
792
|
+
id: "light-ribbon",
|
|
793
|
+
name: "Light ribbon",
|
|
794
|
+
category: "light",
|
|
795
|
+
engine: "particle",
|
|
796
|
+
tip: { size: 80, hardness: 0.8, opacity: 0.9, flow: 1, smoothing: 0.35, pressureSize: false, pressureOpacity: false },
|
|
797
|
+
particle: { kind: "string", render: "ribbon", count: 5, size: 0.015, spread: 1, stiffness: 0.05, damping: 0.3, ribbonFill: 0.45, waveAmplitude: 0.25, waveLength: 220, wavePhaseJitter: 0.25 },
|
|
798
|
+
light: { amount: 0.8 }
|
|
799
|
+
}),
|
|
800
|
+
builtin({
|
|
801
|
+
// Flames: short-lived bands thrown off the pen, rising and curling in the flow.
|
|
802
|
+
id: "flame-ribbon",
|
|
803
|
+
name: "Flame ribbon",
|
|
804
|
+
category: "light",
|
|
805
|
+
engine: "particle",
|
|
806
|
+
tip: { size: 50, hardness: 0.6, opacity: 0.85, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
807
|
+
particle: { kind: "flow", render: "ribbon", count: 4, size: 0.012, spread: 0.5, speed: 0.6, gravity: -0.6, life: 0.2, ribbonFill: 0.5 },
|
|
808
|
+
light: { amount: 1 }
|
|
809
|
+
}),
|
|
810
|
+
builtin({
|
|
811
|
+
// A neon tube that lags and snaps back, with a slow shimmer.
|
|
812
|
+
id: "neon-strand",
|
|
813
|
+
name: "Neon strand",
|
|
814
|
+
category: "light",
|
|
815
|
+
engine: "particle",
|
|
816
|
+
tip: { size: 40, hardness: 0.9, opacity: 1, flow: 1, smoothing: 0.4, pressureSize: false, pressureOpacity: false },
|
|
817
|
+
particle: { kind: "elastic", render: "line", count: 3, size: 0.05, spread: 0.25, stiffness: 0.6, damping: 0.3, waveAmplitude: 0.06, waveLength: 120 },
|
|
818
|
+
light: { amount: 1 }
|
|
819
|
+
}),
|
|
820
|
+
builtin({
|
|
821
|
+
// Smoke lit from within: thin bands thrown into a slow, rising flow.
|
|
822
|
+
id: "smoke-ribbon",
|
|
823
|
+
name: "Smoke ribbon",
|
|
824
|
+
category: "light",
|
|
825
|
+
engine: "particle",
|
|
826
|
+
tip: { size: 70, hardness: 0.3, opacity: 0.45, flow: 1, smoothing: 0.3, pressureSize: true, pressureOpacity: false },
|
|
827
|
+
particle: { kind: "flow", render: "ribbon", count: 3, size: 5e-3, spread: 0.5, speed: 0.15, gravity: -0.15, life: 0.35, ribbonFill: 0.25, waveAmplitude: 0.1, waveLength: 140, wavePhaseJitter: 1 },
|
|
828
|
+
light: { amount: 0.3 }
|
|
829
|
+
}),
|
|
830
|
+
builtin({
|
|
831
|
+
// Out-of-focus lights: soft discs and rings spaced along springy paths.
|
|
832
|
+
id: "bokeh-dots",
|
|
833
|
+
name: "Bokeh dots",
|
|
834
|
+
category: "light",
|
|
835
|
+
engine: "particle",
|
|
836
|
+
tip: { size: 80, hardness: 0.7, opacity: 0.6, flow: 1, smoothing: 0.3, pressureSize: true, pressureOpacity: false },
|
|
837
|
+
particle: { kind: "spring", render: "sequence", count: 3, size: 0.3, spread: 1, stiffness: 0.15, damping: 0.4, interval: 1, dotTextures: ["dot", "dot", "ring"], dotTextureOrder: "random" },
|
|
838
|
+
light: { amount: 0.9 }
|
|
839
|
+
}),
|
|
840
|
+
builtin({
|
|
841
|
+
// Sparks: twinkles and stars thrown into the flow, spaced along it.
|
|
842
|
+
id: "twinkle-sparks",
|
|
843
|
+
name: "Twinkle sparks",
|
|
844
|
+
category: "light",
|
|
845
|
+
engine: "particle",
|
|
846
|
+
tip: { size: 50, hardness: 0.8, opacity: 1, flow: 1, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
|
|
847
|
+
particle: { kind: "flow", render: "sequence", count: 3, size: 0.25, spread: 0.8, speed: 0.9, life: 0.35, interval: 0.6, dotTextures: ["sparkle", "sparkle", "star"], dotTextureOrder: "random", dotRotation: "random" },
|
|
848
|
+
light: { amount: 1 }
|
|
849
|
+
}),
|
|
850
|
+
builtin({
|
|
851
|
+
id: "glow-wash",
|
|
852
|
+
name: "Glow wash",
|
|
853
|
+
category: "light",
|
|
854
|
+
engine: "wet",
|
|
855
|
+
tip: { size: 40, hardness: 0.55, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
|
|
856
|
+
dynamics: { minSize: 0.35 },
|
|
857
|
+
behavior: { pickup: 0.4, mix: 0.5, mixing: "pigment" },
|
|
858
|
+
wet: { tool: "paint", water: 0.65, pigment: 0.75, body: 0, rewet: 0.1 },
|
|
859
|
+
light: { amount: 0.7 }
|
|
860
|
+
}),
|
|
861
|
+
builtin({
|
|
862
|
+
// Fluorescent ink: a transparent chisel marker that multiplies (text under it stays dark) and
|
|
863
|
+
// glows a little brighter than the paper. Turn the layer's spill off (set_light_spill radius 0).
|
|
864
|
+
id: "highlighter",
|
|
865
|
+
name: "Highlighter",
|
|
866
|
+
category: "light",
|
|
867
|
+
tip: { size: 30, hardness: 0.85, spacing: 0.04, angle: 45, roundness: 0.35, opacity: 0.6, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: false, blend: "multiply" },
|
|
868
|
+
grain: 0.05,
|
|
869
|
+
light: { amount: 0.5 }
|
|
870
|
+
}),
|
|
871
|
+
builtin({
|
|
872
|
+
id: "sticker-stars",
|
|
873
|
+
name: "Star stickers",
|
|
874
|
+
category: "sticker",
|
|
875
|
+
engine: "sticker",
|
|
876
|
+
tip: { size: 36, opacity: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
877
|
+
dynamics: { minSize: 0.5 },
|
|
878
|
+
sticker: { stamps: ["star", "sparkle"], density: 0.35, sizeJitter: 0.5, rotationJitter: 1, scatter: 0.6, colorJitter: 0.3 }
|
|
879
|
+
}),
|
|
880
|
+
builtin({
|
|
881
|
+
id: "sticker-leaves",
|
|
882
|
+
name: "Leaf stickers",
|
|
883
|
+
category: "sticker",
|
|
884
|
+
engine: "sticker",
|
|
885
|
+
rotation: "stroke",
|
|
886
|
+
tip: { size: 40, opacity: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
|
|
887
|
+
dynamics: { minSize: 0.5 },
|
|
888
|
+
sticker: { stamps: ["leaf"], density: 0.5, sizeJitter: 0.4, rotationJitter: 0.35, scatter: 0.4, colorJitter: 0.35 }
|
|
889
|
+
}),
|
|
890
|
+
builtin({
|
|
891
|
+
id: "sticker-confetti",
|
|
892
|
+
name: "Confetti",
|
|
893
|
+
category: "sticker",
|
|
894
|
+
engine: "sticker",
|
|
895
|
+
tip: { size: 22, opacity: 1, smoothing: 0.1, pressureSize: false, pressureOpacity: false },
|
|
896
|
+
sticker: { stamps: ["confetti", "dot", "ring"], density: 0.7, sizeJitter: 0.5, rotationJitter: 1, scatter: 1.5, colorJitter: 1 }
|
|
897
|
+
}),
|
|
898
|
+
builtin({
|
|
899
|
+
id: "sticker-flowers",
|
|
900
|
+
name: "Flower stickers",
|
|
901
|
+
category: "sticker",
|
|
902
|
+
engine: "sticker",
|
|
903
|
+
tip: { size: 44, opacity: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
904
|
+
sticker: { stamps: ["flower", "heart"], density: 0.3, sizeJitter: 0.5, rotationJitter: 1, scatter: 0.7, colorJitter: 0.4 }
|
|
905
|
+
}),
|
|
906
|
+
builtin({
|
|
907
|
+
id: "sticker-splats",
|
|
908
|
+
name: "Paint splats",
|
|
909
|
+
category: "sticker",
|
|
910
|
+
engine: "sticker",
|
|
911
|
+
tip: { size: 50, opacity: 0.95, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
|
|
912
|
+
sticker: { stamps: ["splat"], density: 0.25, sizeJitter: 0.6, rotationJitter: 1, scatter: 0.8, colorJitter: 0.15 }
|
|
913
|
+
}),
|
|
914
|
+
builtin({
|
|
915
|
+
id: "add-water",
|
|
916
|
+
name: "Add water",
|
|
917
|
+
category: "wet-tool",
|
|
918
|
+
engine: "wet",
|
|
919
|
+
tip: { size: 40, hardness: 0.4, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
|
|
920
|
+
dynamics: { minSize: 0.5 },
|
|
921
|
+
wet: { tool: "water", water: 0.6, pigment: 0, rewet: 0.3 }
|
|
922
|
+
}),
|
|
923
|
+
builtin({
|
|
924
|
+
id: "hair-dryer",
|
|
925
|
+
name: "Dry",
|
|
926
|
+
category: "wet-tool",
|
|
927
|
+
engine: "wet",
|
|
928
|
+
tip: { size: 80, hardness: 0.1, spacing: 0.15, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: true },
|
|
929
|
+
wet: { tool: "dry", strength: 0.5 }
|
|
930
|
+
}),
|
|
931
|
+
builtin({
|
|
932
|
+
id: "blow",
|
|
933
|
+
name: "Blow",
|
|
934
|
+
category: "wet-tool",
|
|
935
|
+
engine: "wet",
|
|
936
|
+
tip: { size: 50, hardness: 0.2, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: true },
|
|
937
|
+
wet: { tool: "blow", strength: 0.6 }
|
|
938
|
+
}),
|
|
939
|
+
builtin({
|
|
940
|
+
id: "wet-blend",
|
|
941
|
+
name: "Wet blend",
|
|
942
|
+
category: "wet-tool",
|
|
943
|
+
engine: "wet",
|
|
944
|
+
tip: { size: 36, hardness: 0.3, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: false, pressureOpacity: true },
|
|
945
|
+
wet: { tool: "blend", strength: 0.4 }
|
|
946
|
+
}),
|
|
947
|
+
builtin({
|
|
948
|
+
id: "wet-drag",
|
|
949
|
+
name: "Wet smudge",
|
|
950
|
+
category: "wet-tool",
|
|
951
|
+
engine: "wet",
|
|
952
|
+
tip: { size: 30, hardness: 0.5, spacing: 0.08, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: false, pressureOpacity: true },
|
|
953
|
+
wet: { tool: "smudge", strength: 0.6 }
|
|
954
|
+
}),
|
|
955
|
+
builtin({
|
|
956
|
+
id: "blot",
|
|
957
|
+
name: "Blot (erase wet)",
|
|
958
|
+
category: "wet-tool",
|
|
959
|
+
engine: "wet",
|
|
960
|
+
tip: { size: 40, hardness: 0.5, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
|
|
961
|
+
wet: { tool: "blot", strength: 0.7 }
|
|
962
|
+
})
|
|
963
|
+
];
|
|
964
|
+
var DEFAULT_BRUSH_ID = "pencil-6b";
|
|
965
|
+
function brushToJSON(brush) {
|
|
966
|
+
const { builtin: _b, ...rest } = brush;
|
|
967
|
+
return JSON.parse(JSON.stringify(rest));
|
|
968
|
+
}
|
|
969
|
+
function parseBrushLibrary(raw, problems = []) {
|
|
970
|
+
const list2 = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.brushes) ? raw.brushes : null;
|
|
971
|
+
if (!list2) throw new Error('Not a brush library: expected {"format":"pixerate-brushes","version":1,"brushes":[...]} or an array of brushes.');
|
|
972
|
+
const out = [];
|
|
973
|
+
list2.forEach((item, i) => {
|
|
974
|
+
if (!item || typeof item !== "object") {
|
|
975
|
+
problems.push(`brushes[${i}] is not an object; skipped.`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const r = item;
|
|
979
|
+
if (typeof r.id !== "string" || !BRUSH_ID.test(r.id)) {
|
|
980
|
+
problems.push(`brushes[${i}] has no valid id (lowercase letters, digits and dashes); skipped.`);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (typeof r.name !== "string" || !r.name.trim()) {
|
|
984
|
+
problems.push(`brushes[${i}] ("${r.id}") has no name; skipped.`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const { builtin: _b, ...rest } = r;
|
|
988
|
+
out.push(normalizePaintBrush(rest));
|
|
989
|
+
});
|
|
990
|
+
return out;
|
|
991
|
+
}
|
|
992
|
+
|
|
32
993
|
// src/brushes/resolve.ts
|
|
33
994
|
var ALL = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
|
|
34
995
|
var EXCHANGE = ["dab", "wet", "impasto"];
|
|
@@ -241,11 +1202,11 @@ var BRUSH_SURFACES = ["dry", "wet", "thick"];
|
|
|
241
1202
|
// src/brushes/schema.ts
|
|
242
1203
|
var MAX_BRUSH_PALETTE = 16;
|
|
243
1204
|
var BRUSH_WORD = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
244
|
-
var
|
|
1205
|
+
var unit3 = (description) => ({ type: "number", minimum: 0, maximum: 1, description });
|
|
245
1206
|
var num = (minimum, maximum, description) => ({ type: "number", minimum, maximum, description });
|
|
246
1207
|
var int = (minimum, maximum, description) => ({ type: "integer", minimum, maximum, description });
|
|
247
1208
|
var bool = (description) => ({ type: "boolean", description });
|
|
248
|
-
var
|
|
1209
|
+
var oneOf2 = (values, description) => ({ type: "string", enum: [...values], description });
|
|
249
1210
|
var object = (description, properties, required) => ({
|
|
250
1211
|
type: "object",
|
|
251
1212
|
description,
|
|
@@ -253,7 +1214,7 @@ var object = (description, properties, required) => ({
|
|
|
253
1214
|
...required ? { required } : {},
|
|
254
1215
|
additionalProperties: false
|
|
255
1216
|
});
|
|
256
|
-
var MIXING =
|
|
1217
|
+
var MIXING = oneOf2(["pigment", "rgb"], "How colours mix: pigment (Kubelka-Munk, blue and yellow make green) or rgb (straight averages). Default pigment; a Mixer Brush and the smudge tool default to rgb.");
|
|
257
1218
|
var BRUSH_STAMP_PATTERN = `^(?:[^:\\s]+:\\S+|${STAMP_KINDS.join("|")})$`;
|
|
258
1219
|
var STAMP = {
|
|
259
1220
|
type: "string",
|
|
@@ -262,24 +1223,24 @@ var STAMP = {
|
|
|
262
1223
|
};
|
|
263
1224
|
var BRISTLES = object("Bristle tips: the tuft. Missing fields take their defaults.", {
|
|
264
1225
|
count: int(3, 128, "Bristles in the tuft (default 32)."),
|
|
265
|
-
stiffness:
|
|
266
|
-
splay:
|
|
267
|
-
thickness:
|
|
268
|
-
load:
|
|
269
|
-
jitter:
|
|
1226
|
+
stiffness: unit3("Stiff bristles follow the tip; soft ones lag and bend into curves at turns (default 0.5)."),
|
|
1227
|
+
splay: unit3("How far pressure spreads the tuft (default 0.5)."),
|
|
1228
|
+
thickness: unit3("Each bristle's line width as a fraction of the tip size (default 0.06)."),
|
|
1229
|
+
load: unit3("Paint per bristle: 1 never runs dry; lower, bristles run out along the stroke, a dry brush (default 0.6)."),
|
|
1230
|
+
jitter: unit3("Random variation between bristles: length, paint, position (default 0.4).")
|
|
270
1231
|
});
|
|
271
1232
|
var TIP = object(
|
|
272
1233
|
"The tip: its shape and the dab it lays.",
|
|
273
1234
|
{
|
|
274
|
-
shape:
|
|
1235
|
+
shape: oneOf2(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp: textured stamps (see stamps), laid as objects or particles."),
|
|
275
1236
|
size: num(1, MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
|
|
276
|
-
hardness:
|
|
1237
|
+
hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1)."),
|
|
277
1238
|
spacing: num(0.01, 10, "Dab spacing as a fraction of the diameter: 0.05 smooth, 1 dotted (default 0.1)."),
|
|
278
1239
|
angle: num(-360, 360, "Tip angle in degrees, counter-clockwise; added to the stroke direction or barrel with dynamics.rotation (default 0)."),
|
|
279
1240
|
roundness: num(0.01, 1, "Roundness: 1 round (default), lower a flat ellipse (pastel, chisel)."),
|
|
280
|
-
opacity:
|
|
281
|
-
flow:
|
|
282
|
-
blend:
|
|
1241
|
+
opacity: unit3("Stroke opacity ceiling: overlapping dabs never exceed it (default 1)."),
|
|
1242
|
+
flow: unit3("Build-up per dab towards the opacity: low flow builds softly, like an airbrush (default 1)."),
|
|
1243
|
+
blend: oneOf2(
|
|
283
1244
|
BLEND_MODES.map((m) => m.id),
|
|
284
1245
|
"How the stroke paints onto the layer (default normal). pigment mixes like real paint; multiply darkens like marker ink; watercolor glazes; tint, highlight and shadow recolour, light and shade."
|
|
285
1246
|
),
|
|
@@ -299,27 +1260,27 @@ var PARTICLES = object(
|
|
|
299
1260
|
"Particles that move with the pen and draw as they go. Missing fields take their defaults.",
|
|
300
1261
|
{
|
|
301
1262
|
kind: { type: "string", const: "particles", description: 'Always "particles".' },
|
|
302
|
-
physics:
|
|
1263
|
+
physics: oneOf2(
|
|
303
1264
|
PARTICLE_KINDS,
|
|
304
1265
|
"How the particles move (default spring). string: strands dragged behind the pen (hair, fur); spring: on springs to the pen, overshooting; orbit: circling the pen (curls); flow: thrown into a swirling flow until they fade; elastic: links that stretch and snap back; fuzzy: winding strings waving out of step."
|
|
305
1266
|
),
|
|
306
|
-
render:
|
|
1267
|
+
render: oneOf2(PARTICLE_RENDERS, "How they draw (default line). dot: a mark per step; line: a continuous line each; sequence: beads spaced along each path; ribbon: the band between neighbouring paths filled."),
|
|
307
1268
|
count: int(1, 64, "Particles (flow: thrown per tip of travel). Default 12."),
|
|
308
|
-
size:
|
|
309
|
-
spread:
|
|
310
|
-
stiffness:
|
|
311
|
-
damping:
|
|
312
|
-
speed:
|
|
1269
|
+
size: unit3("Mark size as a fraction of the tip size; ribbon: its edge width (default 0.08)."),
|
|
1270
|
+
spread: unit3("How far apart they start, as a fraction of the tip size; ribbon: its width (default 0.5)."),
|
|
1271
|
+
stiffness: unit3("Spring, string and elastic stiffness: stiff follows closely, soft swings wide (default 0.4)."),
|
|
1272
|
+
damping: unit3("How quickly motion dies down (default 0.25)."),
|
|
1273
|
+
speed: unit3("Orbit and flow speed (default 0.5)."),
|
|
313
1274
|
gravity: num(-1, 1, "Pull downwards; negative pulls up (default 0)."),
|
|
314
|
-
life:
|
|
315
|
-
interval:
|
|
316
|
-
ribbonFill:
|
|
317
|
-
ribbonPairs:
|
|
318
|
-
waveAmplitude:
|
|
1275
|
+
life: unit3("Flow particles: how long they live (default 0.5)."),
|
|
1276
|
+
interval: unit3("Sequence: spacing of the beads (default 0.3)."),
|
|
1277
|
+
ribbonFill: unit3("Ribbon: fill opacity between the paths, relative to their edges (default 0.5; 0 only edges)."),
|
|
1278
|
+
ribbonPairs: oneOf2(RIBBON_PAIRS, "Ribbon: adjacent (default) joins each particle to its neighbour; first-last makes one band between the outermost two."),
|
|
1279
|
+
waveAmplitude: unit3("Wave: sideways swing as a fraction of the tip size (default 0: no wave)."),
|
|
319
1280
|
waveLength: num(4, 2e3, "Wave: wavelength in px of pen travel (default 60)."),
|
|
320
|
-
wavePhaseJitter:
|
|
321
|
-
stampOrder:
|
|
322
|
-
stampRotation:
|
|
1281
|
+
wavePhaseJitter: unit3("Wave: how far out of step the particles' waves are; 0 in step, 1 random phases (default 0)."),
|
|
1282
|
+
stampOrder: oneOf2(DOT_TEXTURE_ORDERS, "Stamp tips: sequence (default) uses the stamps in turn; random picks one per dot, seeded."),
|
|
1283
|
+
stampRotation: oneOf2(DOT_ROTATIONS, "Stamp tips: follow turns each with its particle's motion; random (default); fixed uses the tip angle.")
|
|
323
1284
|
},
|
|
324
1285
|
["kind"]
|
|
325
1286
|
);
|
|
@@ -327,11 +1288,11 @@ var OBJECTS = object(
|
|
|
327
1288
|
"Each mark stays an editable object (a sticker) on a sticker layer; needs a stamp tip. Missing fields take their defaults.",
|
|
328
1289
|
{
|
|
329
1290
|
kind: { type: "string", const: "objects", description: 'Always "objects".' },
|
|
330
|
-
density:
|
|
331
|
-
sizeJitter:
|
|
332
|
-
rotationJitter:
|
|
1291
|
+
density: unit3("About density \xD7 4 marks per tip diameter of travel; 0.25 \u2248 one per diameter (default 0.4)."),
|
|
1292
|
+
sizeJitter: unit3("Random size change (default 0.4)."),
|
|
1293
|
+
rotationJitter: unit3("Random turn as a fraction of \xB1180\xB0 (default 1)."),
|
|
333
1294
|
scatter: num(0, 2, "How far from the path marks land, in tip diameters (default 0.5)."),
|
|
334
|
-
colourJitter:
|
|
1295
|
+
colourJitter: unit3("Random colour change: at 1 up to \xB140\xB0 of hue and \xB10.15 OKLab lightness (default 0.2).")
|
|
335
1296
|
},
|
|
336
1297
|
["kind"]
|
|
337
1298
|
);
|
|
@@ -340,62 +1301,62 @@ var DYNAMICS = object("Pen feel: pressure, tilt, velocity, jitter, rotation and
|
|
|
340
1301
|
pressureOpacity: bool("Pressure scales the opacity (default false)."),
|
|
341
1302
|
smoothing: num(0, 0.95, "Position smoothing of the input (default 0)."),
|
|
342
1303
|
pressureSmoothing: num(0, 0.95, "Pressure smoothing, separate from position smoothing (default 0)."),
|
|
343
|
-
rotation:
|
|
344
|
-
minSize:
|
|
345
|
-
pressureFlow:
|
|
346
|
-
tiltSize:
|
|
347
|
-
tiltRoundness:
|
|
348
|
-
tiltOpacity:
|
|
1304
|
+
rotation: oneOf2(BRUSH_ROTATIONS, "What turns the tip: fixed (tip.angle, default), stroke (direction of travel) or barrel (pen barrel rotation)."),
|
|
1305
|
+
minSize: unit3("Size at zero pressure as a fraction of the full size, with pressureSize (default 0)."),
|
|
1306
|
+
pressureFlow: unit3("How much pressure scales flow (default 0)."),
|
|
1307
|
+
tiltSize: unit3("Laying the pen down widens the tip, up to 2.5\xD7 (default 0)."),
|
|
1308
|
+
tiltRoundness: unit3("Laying the pen down flattens the tip along the tilt, side-of-the-pencil shading (default 0)."),
|
|
1309
|
+
tiltOpacity: unit3("Laying the pen down lightens the mark (default 0)."),
|
|
349
1310
|
velocitySize: num(-1, 1, "Positive grows the tip on fast strokes, negative thins it; needs timed points (default 0)."),
|
|
350
1311
|
velocityOpacity: num(-1, 1, "Positive darkens fast marks, negative lightens them; needs timed points (default 0)."),
|
|
351
|
-
sizeJitter:
|
|
352
|
-
angleJitter:
|
|
353
|
-
opacityJitter:
|
|
1312
|
+
sizeJitter: unit3("Random size change per dab (default 0)."),
|
|
1313
|
+
angleJitter: unit3("Random angle per dab, as a fraction of \xB1180\xB0 (default 0)."),
|
|
1314
|
+
opacityJitter: unit3("Random opacity drop per dab (default 0)."),
|
|
354
1315
|
scatter: num(0, 4, "Random offset across the stroke, in diameters (default 0).")
|
|
355
1316
|
});
|
|
356
1317
|
var EXCHANGE2 = object("Paint exchange with the canvas: picking up paint and mixing it into what the brush lays. Paint brushes on a dry, wet or thick surface and Mixer Brushes.", {
|
|
357
|
-
pickup:
|
|
358
|
-
mix:
|
|
1318
|
+
pickup: unit3("Pick-up ratio: how much of the paint under each dab the brush lifts and carries (default 0; with a reservoir 0.5)."),
|
|
1319
|
+
mix: unit3("Mix ratio: how much of the carried paint goes into what the brush lays, against its own colour (default 0.5)."),
|
|
359
1320
|
dirty: bool("Keep the carried paint after the stroke, so the next one starts with the last colour touched (default false)."),
|
|
360
1321
|
reservoir: object("A paint reservoir makes this a Mixer Brush (an empty object takes every default).", {
|
|
361
|
-
load:
|
|
1322
|
+
load: unit3("How much paint the reservoir holds; below 1 it drains per dab and long strokes run dry (default 0.5)."),
|
|
362
1323
|
refill: bool("Refill with the paint colour before each stroke (default true)."),
|
|
363
1324
|
loadsColour: bool("Load the current colour at all (default true; false: it only moves paint already down).")
|
|
364
1325
|
})
|
|
365
1326
|
});
|
|
366
1327
|
var WET = object("Wet surface (watercolour, gouache, wet oil) and the wet tools. Missing fields take their defaults.", {
|
|
367
|
-
medium:
|
|
368
|
-
water:
|
|
369
|
-
pigment:
|
|
370
|
-
body:
|
|
371
|
-
rewet:
|
|
372
|
-
load:
|
|
373
|
-
thinner:
|
|
1328
|
+
medium: oneOf2(WET_MEDIA, "The wet medium; a layer takes the medium's wet settings the first time it holds wet paint (default: the variety when it is watercolor, gouache or oil, else watercolor)."),
|
|
1329
|
+
water: unit3("Wetness: water laid, 0.2 a damp brush .. 1 dripping (default 0.5)."),
|
|
1330
|
+
pigment: unit3("Pigment concentration: 0 clear water .. 1 straight from the tube (default 0.5)."),
|
|
1331
|
+
body: unit3("Body: 0 transparent watercolour .. 1 opaque gouache (default 0)."),
|
|
1332
|
+
rewet: unit3("How much dried paint the water lifts again: 0 staining (default 0)."),
|
|
1333
|
+
load: unit3("Paint loading: 1 never runs out; lower, the brush runs dry along a long stroke (default 1)."),
|
|
1334
|
+
thinner: unit3("Thinner (water or solvent): more transparent, runnier paint (default 0).")
|
|
374
1335
|
});
|
|
375
1336
|
var THICK = object("Thick surface (impasto: paint with relief, lit by the canvas lighting). Missing fields take their defaults.", {
|
|
376
|
-
applicator:
|
|
377
|
-
thickness:
|
|
378
|
-
gloss:
|
|
379
|
-
metallic:
|
|
380
|
-
load:
|
|
381
|
-
thinner:
|
|
382
|
-
drag:
|
|
383
|
-
rim:
|
|
384
|
-
clarity:
|
|
1337
|
+
applicator: oneOf2(IMPASTO_TOOLS, "What lays the paint (default brush). brush: bristle ridges; tube: blobs squeezed from the tube; knife: scrapes the relief flat and smears; roller: a flat stippled band; gloop: a thick glossy bead."),
|
|
1338
|
+
thickness: unit3("How far the paint stands up (default 0.6)."),
|
|
1339
|
+
gloss: unit3("Shine: 0 matte .. 1 wet-looking gloss (default 0.4)."),
|
|
1340
|
+
metallic: unit3("Metallic: highlights take the paint colour, gold, copper, silver (default 0)."),
|
|
1341
|
+
load: unit3("Paint loading: 1 never runs out; lower, the stroke thins as the brush empties (default 0.9)."),
|
|
1342
|
+
thinner: unit3("Thinner: flatter, more transparent paint (default 0)."),
|
|
1343
|
+
drag: unit3("Knife and brush: how much of the paint under the tool is dragged along (default 0.4)."),
|
|
1344
|
+
rim: unit3("Light paint: light is stronger where the bead turns away, as on backlit glass (default 0)."),
|
|
1345
|
+
clarity: unit3("Glassy paint: the middle of a bead lets the layers below show through (default 0).")
|
|
385
1346
|
});
|
|
386
1347
|
var BEHAVIOUR = object("What the material does on the surface, and how tools work. Missing fields take their defaults.", {
|
|
387
|
-
surface:
|
|
388
|
-
grain:
|
|
1348
|
+
surface: oneOf2(BRUSH_SURFACES, "dry (default): stays where it lands; wet: flows, pools and dries (see wet); thick: stands up in relief (see thick)."),
|
|
1349
|
+
grain: unit3("Paper grain response: 0 ignores the grain, 1 catches only its peaks at light pressure (pencil, pastel, charcoal). Default 0."),
|
|
389
1350
|
mixing: MIXING,
|
|
390
1351
|
exchange: EXCHANGE2,
|
|
391
1352
|
wet: WET,
|
|
392
1353
|
thick: THICK,
|
|
393
|
-
strength:
|
|
394
|
-
distance:
|
|
395
|
-
length:
|
|
1354
|
+
strength: unit3("Tools: how strongly the blender, smudge or wet tool works per dab (default 0.5; smudge 0.7)."),
|
|
1355
|
+
distance: unit3("Blender: how far away colours are mixed in, as a fraction of the tip diameter (default 0.35)."),
|
|
1356
|
+
length: unit3("Smudge: how far paint is dragged, the share of carried paint kept per dab (default 0.85).")
|
|
396
1357
|
});
|
|
397
1358
|
var LIGHT = object("Light the brush gives off with its colour (glow and spill). Give light brushes dark paper.", {
|
|
398
|
-
amount:
|
|
1359
|
+
amount: unit3("Light per dab, scaled by its coverage and flow (default 0: none)."),
|
|
399
1360
|
pressure: bool("Pressure scales the light (default false)."),
|
|
400
1361
|
spill: object("Suggested spill for the layer, applied when it has none of its own.", {
|
|
401
1362
|
radius: num(0, 512, "How far the light reaches, px; 0 turns spill off (a highlighter)."),
|
|
@@ -406,7 +1367,7 @@ var LIGHT = object("Light the brush gives off with its colour (glow and spill).
|
|
|
406
1367
|
var MOTION = object("Animation after painting.", {
|
|
407
1368
|
live: bool("Effect paint keeps playing on stroke layers (default false)."),
|
|
408
1369
|
fps: int(1, 60, "Suggested playback rate in frames per second."),
|
|
409
|
-
waveSpeed:
|
|
1370
|
+
waveSpeed: unit3("Particle waves: how fast they run by stroke time; 0 they stand still (default 0), 1 about four a second.")
|
|
410
1371
|
});
|
|
411
1372
|
var SOLID = {
|
|
412
1373
|
type: "string",
|
|
@@ -427,7 +1388,7 @@ var EFFECT = object(
|
|
|
427
1388
|
{
|
|
428
1389
|
by: { type: "string", const: "stroke", description: 'Always "stroke".' },
|
|
429
1390
|
length: num(16, 4096, "Strip length: px of stroke per repeat of the effect."),
|
|
430
|
-
repeat:
|
|
1391
|
+
repeat: oneOf2(["repeat", "mirror", "clamp"], "Past the strip end: start again, run back or hold.")
|
|
431
1392
|
},
|
|
432
1393
|
["by", "length", "repeat"]
|
|
433
1394
|
),
|
|
@@ -454,10 +1415,10 @@ var BRUSH_JSON_SCHEMA = object(
|
|
|
454
1415
|
name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." },
|
|
455
1416
|
description: { type: "string", maxLength: 500, description: "What the brush is for, in a sentence." },
|
|
456
1417
|
tags: { type: "array", maxItems: 16, uniqueItems: true, items: { type: "string", pattern: BRUSH_WORD.source, description: "A tag: lowercase letters, digits and dashes." }, description: "Search tags." },
|
|
457
|
-
kind:
|
|
458
|
-
medium:
|
|
1418
|
+
kind: oneOf2(BRUSH_KINDS, "mark: lays a material (see medium); tool: works on paint already down (see tool)."),
|
|
1419
|
+
medium: oneOf2(BRUSH_MEDIA, "Marks: what it lays. paint (dry, wet or thick), ink, dry (pencil, pastel, charcoal) or spray (airbrush). Descriptive: the tip, marks and behaviour decide how it renders."),
|
|
459
1420
|
variety: { type: "string", pattern: BRUSH_WORD.source, description: "What kind of the medium: oil, watercolor, gouache, pencil, pastel, charcoal, airbrush, marker, highlighter\u2026 A wet brush whose variety is watercolor, gouache or oil paints that medium." },
|
|
460
|
-
tool:
|
|
1421
|
+
tool: oneOf2(
|
|
461
1422
|
BRUSH_TOOLS,
|
|
462
1423
|
"Tools: eraser removes paint; blender mixes the colours under the tip; smudge drags paint along; water lays clear water (lifts dried paint with behaviour.wet.rewet); dryer dries wet paint; blow pushes water along; blot takes wet paint up; wet-blend evens out wet pigment; wet-drag drags wet paint."
|
|
463
1424
|
),
|
|
@@ -1556,7 +2517,7 @@ var BrushLibraryError = class extends Error {
|
|
|
1556
2517
|
};
|
|
1557
2518
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1558
2519
|
var isV1 = (item) => "engine" in item || "category" in item || !("kind" in item);
|
|
1559
|
-
function
|
|
2520
|
+
function parseBrushLibrary2(raw, problems = []) {
|
|
1560
2521
|
if (typeof raw === "string") {
|
|
1561
2522
|
try {
|
|
1562
2523
|
raw = JSON.parse(raw);
|
|
@@ -1624,7 +2585,7 @@ function createBrushLibrary(options = {}) {
|
|
|
1624
2585
|
let brushes = [];
|
|
1625
2586
|
if (raw) {
|
|
1626
2587
|
try {
|
|
1627
|
-
brushes =
|
|
2588
|
+
brushes = parseBrushLibrary2(raw).filter((b) => !builtinIds.has(b.id));
|
|
1628
2589
|
} catch {
|
|
1629
2590
|
}
|
|
1630
2591
|
}
|
|
@@ -1701,7 +2662,7 @@ function createBrushLibrary(options = {}) {
|
|
|
1701
2662
|
}),
|
|
1702
2663
|
importFile: (raw, o = {}) => run(async () => {
|
|
1703
2664
|
const problems = [];
|
|
1704
|
-
const incoming =
|
|
2665
|
+
const incoming = parseBrushLibrary2(raw, problems);
|
|
1705
2666
|
const result = { added: [], replaced: [], skipped: [], problems };
|
|
1706
2667
|
let brushes = [...await load()];
|
|
1707
2668
|
for (const brush of incoming) {
|
|
@@ -1746,496 +2707,81 @@ function closest(id, ids, max = 3) {
|
|
|
1746
2707
|
const near = ids.map((c) => [c, score(c)]).filter(([, s]) => s >= 3).sort((a, b) => b[1] - a[1]).slice(0, max).map(([c]) => c);
|
|
1747
2708
|
return near.length ? `Closest: ${near.join(", ")}. ` : "";
|
|
1748
2709
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
return { ...rest, tip: { ...tip, description: `${tip.description} In a patch, shape may be left out to keep it.` } };
|
|
1758
|
-
})();
|
|
1759
|
-
var UNSETTABLE_BRUSH_FACETS = ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "motion", "colour"];
|
|
1760
|
-
var BRUSH_UNSET_PROPERTY = {
|
|
1761
|
-
type: "array",
|
|
1762
|
-
minItems: 1,
|
|
1763
|
-
uniqueItems: true,
|
|
1764
|
-
items: { type: "string", enum: [...UNSETTABLE_BRUSH_FACETS], description: "A facet to remove." },
|
|
1765
|
-
description: "Facets to remove, back to their defaults (applied before the facets given)."
|
|
1766
|
-
};
|
|
1767
|
-
function mergeBrushPatch(base, patch, path = "") {
|
|
1768
|
-
const out = { ...base };
|
|
1769
|
-
for (const [k, v] of Object.entries(patch)) {
|
|
1770
|
-
if (v === void 0) continue;
|
|
1771
|
-
const old = out[k];
|
|
1772
|
-
const replaceMarks = path === "" && k === "marks" && isObject3(old) && isObject3(v) && old.kind !== v.kind;
|
|
1773
|
-
out[k] = isObject3(v) && isObject3(old) && !replaceMarks ? mergeBrushPatch(old, v, `${path}${k}.`) : copy4(v);
|
|
1774
|
-
}
|
|
1775
|
-
return out;
|
|
1776
|
-
}
|
|
1777
|
-
function changedBrushPaths(a, b, path = "") {
|
|
1778
|
-
if (isObject3(a) && isObject3(b)) return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((k) => changedBrushPaths(a[k], b[k], path ? `${path}.${k}` : k));
|
|
1779
|
-
return JSON.stringify(a) === JSON.stringify(b) ? [] : [path];
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
// src/brushes/tools.ts
|
|
1783
|
-
var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
1784
|
-
var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
1785
|
-
var FACETS2 = BRUSH_JSON_SCHEMA.properties;
|
|
1786
|
-
var ID = { type: "string", pattern: String(FACETS2.id.pattern), description: "Brush id from list_brush_presets (lowercase letters, digits and dashes)." };
|
|
1787
|
-
var FACET_PROPS = BRUSH_PATCH_PROPERTIES;
|
|
1788
|
-
var HOSTS = Object.keys(BRUSH_HOST_PROFILES);
|
|
1789
|
-
var copy5 = (v) => JSON.parse(JSON.stringify(v));
|
|
1790
|
-
var merge = mergeBrushPatch;
|
|
1791
|
-
var changedPaths = changedBrushPaths;
|
|
1792
|
-
function summary(entry, profile, withCheck) {
|
|
1793
|
-
const b = entry.brush;
|
|
1794
|
-
const engine = resolveEngine(b);
|
|
1795
|
-
const check = withCheck ? checkBrush(b, profile) : null;
|
|
1796
|
-
return {
|
|
1797
|
-
id: b.id,
|
|
1798
|
-
name: b.name,
|
|
1799
|
-
builtin: entry.builtin,
|
|
1800
|
-
kind: b.kind,
|
|
1801
|
-
...b.medium ? { medium: b.medium } : {},
|
|
1802
|
-
...b.variety ? { variety: b.variety } : {},
|
|
1803
|
-
...b.tool ? { tool: b.tool } : {},
|
|
1804
|
-
category: resolveCategory(b, engine),
|
|
1805
|
-
engine,
|
|
1806
|
-
tip: b.tip.shape,
|
|
1807
|
-
...b.tip.size !== void 0 ? { size: b.tip.size } : {},
|
|
1808
|
-
marks: b.marks?.kind ?? "continuous",
|
|
1809
|
-
surface: b.behaviour?.surface ?? "dry",
|
|
1810
|
-
...b.light?.amount ? { light: b.light.amount } : {},
|
|
1811
|
-
...b.tags?.length ? { tags: b.tags } : {},
|
|
1812
|
-
...check ? { renderable: check.ok } : {}
|
|
1813
|
-
};
|
|
1814
|
-
}
|
|
1815
|
-
var refusal = (check, host) => `${host.name} can't use this brush: ${check.issues.filter((i) => i.severity === "error").map((i) => `${i.facet}: ${i.reason} ${i.fix}`).join(" ")} (check_brush lists every issue; nothing was saved.)`;
|
|
1816
|
-
var warnings = (check) => check.issues.filter((i) => i.severity === "warning");
|
|
1817
|
-
function createBrushLibraryTools(library, options = {}) {
|
|
1818
|
-
const profile = options.profile ?? PAINT_PROFILE;
|
|
1819
|
-
const hostOf = (id) => {
|
|
1820
|
-
if (id === void 0) return profile;
|
|
1821
|
-
const p = BRUSH_HOST_PROFILES[id];
|
|
1822
|
-
if (!p) throw new BrushLibraryError("invalid", `No host "${id}"; use one of: ${HOSTS.join(", ")}.`);
|
|
1823
|
-
return p;
|
|
1824
|
-
};
|
|
1825
|
-
const entry = async (id) => {
|
|
1826
|
-
const found = await library.get(id);
|
|
1827
|
-
if (found) return found;
|
|
1828
|
-
const ids = (await library.list()).map((e) => e.brush.id);
|
|
1829
|
-
throw new BrushLibraryError("unknown_brush", `No brush "${id}". ${closest(id, ids)}list_brush_presets lists them.`);
|
|
1830
|
-
};
|
|
1831
|
-
const store = async (raw, overwrite) => {
|
|
1832
|
-
const problems = [];
|
|
1833
|
-
let brush;
|
|
1834
|
-
try {
|
|
1835
|
-
brush = normalizeBrushSpec(raw, problems);
|
|
1836
|
-
} catch (e) {
|
|
1837
|
-
throw new BrushLibraryError("invalid", e.message);
|
|
1838
|
-
}
|
|
1839
|
-
const check2 = checkBrush(brush, profile);
|
|
1840
|
-
if (!check2.ok) throw new BrushLibraryError("invalid", refusal(check2, profile));
|
|
1841
|
-
const saved = await library.save(brush, { overwrite });
|
|
1842
|
-
return { ...saved, problems, check: check2 };
|
|
1843
|
-
};
|
|
1844
|
-
const list2 = {
|
|
1845
|
-
name: "list_brush_presets",
|
|
1846
|
-
title: "List brushes",
|
|
1847
|
-
description: `Lists the brush library (built-in presets and saved brushes) that this host (${profile.name}) can render, filtered by kind, medium, variety, tool, tag or words. Returns each brush's id (for describe_brush, create_brush basedOn, duplicate_brush and the editor's set_brush), name, builtin, kind, medium, variety or tool, category, engine, tip shape and size, marks, surface and light, plus total. Call it first to find the nearest brush to start from. Changes nothing.`,
|
|
1848
|
-
inputSchema: {
|
|
1849
|
-
type: "object",
|
|
1850
|
-
properties: {
|
|
1851
|
-
kind: { type: "string", enum: [...BRUSH_KINDS], description: "mark (lays a material) or tool (works on paint already down)." },
|
|
1852
|
-
medium: { type: "string", enum: [...BRUSH_MEDIA], description: "Marks laying this medium." },
|
|
1853
|
-
variety: { type: "string", maxLength: 40, description: "This variety (oil, watercolor, pencil, highlighter\u2026)." },
|
|
1854
|
-
tool: { type: "string", enum: [...BRUSH_TOOLS], description: "Tools that do this." },
|
|
1855
|
-
tag: { type: "string", maxLength: 40, description: "Brushes with this tag." },
|
|
1856
|
-
text: { type: "string", maxLength: 200, description: "Words in the id, name, description, variety or tags (each word must match the start of one)." },
|
|
1857
|
-
source: { type: "string", enum: ["all", "builtin", "user"], description: "all (default), builtin presets only, or user (saved) brushes only." },
|
|
1858
|
-
all: { type: "boolean", description: "Also list brushes this host can't render, each with renderable false (default false)." },
|
|
1859
|
-
offset: { type: "integer", minimum: 0, description: "Skip this many (default 0)." },
|
|
1860
|
-
limit: { type: "integer", minimum: 1, maximum: 200, description: "At most this many (default 100)." }
|
|
1861
|
-
},
|
|
1862
|
-
additionalProperties: false
|
|
1863
|
-
},
|
|
1864
|
-
annotations: { title: "List brushes", ...READ },
|
|
1865
|
-
handler: async (a) => {
|
|
1866
|
-
const source = a.source === "all" ? void 0 : a.source;
|
|
1867
|
-
const found = await library.list({ kind: a.kind, medium: a.medium, variety: a.variety, tool: a.tool, tag: a.tag, text: a.text, source, ...a.all ? {} : { profile } });
|
|
1868
|
-
const offset = a.offset ?? 0;
|
|
1869
|
-
const page = found.slice(offset, offset + (a.limit ?? 100));
|
|
1870
|
-
return { host: profile.id, total: found.length, brushes: page.map((e) => summary(e, profile, !!a.all)) };
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
const describe = {
|
|
1874
|
-
name: "describe_brush",
|
|
1875
|
-
title: "Describe a brush",
|
|
1876
|
-
description: `Reads one brush by id: its full facet spec (kind, medium, variety or tool, tip, marks, dynamics, behaviour, light, motion, colour), whether it is built in, the engine and category it resolves to, and check: what this host (${profile.name}) refuses or paints without, each with a fix. Unknown ids are refused with the closest ones. Changes nothing.`,
|
|
1877
|
-
inputSchema: { type: "object", properties: { id: ID }, required: ["id"], additionalProperties: false },
|
|
1878
|
-
annotations: { title: "Describe a brush", ...READ },
|
|
1879
|
-
handler: async ({ id }) => {
|
|
1880
|
-
const e = await entry(id);
|
|
1881
|
-
const engine = resolveEngine(e.brush);
|
|
1882
|
-
return { brush: e.brush, builtin: e.builtin, engine, category: resolveCategory(e.brush, engine), check: checkBrush(e.brush, profile) };
|
|
1883
|
-
}
|
|
1884
|
-
};
|
|
1885
|
-
const check = {
|
|
1886
|
-
name: "check_brush",
|
|
1887
|
-
title: "Check a brush for a host",
|
|
1888
|
-
description: `Checks a brush (by id, or a facet spec not yet saved) against a host profile: ${HOSTS.join(", ")} (default this host, ${profile.id}). Returns ok (no errors), issues (facet, severity error or warning, reason, fix) and, when it is refused, degraded: the brush with the refused facets stripped and what was dropped. Use it before create_brush, or to see whether a brush works in another editor. Changes nothing.`,
|
|
1889
|
-
inputSchema: {
|
|
1890
|
-
type: "object",
|
|
1891
|
-
properties: {
|
|
1892
|
-
id: { ...ID, description: "A brush in the library (give id or brush, not both)." },
|
|
1893
|
-
brush: { type: "object", description: "A facet brush spec (the create_brush fields, with id and name)." },
|
|
1894
|
-
host: { type: "string", enum: HOSTS, description: `The host profile (default ${profile.id}).` }
|
|
1895
|
-
},
|
|
1896
|
-
additionalProperties: false
|
|
1897
|
-
},
|
|
1898
|
-
annotations: { title: "Check a brush for a host", ...READ },
|
|
1899
|
-
handler: async ({ id, brush, host }) => {
|
|
1900
|
-
if (id === void 0 === (brush === void 0)) throw new BrushLibraryError("invalid", "Give either id (a library brush) or brush (a spec), not both or neither.");
|
|
1901
|
-
const p = hostOf(host);
|
|
1902
|
-
let b;
|
|
1903
|
-
const problems = [];
|
|
1904
|
-
if (id !== void 0) b = (await entry(id)).brush;
|
|
1905
|
-
else {
|
|
1906
|
-
try {
|
|
1907
|
-
b = normalizeBrushSpec(brush, problems);
|
|
1908
|
-
} catch (e) {
|
|
1909
|
-
throw new BrushLibraryError("invalid", e.message);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
const result = checkBrush(b, p);
|
|
1913
|
-
return { host: p.id, ...result, ...problems.length ? { problems } : {}, ...!result.ok ? { degraded: degrade(b, p) } : {} };
|
|
1914
|
-
}
|
|
1915
|
-
};
|
|
1916
|
-
const exportTool = {
|
|
1917
|
-
name: "export_brushes",
|
|
1918
|
-
title: "Export brushes",
|
|
1919
|
-
description: "Writes a brush library file (format pixerate-brushes, version 2) of the saved brushes, or of the ids given (built-ins included). Returns the file as JSON, for import_brushes in another library. Changes nothing.",
|
|
1920
|
-
inputSchema: { type: "object", properties: { ids: { type: "array", minItems: 1, maxItems: 500, uniqueItems: true, items: ID, description: "Only these brushes (default every saved brush)." } }, additionalProperties: false },
|
|
1921
|
-
annotations: { title: "Export brushes", ...READ },
|
|
1922
|
-
handler: async ({ ids }) => ({ file: await library.exportFile(ids) })
|
|
1923
|
-
};
|
|
1924
|
-
const tools = [list2, describe, check, exportTool];
|
|
1925
|
-
if (options.renderPreview) {
|
|
1926
|
-
const render = options.renderPreview;
|
|
1927
|
-
tools.push({
|
|
1928
|
-
name: "preview_brush",
|
|
1929
|
-
title: "Preview a brush",
|
|
1930
|
-
description: "Draws a sample S-stroke with a brush (by id) on a plain background and returns it as an image, to see a brush before painting with it or after create_brush / update_brush. Returns the image plus id, width and height. Changes nothing.",
|
|
1931
|
-
inputSchema: {
|
|
1932
|
-
type: "object",
|
|
1933
|
-
properties: {
|
|
1934
|
-
id: ID,
|
|
1935
|
-
width: { type: "integer", minimum: 32, maximum: 1024, description: "Image width in px (default 256)." },
|
|
1936
|
-
height: { type: "integer", minimum: 16, maximum: 512, description: "Image height in px (default 96)." },
|
|
1937
|
-
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Paint colour #rrggbb (default the brush's own default colour, else #1f2937)." }
|
|
1938
|
-
},
|
|
1939
|
-
required: ["id"],
|
|
1940
|
-
additionalProperties: false
|
|
1941
|
-
},
|
|
1942
|
-
annotations: { title: "Preview a brush", ...READ },
|
|
1943
|
-
handler: async ({ id, width, height, color }) => {
|
|
1944
|
-
const { brush } = await entry(id);
|
|
1945
|
-
const own = typeof brush.colour?.default === "string" && /^#[0-9a-f]{6}/i.test(brush.colour.default) ? brush.colour.default.slice(0, 7) : void 0;
|
|
1946
|
-
const w = width ?? 256;
|
|
1947
|
-
const h = height ?? 96;
|
|
1948
|
-
const image = await render(brush, { width: w, height: h, color: color ?? own ?? "#1f2937" });
|
|
1949
|
-
return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }], structuredContent: { id, width: w, height: h } };
|
|
1950
|
-
}
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
if (options.readOnly) return tools;
|
|
1954
|
-
tools.push(
|
|
1955
|
-
{
|
|
1956
|
-
name: "create_brush",
|
|
1957
|
-
title: "Save a new brush",
|
|
1958
|
-
description: `Saves a new brush made of facets (to save an editor's current brush settings, pass the brush get_brush returns), optionally basedOn an existing brush (its facets are copied and the ones given here laid over them: objects merge, lists and values replace). Values are normalised (out-of-range numbers clamped, unused facets dropped, each reported in problems) and checked against this host (${profile.name}): a brush it can't render is refused with the facet and fix, and nothing is saved. Returns the new id, the stored brush, the engine it resolves to, problems and warnings (settings this host paints without). Start from the nearest preset (list_brush_presets) rather than from nothing.`,
|
|
1959
|
-
inputSchema: {
|
|
1960
|
-
type: "object",
|
|
1961
|
-
properties: {
|
|
1962
|
-
id: { ...FACETS2.id, description: "The new brush id: lowercase letters, digits and dashes, not taken (list_brush_presets)." },
|
|
1963
|
-
name: FACETS2.name,
|
|
1964
|
-
basedOn: { ...ID, description: "Copy this brush's facets first (a built-in preset or a saved brush)." },
|
|
1965
|
-
...FACET_PROPS
|
|
1966
|
-
},
|
|
1967
|
-
required: ["id", "name"],
|
|
1968
|
-
additionalProperties: false
|
|
1969
|
-
},
|
|
1970
|
-
annotations: { title: "Save a new brush", ...WRITE },
|
|
1971
|
-
handler: async ({ basedOn, ...spec }) => {
|
|
1972
|
-
if (library.isBuiltin(spec.id)) throw new BrushLibraryError("builtin", `"${spec.id}" is a built-in brush id; pick another id (or basedOn "${spec.id}" with a new id).`);
|
|
1973
|
-
if (await library.get(spec.id)) throw new BrushLibraryError("exists", `A brush "${spec.id}" exists already; pick another id, or change it with update_brush.`);
|
|
1974
|
-
const base = basedOn !== void 0 ? (await entry(basedOn)).brush : void 0;
|
|
1975
|
-
const raw = base ? merge(base, spec) : spec;
|
|
1976
|
-
const saved = await store(raw, false);
|
|
1977
|
-
return { id: saved.brush.id, brush: saved.brush, engine: resolveEngine(saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
|
|
1978
|
-
}
|
|
2710
|
+
function snapshotBrushLibrary(library, onChange) {
|
|
2711
|
+
let entries = [];
|
|
2712
|
+
let user = [];
|
|
2713
|
+
const refresh = () => library.list().then(
|
|
2714
|
+
(list2) => {
|
|
2715
|
+
entries = list2;
|
|
2716
|
+
user = list2.filter((e) => !e.builtin).map((e) => e.brush);
|
|
2717
|
+
onChange?.();
|
|
1979
2718
|
},
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
...FACET_PROPS,
|
|
1990
|
-
unset: BRUSH_UNSET_PROPERTY
|
|
1991
|
-
},
|
|
1992
|
-
required: ["id"],
|
|
1993
|
-
additionalProperties: false
|
|
1994
|
-
},
|
|
1995
|
-
annotations: { title: "Update a brush", ...WRITE, idempotentHint: true },
|
|
1996
|
-
handler: async ({ id, unset, ...patch }) => {
|
|
1997
|
-
const e = await entry(id);
|
|
1998
|
-
if (e.builtin) throw new BrushLibraryError("builtin", `"${id}" is a built-in brush and can't be changed; duplicate_brush makes a copy you can change.`);
|
|
1999
|
-
const base = copy5(e.brush);
|
|
2000
|
-
for (const k of unset ?? []) delete base[k];
|
|
2001
|
-
const saved = await store(merge(base, { ...patch, id }), true);
|
|
2002
|
-
return { id, brush: saved.brush, changed: changedPaths(e.brush, saved.brush), problems: saved.problems, warnings: warnings(saved.check) };
|
|
2003
|
-
}
|
|
2719
|
+
() => void 0
|
|
2720
|
+
);
|
|
2721
|
+
let ready = refresh();
|
|
2722
|
+
const unsubscribe = library.subscribe(() => {
|
|
2723
|
+
ready = refresh();
|
|
2724
|
+
});
|
|
2725
|
+
return {
|
|
2726
|
+
get entries() {
|
|
2727
|
+
return entries;
|
|
2004
2728
|
},
|
|
2005
|
-
{
|
|
2006
|
-
|
|
2007
|
-
title: "Duplicate a brush",
|
|
2008
|
-
description: "Copies any brush (built-in presets included) to a new saved brush you can change with update_brush. Returns the new id (default <id>-copy, -copy-2\u2026) and the brush.",
|
|
2009
|
-
inputSchema: {
|
|
2010
|
-
type: "object",
|
|
2011
|
-
properties: {
|
|
2012
|
-
id: ID,
|
|
2013
|
-
newId: { ...FACETS2.id, description: "Id for the copy (default <id>-copy, -copy-2\u2026); must not be taken." },
|
|
2014
|
-
name: { ...FACETS2.name, description: 'Name for the copy (default "<name> copy").' }
|
|
2015
|
-
},
|
|
2016
|
-
required: ["id"],
|
|
2017
|
-
additionalProperties: false
|
|
2018
|
-
},
|
|
2019
|
-
annotations: { title: "Duplicate a brush", ...WRITE },
|
|
2020
|
-
handler: async ({ id, newId, name }) => {
|
|
2021
|
-
const brush = await library.duplicate(id, { ...newId !== void 0 ? { id: newId } : {}, ...name !== void 0 ? { name } : {} });
|
|
2022
|
-
return { id: brush.id, brush };
|
|
2023
|
-
}
|
|
2729
|
+
get user() {
|
|
2730
|
+
return user;
|
|
2024
2731
|
},
|
|
2025
|
-
{
|
|
2026
|
-
|
|
2027
|
-
title: "Remove a brush",
|
|
2028
|
-
description: "Deletes a saved brush from the library for good (export_brushes first to keep a copy). Built-in presets are refused. Returns the id removed.",
|
|
2029
|
-
inputSchema: { type: "object", properties: { id: { ...ID, description: "The saved brush to remove (not a built-in)." } }, required: ["id"], additionalProperties: false },
|
|
2030
|
-
annotations: { title: "Remove a brush", ...WRITE, destructiveHint: true },
|
|
2031
|
-
handler: async ({ id }) => {
|
|
2032
|
-
await entry(id);
|
|
2033
|
-
await library.remove(id);
|
|
2034
|
-
return { removed: id };
|
|
2035
|
-
}
|
|
2732
|
+
get ready() {
|
|
2733
|
+
return ready;
|
|
2036
2734
|
},
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
title: "Import brushes",
|
|
2040
|
-
description: "Adds the brushes of a library file (format pixerate-brushes version 2 from export_brushes, a version 1 file of the paint editor's brushes, which are migrated, or a bare list of brushes). Brushes with a built-in id are skipped, and so are ids already saved unless overwrite. Returns added, replaced and skipped ids and problems (each fix or skip, per brush).",
|
|
2041
|
-
inputSchema: {
|
|
2042
|
-
type: "object",
|
|
2043
|
-
properties: {
|
|
2044
|
-
file: { type: ["object", "array"], description: "The library file object, or an array of brushes." },
|
|
2045
|
-
overwrite: { type: "boolean", description: "Replace saved brushes with the same id (default false: skip them)." }
|
|
2046
|
-
},
|
|
2047
|
-
required: ["file"],
|
|
2048
|
-
additionalProperties: false
|
|
2049
|
-
},
|
|
2050
|
-
annotations: { title: "Import brushes", ...WRITE, destructiveHint: true },
|
|
2051
|
-
handler: async ({ file, overwrite }) => library.importFile(file, { overwrite: !!overwrite })
|
|
2052
|
-
}
|
|
2053
|
-
);
|
|
2054
|
-
return tools;
|
|
2055
|
-
}
|
|
2056
|
-
function createBrushLibraryToolProvider(library, options = {}) {
|
|
2057
|
-
const profile = options.profile ?? PAINT_PROFILE;
|
|
2058
|
-
const tools = createBrushLibraryTools(library, options);
|
|
2059
|
-
return {
|
|
2060
|
-
id: options.id ?? "brushes",
|
|
2061
|
-
namespace: options.namespace,
|
|
2062
|
-
title: "Brush library",
|
|
2063
|
-
description: `The shared brush library: find, describe, create, change, check and share brushes made of facets, for ${profile.name}.`,
|
|
2064
|
-
skill: "pixerate-brushes",
|
|
2065
|
-
tools
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
// src/paint-editor/brush-controls.ts
|
|
2070
|
-
var BRUSH_SLIDERS = [
|
|
2071
|
-
{ path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
|
|
2072
|
-
{ path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
|
|
2073
|
-
{ path: "tip.flow", label: "Flow", min: 0, max: 1, step: 0.01 },
|
|
2074
|
-
{ path: "tip.hardness", label: "Hardness", min: 0, max: 1, step: 0.01 },
|
|
2075
|
-
{ path: "tip.spacing", label: "Spacing", min: 0.01, max: 1, step: 0.01 },
|
|
2076
|
-
{ path: "tip.roundness", label: "Roundness", min: 0.05, max: 1, step: 0.01 },
|
|
2077
|
-
{ path: "tip.angle", label: "Angle", min: -180, max: 180, step: 1 },
|
|
2078
|
-
{ path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 },
|
|
2079
|
-
{ path: "tip.smoothing", label: "Smoothing", min: 0, max: 0.95, step: 0.01 },
|
|
2080
|
-
{ path: "pressureSmoothing", label: "Pressure smoothing", min: 0, max: 0.95, step: 0.01 },
|
|
2081
|
-
{ path: "dynamics.minSize", label: "Min size", min: 0, max: 1, step: 0.01 },
|
|
2082
|
-
{ path: "dynamics.tiltSize", label: "Tilt size", min: 0, max: 1, step: 0.01 },
|
|
2083
|
-
{ path: "dynamics.velocitySize", label: "Speed size", min: -1, max: 1, step: 0.01 }
|
|
2084
|
-
];
|
|
2085
|
-
var LIGHT_SLIDER = { path: "light.amount", label: "Light", min: 0, max: 1, step: 0.01 };
|
|
2086
|
-
var PICKUP_SLIDERS = [
|
|
2087
|
-
{ path: "behavior.pickup", label: "Pick-up", min: 0, max: 1, step: 0.01 },
|
|
2088
|
-
{ path: "behavior.mix", label: "Mix", min: 0, max: 1, step: 0.01 }
|
|
2089
|
-
];
|
|
2090
|
-
function brushEngineControls(brush) {
|
|
2091
|
-
const c = engineControls(brush);
|
|
2092
|
-
const wetPaint = brush.engine === "wet" && brush.wet?.tool === "paint";
|
|
2093
|
-
if (!c || !(wetPaint || ["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine))) return c;
|
|
2094
|
-
const extra = brush.engine === "impasto" ? [{ path: "impasto.rim", label: "Rim", min: 0, max: 1, step: 0.01 }, { path: "impasto.clarity", label: "Clarity", min: 0, max: 1, step: 0.01 }] : [];
|
|
2095
|
-
return { ...c, sliders: [...c.sliders, LIGHT_SLIDER, ...extra], light: true };
|
|
2096
|
-
}
|
|
2097
|
-
function engineControls(brush) {
|
|
2098
|
-
switch (brush.engine) {
|
|
2099
|
-
case "particle":
|
|
2100
|
-
return {
|
|
2101
|
-
title: "Particles",
|
|
2102
|
-
sliders: [
|
|
2103
|
-
{ path: "particle.count", label: "Particles", min: 1, max: 64, step: 1 },
|
|
2104
|
-
{ path: "particle.size", label: "Mark size", min: 0, max: 1, step: 0.01 },
|
|
2105
|
-
{ path: "particle.spread", label: "Spread", min: 0, max: 1, step: 0.01 },
|
|
2106
|
-
{ path: "particle.stiffness", label: "Stiffness", min: 0, max: 1, step: 0.01 },
|
|
2107
|
-
{ path: "particle.damping", label: "Damping", min: 0, max: 1, step: 0.01 },
|
|
2108
|
-
{ path: "particle.speed", label: "Speed", min: 0, max: 1, step: 0.01 },
|
|
2109
|
-
{ path: "particle.gravity", label: "Gravity", min: -1, max: 1, step: 0.01 },
|
|
2110
|
-
{ path: "particle.life", label: "Life", min: 0, max: 1, step: 0.01 },
|
|
2111
|
-
{ path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 },
|
|
2112
|
-
{ path: "particle.ribbonFill", label: "Ribbon fill", min: 0, max: 1, step: 0.01 },
|
|
2113
|
-
{ path: "particle.waveAmplitude", label: "Wave", min: 0, max: 1, step: 0.01 },
|
|
2114
|
-
{ path: "particle.waveLength", label: "Wavelength", min: 4, max: 400, step: 1 },
|
|
2115
|
-
{ path: "particle.waveSpeed", label: "Wave speed", min: 0, max: 1, step: 0.01 },
|
|
2116
|
-
{ path: "particle.wavePhaseJitter", label: "Wave scatter", min: 0, max: 1, step: 0.01 }
|
|
2117
|
-
],
|
|
2118
|
-
mixingPath: null,
|
|
2119
|
-
dirty: false,
|
|
2120
|
-
multicolor: false,
|
|
2121
|
-
load: false,
|
|
2122
|
-
particleMenus: true
|
|
2123
|
-
};
|
|
2124
|
-
case "bristle":
|
|
2125
|
-
return {
|
|
2126
|
-
title: "Bristles",
|
|
2127
|
-
sliders: [
|
|
2128
|
-
{ path: "bristle.count", label: "Bristles", min: 3, max: 128, step: 1 },
|
|
2129
|
-
{ path: "bristle.stiffness", label: "Stiffness", min: 0, max: 1, step: 0.01 },
|
|
2130
|
-
{ path: "bristle.splay", label: "Splay", min: 0, max: 1, step: 0.01 },
|
|
2131
|
-
{ path: "bristle.thickness", label: "Bristle width", min: 0, max: 1, step: 0.01 },
|
|
2132
|
-
{ path: "bristle.load", label: "Load", min: 0, max: 1, step: 0.01 },
|
|
2133
|
-
{ path: "bristle.jitter", label: "Variation", min: 0, max: 1, step: 0.01 }
|
|
2134
|
-
],
|
|
2135
|
-
mixingPath: null,
|
|
2136
|
-
dirty: false,
|
|
2137
|
-
multicolor: false,
|
|
2138
|
-
load: false
|
|
2139
|
-
};
|
|
2140
|
-
case "sticker":
|
|
2141
|
-
return {
|
|
2142
|
-
title: "Stickers",
|
|
2143
|
-
sliders: [
|
|
2144
|
-
{ path: "sticker.density", label: "Density", min: 0, max: 1, step: 0.01 },
|
|
2145
|
-
{ path: "sticker.sizeJitter", label: "Size jitter", min: 0, max: 1, step: 0.01 },
|
|
2146
|
-
{ path: "sticker.rotationJitter", label: "Turn jitter", min: 0, max: 1, step: 0.01 },
|
|
2147
|
-
{ path: "sticker.scatter", label: "Scatter", min: 0, max: 2, step: 0.01 },
|
|
2148
|
-
{ path: "sticker.colorJitter", label: "Colour jitter", min: 0, max: 1, step: 0.01 }
|
|
2149
|
-
],
|
|
2150
|
-
mixingPath: null,
|
|
2151
|
-
dirty: false,
|
|
2152
|
-
multicolor: false,
|
|
2153
|
-
load: false,
|
|
2154
|
-
stickerStamps: true
|
|
2155
|
-
};
|
|
2156
|
-
case "impasto":
|
|
2157
|
-
return {
|
|
2158
|
-
title: "Thick paint",
|
|
2159
|
-
sliders: [
|
|
2160
|
-
{ path: "impasto.thickness", label: "Thickness", min: 0, max: 1, step: 0.01 },
|
|
2161
|
-
{ path: "impasto.gloss", label: "Gloss", min: 0, max: 1, step: 0.01 },
|
|
2162
|
-
{ path: "impasto.metallic", label: "Metallic", min: 0, max: 1, step: 0.01 },
|
|
2163
|
-
{ path: "impasto.load", label: "Load", min: 0, max: 1, step: 0.01 },
|
|
2164
|
-
{ path: "impasto.thinner", label: "Thinner", min: 0, max: 1, step: 0.01 },
|
|
2165
|
-
{ path: "impasto.drag", label: "Drag", min: 0, max: 1, step: 0.01 },
|
|
2166
|
-
...PICKUP_SLIDERS
|
|
2167
|
-
],
|
|
2168
|
-
mixingPath: "behavior.mixing",
|
|
2169
|
-
dirty: true,
|
|
2170
|
-
multicolor: false,
|
|
2171
|
-
load: true,
|
|
2172
|
-
impastoTool: true
|
|
2173
|
-
};
|
|
2174
|
-
case "wet": {
|
|
2175
|
-
const tool = brush.wet?.tool ?? "paint";
|
|
2176
|
-
const sliders = tool === "paint" ? [
|
|
2177
|
-
{ path: "wet.water", label: "Water", min: 0, max: 1, step: 0.01 },
|
|
2178
|
-
{ path: "wet.pigment", label: "Pigment", min: 0, max: 1, step: 0.01 },
|
|
2179
|
-
{ path: "wet.body", label: "Body (opacity)", min: 0, max: 1, step: 0.01 },
|
|
2180
|
-
{ path: "wet.rewet", label: "Re-wet", min: 0, max: 1, step: 0.01 },
|
|
2181
|
-
{ path: "wet.load", label: "Load", min: 0, max: 1, step: 0.01 },
|
|
2182
|
-
{ path: "wet.thinner", label: "Thinner", min: 0, max: 1, step: 0.01 },
|
|
2183
|
-
...PICKUP_SLIDERS
|
|
2184
|
-
] : tool === "water" ? [
|
|
2185
|
-
{ path: "wet.water", label: "Water", min: 0, max: 1, step: 0.01 },
|
|
2186
|
-
{ path: "wet.rewet", label: "Re-wet", min: 0, max: 1, step: 0.01 }
|
|
2187
|
-
] : [{ path: "wet.strength", label: "Strength", min: 0, max: 1, step: 0.01 }];
|
|
2188
|
-
return { title: "Wet brush", sliders, mixingPath: tool === "paint" ? "behavior.mixing" : null, dirty: tool === "paint", multicolor: false, load: tool === "paint", wetTool: true };
|
|
2189
|
-
}
|
|
2190
|
-
case "blender":
|
|
2191
|
-
return {
|
|
2192
|
-
title: "Blender",
|
|
2193
|
-
sliders: [
|
|
2194
|
-
{ path: "blender.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
|
|
2195
|
-
{ path: "blender.distance", label: "Blend distance", min: 0, max: 1, step: 0.01 }
|
|
2196
|
-
],
|
|
2197
|
-
mixingPath: "blender.mixing",
|
|
2198
|
-
dirty: false,
|
|
2199
|
-
multicolor: false,
|
|
2200
|
-
load: false
|
|
2201
|
-
};
|
|
2202
|
-
case "smudge":
|
|
2203
|
-
return {
|
|
2204
|
-
title: "Smudge",
|
|
2205
|
-
sliders: [
|
|
2206
|
-
{ path: "smudge.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
|
|
2207
|
-
{ path: "smudge.length", label: "Length", min: 0, max: 1, step: 0.01 }
|
|
2208
|
-
],
|
|
2209
|
-
mixingPath: "smudge.mixing",
|
|
2210
|
-
dirty: false,
|
|
2211
|
-
multicolor: false,
|
|
2212
|
-
load: false
|
|
2213
|
-
};
|
|
2214
|
-
case "mixer":
|
|
2215
|
-
return { title: "Mixer Brush", sliders: [...PICKUP_SLIDERS, { path: "mixer.load", label: "Load", min: 0, max: 1, step: 0.01 }], mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
|
|
2216
|
-
case "dab":
|
|
2217
|
-
return { title: "Paint behaviour", sliders: PICKUP_SLIDERS, mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
|
|
2218
|
-
default:
|
|
2219
|
-
return null;
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
function multiColorSliders(mode) {
|
|
2223
|
-
if (mode === "off") return [];
|
|
2224
|
-
const soft = { path: "behavior.multicolor.softness", label: "Colour softness", min: 0, max: 1, step: 0.01 };
|
|
2225
|
-
return mode === "dab" ? [soft, { path: "behavior.multicolor.length", label: "Dabs per colour", min: 1, max: 100, step: 1 }] : [soft];
|
|
2226
|
-
}
|
|
2227
|
-
function patchBrushValue(brush, path, value) {
|
|
2228
|
-
const set = (obj, keys) => {
|
|
2229
|
-
const [head2, ...rest2] = keys;
|
|
2230
|
-
if (!rest2.length) return { ...obj, [head2]: value };
|
|
2231
|
-
return { ...obj, [head2]: set(obj[head2] ?? {}, rest2) };
|
|
2735
|
+
refresh: () => ready = refresh(),
|
|
2736
|
+
dispose: unsubscribe
|
|
2232
2737
|
};
|
|
2233
|
-
const [head, ...rest] = path.split(".");
|
|
2234
|
-
if (!rest.length) return { [head]: value };
|
|
2235
|
-
return { [head]: set({ ...brush[head] ?? {} }, rest) };
|
|
2236
2738
|
}
|
|
2237
2739
|
|
|
2238
2740
|
export {
|
|
2741
|
+
MAX_STICKER_STAMPS,
|
|
2742
|
+
DEFAULT_STICKER,
|
|
2743
|
+
normalizeStickerSettings,
|
|
2744
|
+
PARTICLE_KINDS,
|
|
2745
|
+
PARTICLE_RENDERS,
|
|
2746
|
+
RIBBON_PAIRS,
|
|
2747
|
+
DOT_TEXTURE_ORDERS,
|
|
2748
|
+
DOT_ROTATIONS,
|
|
2749
|
+
MAX_DOT_TEXTURES,
|
|
2750
|
+
DEFAULT_PARTICLE,
|
|
2751
|
+
normalizeParticle,
|
|
2752
|
+
DEFAULT_BRISTLE,
|
|
2753
|
+
normalizeBristle,
|
|
2754
|
+
PAINT_BRUSH_ENGINES,
|
|
2755
|
+
PAINT_BRUSH_CATEGORIES,
|
|
2756
|
+
WET_TOOLS,
|
|
2757
|
+
WET_MEDIA,
|
|
2758
|
+
DEFAULT_WET_BRUSH,
|
|
2759
|
+
normalizeWetBrush,
|
|
2760
|
+
IMPASTO_TOOLS,
|
|
2761
|
+
DEFAULT_IMPASTO,
|
|
2762
|
+
DEFAULT_LIGHT,
|
|
2763
|
+
normalizeLight,
|
|
2764
|
+
normalizeImpasto,
|
|
2765
|
+
MULTI_COLOR_MODES,
|
|
2766
|
+
DEFAULT_MULTI_COLOR,
|
|
2767
|
+
DEFAULT_BEHAVIOR,
|
|
2768
|
+
BRUSH_ROTATIONS,
|
|
2769
|
+
DEFAULT_DYNAMICS,
|
|
2770
|
+
normalizeDynamics,
|
|
2771
|
+
BRUSH_ID,
|
|
2772
|
+
normalizeMultiColor,
|
|
2773
|
+
normalizeBehavior,
|
|
2774
|
+
isWetCapable,
|
|
2775
|
+
isWetBrush,
|
|
2776
|
+
usesPaintExchange,
|
|
2777
|
+
brushTakesPaint,
|
|
2778
|
+
paintToMultiColor,
|
|
2779
|
+
multiColorToPaint,
|
|
2780
|
+
normalizePaintBrush,
|
|
2781
|
+
BUILTIN_BRUSHES,
|
|
2782
|
+
DEFAULT_BRUSH_ID,
|
|
2783
|
+
brushToJSON,
|
|
2784
|
+
parseBrushLibrary,
|
|
2239
2785
|
BRUSH_FIELDS,
|
|
2240
2786
|
getPath,
|
|
2241
2787
|
setPath,
|
|
@@ -2267,18 +2813,8 @@ export {
|
|
|
2267
2813
|
listPresets,
|
|
2268
2814
|
degrade,
|
|
2269
2815
|
BrushLibraryError,
|
|
2270
|
-
|
|
2816
|
+
parseBrushLibrary2,
|
|
2271
2817
|
createBrushLibrary,
|
|
2272
2818
|
closest,
|
|
2273
|
-
|
|
2274
|
-
UNSETTABLE_BRUSH_FACETS,
|
|
2275
|
-
BRUSH_UNSET_PROPERTY,
|
|
2276
|
-
mergeBrushPatch,
|
|
2277
|
-
changedBrushPaths,
|
|
2278
|
-
createBrushLibraryTools,
|
|
2279
|
-
createBrushLibraryToolProvider,
|
|
2280
|
-
BRUSH_SLIDERS,
|
|
2281
|
-
brushEngineControls,
|
|
2282
|
-
multiColorSliders,
|
|
2283
|
-
patchBrushValue
|
|
2819
|
+
snapshotBrushLibrary
|
|
2284
2820
|
};
|