@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
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
var _chunkJSDND7DFcjs = require('./chunk-JSDND7DF.cjs');
|
|
5
6
|
|
|
6
7
|
|
|
8
|
+
var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
|
|
@@ -11,15 +13,7 @@
|
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
var _chunkHS6QDWDFcjs = require('./chunk-HS6QDWDF.cjs');
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
16
|
|
|
19
|
-
var _chunkJSDND7DFcjs = require('./chunk-JSDND7DF.cjs');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
|
|
23
17
|
|
|
24
18
|
|
|
25
19
|
|
|
@@ -29,6 +23,973 @@ var _chunkFQPUF2W2cjs = require('./chunk-FQPUF2W2.cjs');
|
|
|
29
23
|
|
|
30
24
|
var _chunk2VX55NFMcjs = require('./chunk-2VX55NFM.cjs');
|
|
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 [_chunkJSDND7DFcjs.normalizeStickerRef.call(void 0, s)];
|
|
35
|
+
} catch (e2) {
|
|
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) => _chunkJSDND7DFcjs.STAMP_KINDS.includes(t)).slice(0, MAX_DOT_TEXTURES) : [..._nullishCoalesce(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, _nullishCoalesce(base.ribbonFill, () => ( DEFAULT_PARTICLE.ribbonFill))),
|
|
92
|
+
ribbonPairs: oneOf(raw.ribbonPairs, RIBBON_PAIRS, _nullishCoalesce(base.ribbonPairs, () => ( DEFAULT_PARTICLE.ribbonPairs))),
|
|
93
|
+
waveAmplitude: unit(raw.waveAmplitude, _nullishCoalesce(base.waveAmplitude, () => ( 0))),
|
|
94
|
+
waveLength: typeof raw.waveLength === "number" && Number.isFinite(raw.waveLength) ? Math.min(2e3, Math.max(4, raw.waveLength)) : _nullishCoalesce(base.waveLength, () => ( DEFAULT_PARTICLE.waveLength)),
|
|
95
|
+
waveSpeed: unit(raw.waveSpeed, _nullishCoalesce(base.waveSpeed, () => ( 0))),
|
|
96
|
+
wavePhaseJitter: unit(raw.wavePhaseJitter, _nullishCoalesce(base.wavePhaseJitter, () => ( 0))),
|
|
97
|
+
dotTextures: textures,
|
|
98
|
+
dotTextureOrder: oneOf(raw.dotTextureOrder, DOT_TEXTURE_ORDERS, _nullishCoalesce(base.dotTextureOrder, () => ( DEFAULT_PARTICLE.dotTextureOrder))),
|
|
99
|
+
dotRotation: oneOf(raw.dotRotation, DOT_ROTATIONS, _nullishCoalesce(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 : _nullishCoalesce(base.medium, () => ( "watercolor")),
|
|
129
|
+
load: unit2(raw.load, _nullishCoalesce(base.load, () => ( 1))),
|
|
130
|
+
thinner: unit2(raw.thinner, _nullishCoalesce(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, _nullishCoalesce(base.rim, () => ( 0))),
|
|
149
|
+
clarity: unit2(raw.clarity, _nullishCoalesce(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(_nullishCoalesce(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 = _nullishCoalesce(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: _nullishCoalesce(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 = _nullishCoalesce(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 = _nullishCoalesce(raw.behavior, () => ( {}));
|
|
268
|
+
const mixerIn = { ...b.mixer, ..._nullishCoalesce(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 = _chunkFQPUF2W2cjs.normalizeMixer.call(void 0, 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 = _nullishCoalesce(b.paint, () => ( null));
|
|
279
|
+
if (raw.paint !== void 0) {
|
|
280
|
+
const p = _chunkFQPUF2W2cjs.readPaintSource.call(void 0, raw.paint);
|
|
281
|
+
paint = p && p.type === "gradient" ? p : null;
|
|
282
|
+
} else if (rb.multicolor !== void 0) {
|
|
283
|
+
const mc = normalizeMultiColor(rb.multicolor, _nullishCoalesce(paintToMultiColor(paint), () => ( DEFAULT_MULTI_COLOR)));
|
|
284
|
+
paint = mc.mode === "off" ? paintToMultiColor(paint) ? null : paint : multiColorToPaint(mc, behavior.mixing);
|
|
285
|
+
}
|
|
286
|
+
behavior = { ...behavior, multicolor: _nullishCoalesce(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: _chunkFQPUF2W2cjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(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(_nullishCoalesce(raw.dynamics, () => ( {})), b.dynamics),
|
|
297
|
+
mixer,
|
|
298
|
+
loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
|
|
299
|
+
behavior,
|
|
300
|
+
blender: _chunkFQPUF2W2cjs.normalizeBlender.call(void 0, _nullishCoalesce(raw.blender, () => ( {})), b.blender),
|
|
301
|
+
smudge: _chunkFQPUF2W2cjs.normalizeSmudge.call(void 0, _nullishCoalesce(raw.smudge, () => ( {})), b.smudge),
|
|
302
|
+
wet: normalizeWetBrush(_nullishCoalesce(raw.wet, () => ( {})), _nullishCoalesce(b.wet, () => ( DEFAULT_WET_BRUSH))),
|
|
303
|
+
impasto: normalizeImpasto(_nullishCoalesce(raw.impasto, () => ( {})), _nullishCoalesce(b.impasto, () => ( DEFAULT_IMPASTO))),
|
|
304
|
+
particle: normalizeParticle(_nullishCoalesce(raw.particle, () => ( {})), _nullishCoalesce(b.particle, () => ( DEFAULT_PARTICLE))),
|
|
305
|
+
bristle: normalizeBristle(_nullishCoalesce(raw.bristle, () => ( {})), _nullishCoalesce(b.bristle, () => ( DEFAULT_BRISTLE))),
|
|
306
|
+
sticker: normalizeStickerSettings(_nullishCoalesce(raw.sticker, () => ( {})), _nullishCoalesce(b.sticker, () => ( DEFAULT_STICKER))),
|
|
307
|
+
light: normalizeLight(_nullishCoalesce(raw.light, () => ( {})), _nullishCoalesce(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: { ..._chunkFQPUF2W2cjs.DEFAULT_BRUSH },
|
|
318
|
+
grain: 0,
|
|
319
|
+
pressureSmoothing: 0,
|
|
320
|
+
rotation: "fixed",
|
|
321
|
+
dynamics: { ...DEFAULT_DYNAMICS },
|
|
322
|
+
mixer: { ..._chunkFQPUF2W2cjs.DEFAULT_MIXER },
|
|
323
|
+
loadsColor: true,
|
|
324
|
+
behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
|
|
325
|
+
blender: { ..._chunkFQPUF2W2cjs.DEFAULT_BLENDER },
|
|
326
|
+
smudge: { ..._chunkFQPUF2W2cjs.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"];
|
|
@@ -38,7 +999,7 @@ var EDITOR_STAMPS = {
|
|
|
38
999
|
try {
|
|
39
1000
|
const stamp = _chunkJSDND7DFcjs.editorStampOf.call(void 0, ref);
|
|
40
1001
|
return stamp ? [stamp] : [];
|
|
41
|
-
} catch (
|
|
1002
|
+
} catch (e3) {
|
|
42
1003
|
return [];
|
|
43
1004
|
}
|
|
44
1005
|
}),
|
|
@@ -111,13 +1072,13 @@ function setPath(o, path, value) {
|
|
|
111
1072
|
var copy = (v) => v !== null && typeof v === "object" ? JSON.parse(JSON.stringify(v)) : v;
|
|
112
1073
|
function resolveEngine(brush) {
|
|
113
1074
|
if (brush.kind === "tool") return brush.tool === "eraser" || brush.tool === "blender" || brush.tool === "smudge" ? brush.tool : "wet";
|
|
114
|
-
if (_optionalChain([brush, 'access',
|
|
115
|
-
if (_optionalChain([brush, 'access',
|
|
1075
|
+
if (_optionalChain([brush, 'access', _2 => _2.marks, 'optionalAccess', _3 => _3.kind]) === "particles") return "particle";
|
|
1076
|
+
if (_optionalChain([brush, 'access', _4 => _4.marks, 'optionalAccess', _5 => _5.kind]) === "objects") return "sticker";
|
|
116
1077
|
if (brush.tip.shape === "bristle") return "bristle";
|
|
117
|
-
const surface = _optionalChain([brush, 'access',
|
|
1078
|
+
const surface = _optionalChain([brush, 'access', _6 => _6.behaviour, 'optionalAccess', _7 => _7.surface]);
|
|
118
1079
|
if (surface === "thick") return "impasto";
|
|
119
1080
|
if (surface === "wet") return "wet";
|
|
120
|
-
if (_optionalChain([brush, 'access',
|
|
1081
|
+
if (_optionalChain([brush, 'access', _8 => _8.behaviour, 'optionalAccess', _9 => _9.exchange, 'optionalAccess', _10 => _10.reservoir])) return "mixer";
|
|
121
1082
|
return "dab";
|
|
122
1083
|
}
|
|
123
1084
|
var VARIETY_CATEGORY = {
|
|
@@ -131,11 +1092,11 @@ var VARIETY_CATEGORY = {
|
|
|
131
1092
|
};
|
|
132
1093
|
var MEDIUM_CATEGORY = { paint: "paint", ink: "ink", dry: "pencil", spray: "airbrush" };
|
|
133
1094
|
function resolveWetMedium(brush) {
|
|
134
|
-
return _nullishCoalesce(_optionalChain([brush, 'access',
|
|
1095
|
+
return _nullishCoalesce(_optionalChain([brush, 'access', _11 => _11.behaviour, 'optionalAccess', _12 => _12.wet, 'optionalAccess', _13 => _13.medium]), () => ( (WET_MEDIA.includes(brush.variety) ? brush.variety : void 0)));
|
|
135
1096
|
}
|
|
136
1097
|
function resolveCategory(brush, engine = resolveEngine(brush)) {
|
|
137
1098
|
if (brush.kind === "tool") return brush.tool === "eraser" || brush.tool === "blender" || brush.tool === "smudge" ? brush.tool : "wet-tool";
|
|
138
|
-
if ((_nullishCoalesce(_optionalChain([brush, 'access',
|
|
1099
|
+
if ((_nullishCoalesce(_optionalChain([brush, 'access', _14 => _14.light, 'optionalAccess', _15 => _15.amount]), () => ( 0))) > 0) return "light";
|
|
139
1100
|
switch (engine) {
|
|
140
1101
|
case "particle":
|
|
141
1102
|
return "procedural";
|
|
@@ -167,9 +1128,9 @@ function resolveBrush(brush, options = {}) {
|
|
|
167
1128
|
if (medium) setPath(input, "wet.medium", medium);
|
|
168
1129
|
}
|
|
169
1130
|
if (engine === "blender" || engine === "smudge") input.loadsColor = false;
|
|
170
|
-
const paint = _optionalChain([brush, 'access',
|
|
1131
|
+
const paint = _optionalChain([brush, 'access', _16 => _16.colour, 'optionalAccess', _17 => _17.default]);
|
|
171
1132
|
if (paint && typeof paint === "object" && "type" in paint && paint.type === "gradient") input.paint = copy(paint);
|
|
172
|
-
return
|
|
1133
|
+
return normalizePaintBrush({ ...input, ...options.builtin ? { builtin: true } : {} });
|
|
173
1134
|
}
|
|
174
1135
|
|
|
175
1136
|
// src/brushes/migrate.ts
|
|
@@ -218,7 +1179,7 @@ function skeleton(b) {
|
|
|
218
1179
|
}
|
|
219
1180
|
var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "motion", "colour"];
|
|
220
1181
|
function migrateBrushV1(raw) {
|
|
221
|
-
const b =
|
|
1182
|
+
const b = normalizePaintBrush(raw);
|
|
222
1183
|
const brush = skeleton(b);
|
|
223
1184
|
const defaults = resolveBrush(brush);
|
|
224
1185
|
for (const field of BRUSH_FIELDS) {
|
|
@@ -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+|${_chunkJSDND7DFcjs.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, _chunkFQPUF2W2cjs.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
|
_chunk2VX55NFMcjs.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
|
),
|
|
@@ -287,9 +1248,9 @@ var TIP = object(
|
|
|
287
1248
|
stamps: {
|
|
288
1249
|
type: "array",
|
|
289
1250
|
minItems: 1,
|
|
290
|
-
maxItems:
|
|
1251
|
+
maxItems: MAX_STICKER_STAMPS,
|
|
291
1252
|
items: STAMP,
|
|
292
|
-
description: `Stamp tips: the stickers. Objects take any sticker and pick one at random per mark (default editor:star; repeats count once); particles take only the editor's stamps, use the first ${
|
|
1253
|
+
description: `Stamp tips: the stickers. Objects take any sticker and pick one at random per mark (default editor:star; repeats count once); particles take only the editor's stamps, use the first ${MAX_DOT_TEXTURES} in marks.stampOrder, and repeats weight the order.`
|
|
293
1254
|
}
|
|
294
1255
|
},
|
|
295
1256
|
["shape"]
|
|
@@ -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:
|
|
303
|
-
|
|
1263
|
+
physics: oneOf2(
|
|
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
|
),
|
|
@@ -450,14 +1411,14 @@ var COLOUR = object("The brush's colours. The user's colour still wins unless ow
|
|
|
450
1411
|
var BRUSH_JSON_SCHEMA = object(
|
|
451
1412
|
"A brush made of facets. Only id, name, kind and tip.shape are required; every other field takes the default for its engine.",
|
|
452
1413
|
{
|
|
453
|
-
id: { type: "string", pattern:
|
|
1414
|
+
id: { type: "string", pattern: BRUSH_ID.source, description: "Library id: lowercase letters, digits and dashes, up to 64 characters." },
|
|
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
|
),
|
|
@@ -549,7 +1510,7 @@ function readObject(value, schema, path, problems) {
|
|
|
549
1510
|
const out = {};
|
|
550
1511
|
for (const [key, v] of Object.entries(value)) {
|
|
551
1512
|
if (v === void 0) continue;
|
|
552
|
-
const child = _optionalChain([schema, 'access',
|
|
1513
|
+
const child = _optionalChain([schema, 'access', _18 => _18.properties, 'optionalAccess', _19 => _19[key]]);
|
|
553
1514
|
if (!child) {
|
|
554
1515
|
problems.push(`${path}.${key} is not a brush field; dropped.`);
|
|
555
1516
|
continue;
|
|
@@ -575,7 +1536,7 @@ function readPaint(value, path, problems) {
|
|
|
575
1536
|
}
|
|
576
1537
|
function normalizeBrushSpec(raw, problems = []) {
|
|
577
1538
|
if (!isObject(raw)) throw new Error("A brush must be an object with id, name, kind and tip.");
|
|
578
|
-
if (typeof raw.id !== "string" || !
|
|
1539
|
+
if (typeof raw.id !== "string" || !BRUSH_ID.test(raw.id)) throw new Error(`Brush id ${JSON.stringify(raw.id)} is not valid: use lowercase letters, digits and dashes (up to 64), starting with a letter or digit.`);
|
|
579
1540
|
const name = typeof raw.name === "string" ? raw.name.trim().slice(0, 80) : "";
|
|
580
1541
|
if (!name) throw new Error(`Brush "${raw.id}" needs a name.`);
|
|
581
1542
|
const tool = BRUSH_TOOLS.includes(raw.tool) ? raw.tool : void 0;
|
|
@@ -639,7 +1600,7 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
639
1600
|
delete brush.marks;
|
|
640
1601
|
}
|
|
641
1602
|
}
|
|
642
|
-
if (_optionalChain([brush, 'access',
|
|
1603
|
+
if (_optionalChain([brush, 'access', _20 => _20.marks, 'optionalAccess', _21 => _21.kind]) === "objects" && brush.tip.shape !== "stamp") {
|
|
643
1604
|
problems.push("marks objects lay stamps: tip.shape set to stamp.");
|
|
644
1605
|
brush.tip.shape = "stamp";
|
|
645
1606
|
}
|
|
@@ -652,7 +1613,7 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
652
1613
|
delete brush.tip.stamps;
|
|
653
1614
|
}
|
|
654
1615
|
if (brush.tip.stamps) {
|
|
655
|
-
const particles = _optionalChain([brush, 'access',
|
|
1616
|
+
const particles = _optionalChain([brush, 'access', _22 => _22.marks, 'optionalAccess', _23 => _23.kind]) === "particles";
|
|
656
1617
|
const stamps = brush.tip.stamps.flatMap((s) => {
|
|
657
1618
|
const ref = _chunkJSDND7DFcjs.normalizeStickerRef.call(void 0, s);
|
|
658
1619
|
if (particles && !_chunkJSDND7DFcjs.editorStampOf.call(void 0, ref)) {
|
|
@@ -697,7 +1658,7 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
697
1658
|
if (b.exchange && kind === "tool" && !wetTool) {
|
|
698
1659
|
drop("behaviour.exchange", `the ${brush.tool} does not pick paint up`);
|
|
699
1660
|
delete b.exchange;
|
|
700
|
-
} else if (b.exchange && (_optionalChain([brush, 'access',
|
|
1661
|
+
} else if (b.exchange && (_optionalChain([brush, 'access', _24 => _24.marks, 'optionalAccess', _25 => _25.kind]) === "particles" || _optionalChain([brush, 'access', _26 => _26.marks, 'optionalAccess', _27 => _27.kind]) === "objects" || brush.tip.shape === "bristle")) {
|
|
701
1662
|
drop("behaviour.exchange", "particle, object and bristle marks do not pick paint up");
|
|
702
1663
|
delete b.exchange;
|
|
703
1664
|
}
|
|
@@ -1459,26 +2420,26 @@ function checkBrush(brush, profile) {
|
|
|
1459
2420
|
add("tool", "error", `${host} has no ${brush.tool} tool.`, profile.tools.length ? `Use one of: ${list(profile.tools)}.` : `${host} has no tools; use a mark brush.`);
|
|
1460
2421
|
}
|
|
1461
2422
|
if (!profile.tipShapes.includes(brush.tip.shape)) add("tip.shape", "error", `${host} can't lay a ${brush.tip.shape} tip.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
|
|
1462
|
-
const marks = _nullishCoalesce(_optionalChain([brush, 'access',
|
|
2423
|
+
const marks = _nullishCoalesce(_optionalChain([brush, 'access', _28 => _28.marks, 'optionalAccess', _29 => _29.kind]), () => ( "continuous"));
|
|
1463
2424
|
if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
|
|
1464
|
-
const surface = _nullishCoalesce(_optionalChain([brush, 'access',
|
|
2425
|
+
const surface = _nullishCoalesce(_optionalChain([brush, 'access', _30 => _30.behaviour, 'optionalAccess', _31 => _31.surface]), () => ( "dry"));
|
|
1465
2426
|
if (brush.kind === "mark" && !profile.surfaces.includes(surface)) add("behaviour.surface", "error", `${host} has no ${surface} surface.`, `Set behaviour.surface to ${list(profile.surfaces)}.`);
|
|
1466
|
-
const ex = _optionalChain([brush, 'access',
|
|
1467
|
-
feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess',
|
|
1468
|
-
feature(profile.exchange, !_optionalChain([ex, 'optionalAccess',
|
|
1469
|
-
feature(profile.grain, nonZero(_optionalChain([brush, 'access',
|
|
1470
|
-
feature(profile.light, nonZero(_optionalChain([brush, 'access',
|
|
2427
|
+
const ex = _optionalChain([brush, 'access', _32 => _32.behaviour, 'optionalAccess', _33 => _33.exchange]);
|
|
2428
|
+
feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _34 => _34.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
|
|
2429
|
+
feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _35 => _35.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _36 => _36.pickup])) || !!_optionalChain([ex, 'optionalAccess', _37 => _37.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
|
|
2430
|
+
feature(profile.grain, nonZero(_optionalChain([brush, 'access', _38 => _38.behaviour, 'optionalAccess', _39 => _39.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
|
|
2431
|
+
feature(profile.light, nonZero(_optionalChain([brush, 'access', _40 => _40.light, 'optionalAccess', _41 => _41.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
|
|
1471
2432
|
const m = brush.motion;
|
|
1472
2433
|
feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
|
|
1473
2434
|
const d = _nullishCoalesce(brush.dynamics, () => ( {}));
|
|
1474
2435
|
feature(profile.velocity, nonZero(d.velocitySize) || nonZero(d.velocityOpacity), "dynamics.velocity", "velocity dynamics (its strokes carry no timing)", "Set dynamics.velocitySize and velocityOpacity to 0.");
|
|
1475
2436
|
feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
|
|
1476
2437
|
const colour = brush.colour;
|
|
1477
|
-
if (_optionalChain([colour, 'optionalAccess',
|
|
2438
|
+
if (_optionalChain([colour, 'optionalAccess', _42 => _42.default]) !== void 0) {
|
|
1478
2439
|
const kind = brushColourKind(colour.default);
|
|
1479
2440
|
if (!profile.colours.includes(kind)) add("colour.default", "error", `${host} can't paint ${kind} colours.`, `Use a ${list(profile.colours)} colour, or remove colour.default.`);
|
|
1480
2441
|
}
|
|
1481
|
-
const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess',
|
|
2442
|
+
const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _43 => _43.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
|
|
1482
2443
|
if (foreign.length) add("colour.palette", "warning", `${host} skips the palette's ${list(foreign)} colours.`, `Nothing to do; or keep to ${list(profile.colours)} colours.`);
|
|
1483
2444
|
return { ok: !issues.some((i) => i.severity === "error"), issues };
|
|
1484
2445
|
}
|
|
@@ -1540,7 +2501,7 @@ function degrade(brush, profile) {
|
|
|
1540
2501
|
}
|
|
1541
2502
|
dropped.push(issue);
|
|
1542
2503
|
}
|
|
1543
|
-
if (_optionalChain([b, 'access',
|
|
2504
|
+
if (_optionalChain([b, 'access', _44 => _44.behaviour, 'optionalAccess', _45 => _45.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
|
|
1544
2505
|
const after = checkBrush(b, profile).issues.filter((i) => i.severity === "error");
|
|
1545
2506
|
return { brush: b, dropped, issues: after };
|
|
1546
2507
|
}
|
|
@@ -1556,11 +2517,11 @@ 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);
|
|
1563
|
-
} catch (
|
|
2524
|
+
} catch (e4) {
|
|
1564
2525
|
throw new BrushLibraryError("invalid", "Not a brush library: the text is not JSON.");
|
|
1565
2526
|
}
|
|
1566
2527
|
}
|
|
@@ -1570,12 +2531,12 @@ function parseBrushLibrary(raw, problems = []) {
|
|
|
1570
2531
|
if (file && file.version !== void 0 && file.version !== 1 && file.version !== 2) {
|
|
1571
2532
|
throw new BrushLibraryError("invalid", `Brush library version ${JSON.stringify(file.version)} is not supported; this library reads versions 1 and 2.`);
|
|
1572
2533
|
}
|
|
1573
|
-
const v1File = _optionalChain([file, 'optionalAccess',
|
|
2534
|
+
const v1File = _optionalChain([file, 'optionalAccess', _46 => _46.version]) === 1;
|
|
1574
2535
|
const out = [];
|
|
1575
2536
|
items.forEach((item, i) => {
|
|
1576
2537
|
const at = `brushes[${i}]`;
|
|
1577
2538
|
if (!isObject2(item)) return void problems.push(`${at} is not an object; skipped.`);
|
|
1578
|
-
if (typeof item.id !== "string" || !
|
|
2539
|
+
if (typeof item.id !== "string" || !BRUSH_ID.test(item.id)) return void problems.push(`${at} has no valid id (lowercase letters, digits and dashes); skipped.`);
|
|
1579
2540
|
if (typeof item.name !== "string" || !item.name.trim()) return void problems.push(`${at} ("${item.id}") has no name; skipped.`);
|
|
1580
2541
|
const { builtin: _b, ...rest } = item;
|
|
1581
2542
|
const own = [];
|
|
@@ -1624,8 +2585,8 @@ function createBrushLibrary(options = {}) {
|
|
|
1624
2585
|
let brushes = [];
|
|
1625
2586
|
if (raw) {
|
|
1626
2587
|
try {
|
|
1627
|
-
brushes =
|
|
1628
|
-
} catch (
|
|
2588
|
+
brushes = parseBrushLibrary2(raw).filter((b) => !builtinIds.has(b.id));
|
|
2589
|
+
} catch (e5) {
|
|
1629
2590
|
}
|
|
1630
2591
|
}
|
|
1631
2592
|
user = brushes;
|
|
@@ -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,494 +2707,36 @@ 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: _nullishCoalesce(_optionalChain([b, 'access', _46 => _46.marks, 'optionalAccess', _47 => _47.kind]), () => ( "continuous")),
|
|
1809
|
-
surface: _nullishCoalesce(_optionalChain([b, 'access', _48 => _48.behaviour, 'optionalAccess', _49 => _49.surface]), () => ( "dry")),
|
|
1810
|
-
..._optionalChain([b, 'access', _50 => _50.light, 'optionalAccess', _51 => _51.amount]) ? { light: b.light.amount } : {},
|
|
1811
|
-
..._optionalChain([b, 'access', _52 => _52.tags, 'optionalAccess', _53 => _53.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 = _nullishCoalesce(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
|
|
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
|
+
_optionalChain([onChange, 'optionalCall', _47 => _47()]);
|
|
1863
2718
|
},
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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 _optionalChain([brush, 'access', _54 => _54.colour, 'optionalAccess', _55 => _55.default]) === "string" && /^#[0-9a-f]{6}/i.test(brush.colour.default) ? brush.colour.default.slice(0, 7) : void 0;
|
|
1946
|
-
const w = _nullishCoalesce(width, () => ( 256));
|
|
1947
|
-
const h = _nullishCoalesce(height, () => ( 96));
|
|
1948
|
-
const image = await render(brush, { width: w, height: h, color: _nullishCoalesce(_nullishCoalesce(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
|
-
}
|
|
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;
|
|
1979
2728
|
},
|
|
1980
|
-
{
|
|
1981
|
-
|
|
1982
|
-
title: "Update a brush",
|
|
1983
|
-
description: `Changes a saved brush: the facets given are laid over its own (objects merge, lists and values replace; marks of another kind replace the marks), and unset removes whole facets back to their defaults. Built-in presets are refused (duplicate_brush first). Checked against this host like create_brush; a refused change saves nothing. One change. Returns the brush, the leaf paths changed, problems and warnings.`,
|
|
1984
|
-
inputSchema: {
|
|
1985
|
-
type: "object",
|
|
1986
|
-
properties: {
|
|
1987
|
-
id: { ...ID, description: "The saved brush to change (not a built-in)." },
|
|
1988
|
-
name: FACETS2.name,
|
|
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 _nullishCoalesce(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
|
-
}
|
|
2729
|
+
get user() {
|
|
2730
|
+
return user;
|
|
2004
2731
|
},
|
|
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
|
-
}
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
name: "remove_brush",
|
|
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 = _nullishCoalesce(options.profile, () => ( PAINT_PROFILE));
|
|
2058
|
-
const tools = createBrushLibraryTools(library, options);
|
|
2059
|
-
return {
|
|
2060
|
-
id: _nullishCoalesce(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
|
|
2735
|
+
refresh: () => ready = refresh(),
|
|
2736
|
+
dispose: unsubscribe
|
|
2066
2737
|
};
|
|
2067
2738
|
}
|
|
2068
2739
|
|
|
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" && _optionalChain([brush, 'access', _56 => _56.wet, 'optionalAccess', _57 => _57.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 = _nullishCoalesce(_optionalChain([brush, 'access', _58 => _58.wet, 'optionalAccess', _59 => _59.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(_nullishCoalesce(obj[head2], () => ( {})), rest2) };
|
|
2232
|
-
};
|
|
2233
|
-
const [head, ...rest] = path.split(".");
|
|
2234
|
-
if (!rest.length) return { [head]: value };
|
|
2235
|
-
return { [head]: set({ ..._nullishCoalesce(brush[head], () => ( {})) }, rest) };
|
|
2236
|
-
}
|
|
2237
2740
|
|
|
2238
2741
|
|
|
2239
2742
|
|
|
@@ -2281,4 +2784,37 @@ function patchBrushValue(brush, path, value) {
|
|
|
2281
2784
|
|
|
2282
2785
|
|
|
2283
2786
|
|
|
2284
|
-
|
|
2787
|
+
|
|
2788
|
+
|
|
2789
|
+
|
|
2790
|
+
|
|
2791
|
+
|
|
2792
|
+
|
|
2793
|
+
|
|
2794
|
+
|
|
2795
|
+
|
|
2796
|
+
|
|
2797
|
+
|
|
2798
|
+
|
|
2799
|
+
|
|
2800
|
+
|
|
2801
|
+
|
|
2802
|
+
|
|
2803
|
+
|
|
2804
|
+
|
|
2805
|
+
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
|
|
2809
|
+
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
|
|
2813
|
+
|
|
2814
|
+
|
|
2815
|
+
|
|
2816
|
+
|
|
2817
|
+
|
|
2818
|
+
|
|
2819
|
+
|
|
2820
|
+
exports.MAX_STICKER_STAMPS = MAX_STICKER_STAMPS; exports.DEFAULT_STICKER = DEFAULT_STICKER; exports.normalizeStickerSettings = normalizeStickerSettings; exports.PARTICLE_KINDS = PARTICLE_KINDS; exports.PARTICLE_RENDERS = PARTICLE_RENDERS; exports.RIBBON_PAIRS = RIBBON_PAIRS; exports.DOT_TEXTURE_ORDERS = DOT_TEXTURE_ORDERS; exports.DOT_ROTATIONS = DOT_ROTATIONS; exports.MAX_DOT_TEXTURES = MAX_DOT_TEXTURES; exports.DEFAULT_PARTICLE = DEFAULT_PARTICLE; exports.normalizeParticle = normalizeParticle; exports.DEFAULT_BRISTLE = DEFAULT_BRISTLE; exports.normalizeBristle = normalizeBristle; exports.PAINT_BRUSH_ENGINES = PAINT_BRUSH_ENGINES; exports.PAINT_BRUSH_CATEGORIES = PAINT_BRUSH_CATEGORIES; exports.WET_TOOLS = WET_TOOLS; exports.WET_MEDIA = WET_MEDIA; exports.DEFAULT_WET_BRUSH = DEFAULT_WET_BRUSH; exports.normalizeWetBrush = normalizeWetBrush; exports.IMPASTO_TOOLS = IMPASTO_TOOLS; exports.DEFAULT_IMPASTO = DEFAULT_IMPASTO; exports.DEFAULT_LIGHT = DEFAULT_LIGHT; exports.normalizeLight = normalizeLight; exports.normalizeImpasto = normalizeImpasto; exports.MULTI_COLOR_MODES = MULTI_COLOR_MODES; exports.DEFAULT_MULTI_COLOR = DEFAULT_MULTI_COLOR; exports.DEFAULT_BEHAVIOR = DEFAULT_BEHAVIOR; exports.BRUSH_ROTATIONS = BRUSH_ROTATIONS; exports.DEFAULT_DYNAMICS = DEFAULT_DYNAMICS; exports.normalizeDynamics = normalizeDynamics; exports.BRUSH_ID = BRUSH_ID; exports.normalizeMultiColor = normalizeMultiColor; exports.normalizeBehavior = normalizeBehavior; exports.isWetCapable = isWetCapable; exports.isWetBrush = isWetBrush; exports.usesPaintExchange = usesPaintExchange; exports.brushTakesPaint = brushTakesPaint; exports.paintToMultiColor = paintToMultiColor; exports.multiColorToPaint = multiColorToPaint; exports.normalizePaintBrush = normalizePaintBrush; exports.BUILTIN_BRUSHES = BUILTIN_BRUSHES; exports.DEFAULT_BRUSH_ID = DEFAULT_BRUSH_ID; exports.brushToJSON = brushToJSON; exports.parseBrushLibrary = parseBrushLibrary; exports.BRUSH_FIELDS = BRUSH_FIELDS; exports.getPath = getPath; exports.setPath = setPath; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium; exports.resolveCategory = resolveCategory; exports.resolveBrush = resolveBrush; exports.migrateBrushV1 = migrateBrushV1; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.BRUSH_WORD = BRUSH_WORD; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.normalizeBrushSpec = normalizeBrushSpec; exports.BRUSH_PRESETS = BRUSH_PRESETS; exports.PAINT_PROFILE = PAINT_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.listPresets = listPresets; exports.degrade = degrade; exports.BrushLibraryError = BrushLibraryError; exports.parseBrushLibrary2 = parseBrushLibrary2; exports.createBrushLibrary = createBrushLibrary; exports.closest = closest; exports.snapshotBrushLibrary = snapshotBrushLibrary;
|