@pixerate/editor 0.254.0 → 0.255.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.
Files changed (73) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.js +1 -1
  3. package/dist/animation-editor/index.cjs +59 -59
  4. package/dist/animation-editor/index.js +3 -3
  5. package/dist/brushes/index.cjs +31 -29
  6. package/dist/brushes/index.d.cts +1 -1
  7. package/dist/brushes/index.d.ts +1 -1
  8. package/dist/brushes/index.js +27 -25
  9. package/dist/brushes/library.d.cts +17 -0
  10. package/dist/brushes/library.d.ts +17 -0
  11. package/dist/{brushstrokeRender-5PYSKZIJ.js → brushstrokeRender-HCOQUSWW.js} +53 -18
  12. package/dist/{brushstrokeRender-7FLLSQEC.cjs → brushstrokeRender-ZZOXI5O5.cjs} +55 -20
  13. package/dist/canvas/brushTypes.d.cts +22 -3
  14. package/dist/canvas/brushTypes.d.ts +22 -3
  15. package/dist/canvas/brushstroke.d.cts +34 -12
  16. package/dist/canvas/brushstroke.d.ts +34 -12
  17. package/dist/canvas/brushstrokeRender.d.cts +14 -8
  18. package/dist/canvas/brushstrokeRender.d.ts +14 -8
  19. package/dist/canvas/changeFeed.d.cts +6 -0
  20. package/dist/canvas/changeFeed.d.ts +6 -0
  21. package/dist/canvas/drawController.d.cts +11 -3
  22. package/dist/canvas/drawController.d.ts +11 -3
  23. package/dist/canvas/freehand.d.cts +6 -1
  24. package/dist/canvas/freehand.d.ts +6 -1
  25. package/dist/canvas/index.cjs +123 -104
  26. package/dist/canvas/index.js +25 -6
  27. package/dist/{chunk-XVIK73LS.js → chunk-5P4JEOIV.js} +4 -4
  28. package/dist/{chunk-JPEHXIB5.cjs → chunk-5YWWQ36H.cjs} +66 -21
  29. package/dist/{chunk-CHAWTXBE.cjs → chunk-6WCQ35GI.cjs} +4 -4
  30. package/dist/{chunk-GYMMNCFT.js → chunk-AF23QK5F.js} +56 -11
  31. package/dist/{chunk-JFHBXNR2.cjs → chunk-B7V34GIH.cjs} +86 -86
  32. package/dist/{chunk-QC3OXRE4.js → chunk-DMTEQ6SH.js} +1 -1
  33. package/dist/{chunk-TLOGFIIV.cjs → chunk-GCTUOTCW.cjs} +35 -35
  34. package/dist/{chunk-34KLWUH4.cjs → chunk-HIMGBZAM.cjs} +1133 -597
  35. package/dist/{chunk-7P6VF3FG.js → chunk-HYWRU5VX.js} +1 -1
  36. package/dist/{chunk-QPHNVZHO.js → chunk-JVRLGV3C.js} +1118 -582
  37. package/dist/{chunk-LFMKWGFL.cjs → chunk-LRNUZKWV.cjs} +22 -8
  38. package/dist/chunk-PGPFWC35.cjs +517 -0
  39. package/dist/{chunk-QLSGTCYT.cjs → chunk-TBAVCLEQ.cjs} +23 -23
  40. package/dist/{chunk-2ZJ4DT3M.js → chunk-VUKUTT2W.js} +1 -1
  41. package/dist/{chunk-5JNA6LLY.js → chunk-WYGSR3IJ.js} +22 -8
  42. package/dist/chunk-YHUMGBZD.js +517 -0
  43. package/dist/commands/builtins.d.cts +1 -1
  44. package/dist/commands/builtins.d.ts +1 -1
  45. package/dist/commands/index.cjs +2 -2
  46. package/dist/commands/index.js +1 -1
  47. package/dist/image-editor/brush-presets.d.cts +32 -0
  48. package/dist/image-editor/brush-presets.d.ts +32 -0
  49. package/dist/image-editor/controller.d.cts +15 -0
  50. package/dist/image-editor/controller.d.ts +15 -0
  51. package/dist/image-editor/index.cjs +848 -775
  52. package/dist/image-editor/index.d.cts +1 -0
  53. package/dist/image-editor/index.d.ts +1 -0
  54. package/dist/image-editor/index.js +81 -8
  55. package/dist/image-editor/types.d.cts +7 -0
  56. package/dist/image-editor/types.d.ts +7 -0
  57. package/dist/paint-editor/index.cjs +80 -80
  58. package/dist/paint-editor/index.js +13 -13
  59. package/dist/vector/brushstroke.d.cts +21 -5
  60. package/dist/vector/brushstroke.d.ts +21 -5
  61. package/dist/vector/controller.d.cts +14 -1
  62. package/dist/vector/controller.d.ts +14 -1
  63. package/dist/vector/index.cjs +1220 -1199
  64. package/dist/vector/index.js +34 -13
  65. package/dist/vector/types.d.cts +7 -0
  66. package/dist/vector/types.d.ts +7 -0
  67. package/package.json +1 -1
  68. package/skills/pixerate-brushes/SKILL.md +9 -3
  69. package/skills/pixerate-canvas/SKILL.md +10 -5
  70. package/skills/pixerate-image-editor/SKILL.md +2 -1
  71. package/skills/pixerate-vector/SKILL.md +1 -1
  72. package/dist/chunk-HS6QDWDF.cjs +0 -1029
  73. package/dist/chunk-MTKY6K3Q.js +0 -1029
@@ -1,1029 +0,0 @@
1
- import {
2
- STAMP_KINDS,
3
- normalizeStickerRef
4
- } from "./chunk-APX63ILU.js";
5
- import {
6
- DEFAULT_BLENDER,
7
- DEFAULT_BRUSH,
8
- DEFAULT_MIXER,
9
- DEFAULT_SMUDGE,
10
- normalizeBlender,
11
- normalizeBrush,
12
- normalizeMixer,
13
- normalizeSmudge,
14
- readPaintSource
15
- } from "./chunk-GCVFEPJG.js";
16
-
17
- // src/paint-editor/mark-settings.ts
18
- var unit = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
19
- var oneOf = (v, all, d) => all.includes(v) ? v : d;
20
- var MAX_STICKER_STAMPS = 16;
21
- var DEFAULT_STICKER = { stamps: ["editor:star"], density: 0.4, sizeJitter: 0.4, rotationJitter: 1, scatter: 0.5, colorJitter: 0.2 };
22
- function normalizeStickerSettings(raw = {}, base = DEFAULT_STICKER) {
23
- const refs = (list) => list.flatMap((s) => {
24
- try {
25
- return [normalizeStickerRef(s)];
26
- } catch {
27
- return [];
28
- }
29
- });
30
- const stamps = Array.isArray(raw.stamps) ? refs(raw.stamps) : refs(base.stamps);
31
- return {
32
- stamps: stamps.length ? [...new Set(stamps)].slice(0, MAX_STICKER_STAMPS) : refs(base.stamps.length ? base.stamps : DEFAULT_STICKER.stamps),
33
- density: unit(raw.density, base.density),
34
- sizeJitter: unit(raw.sizeJitter, base.sizeJitter),
35
- rotationJitter: unit(raw.rotationJitter, base.rotationJitter),
36
- scatter: typeof raw.scatter === "number" && Number.isFinite(raw.scatter) ? Math.min(2, Math.max(0, raw.scatter)) : base.scatter,
37
- colorJitter: unit(raw.colorJitter, base.colorJitter)
38
- };
39
- }
40
- var PARTICLE_KINDS = ["string", "spring", "orbit", "flow", "elastic", "fuzzy"];
41
- var PARTICLE_RENDERS = ["dot", "line", "sequence", "ribbon"];
42
- var RIBBON_PAIRS = ["adjacent", "first-last"];
43
- var DOT_TEXTURE_ORDERS = ["sequence", "random"];
44
- var DOT_ROTATIONS = ["follow", "random", "fixed"];
45
- var MAX_DOT_TEXTURES = 4;
46
- var DEFAULT_PARTICLE = {
47
- kind: "spring",
48
- render: "line",
49
- count: 12,
50
- size: 0.08,
51
- spread: 0.5,
52
- stiffness: 0.4,
53
- damping: 0.25,
54
- speed: 0.5,
55
- gravity: 0,
56
- life: 0.5,
57
- interval: 0.3,
58
- ribbonFill: 0.5,
59
- ribbonPairs: "adjacent",
60
- waveAmplitude: 0,
61
- waveLength: 60,
62
- waveSpeed: 0,
63
- wavePhaseJitter: 0,
64
- dotTextures: [],
65
- dotTextureOrder: "sequence",
66
- dotRotation: "random"
67
- };
68
- function normalizeParticle(raw = {}, base = DEFAULT_PARTICLE) {
69
- const textures = Array.isArray(raw.dotTextures) ? raw.dotTextures.filter((t) => STAMP_KINDS.includes(t)).slice(0, MAX_DOT_TEXTURES) : [...base.dotTextures ?? []];
70
- return {
71
- kind: oneOf(raw.kind, PARTICLE_KINDS, base.kind),
72
- render: oneOf(raw.render, PARTICLE_RENDERS, base.render),
73
- count: typeof raw.count === "number" && Number.isFinite(raw.count) ? Math.min(64, Math.max(1, Math.round(raw.count))) : base.count,
74
- size: unit(raw.size, base.size),
75
- spread: unit(raw.spread, base.spread),
76
- stiffness: unit(raw.stiffness, base.stiffness),
77
- damping: unit(raw.damping, base.damping),
78
- speed: unit(raw.speed, base.speed),
79
- gravity: typeof raw.gravity === "number" && Number.isFinite(raw.gravity) ? Math.min(1, Math.max(-1, raw.gravity)) : base.gravity,
80
- life: unit(raw.life, base.life),
81
- interval: unit(raw.interval, base.interval),
82
- ribbonFill: unit(raw.ribbonFill, base.ribbonFill ?? DEFAULT_PARTICLE.ribbonFill),
83
- ribbonPairs: oneOf(raw.ribbonPairs, RIBBON_PAIRS, base.ribbonPairs ?? DEFAULT_PARTICLE.ribbonPairs),
84
- waveAmplitude: unit(raw.waveAmplitude, base.waveAmplitude ?? 0),
85
- waveLength: typeof raw.waveLength === "number" && Number.isFinite(raw.waveLength) ? Math.min(2e3, Math.max(4, raw.waveLength)) : base.waveLength ?? DEFAULT_PARTICLE.waveLength,
86
- waveSpeed: unit(raw.waveSpeed, base.waveSpeed ?? 0),
87
- wavePhaseJitter: unit(raw.wavePhaseJitter, base.wavePhaseJitter ?? 0),
88
- dotTextures: textures,
89
- dotTextureOrder: oneOf(raw.dotTextureOrder, DOT_TEXTURE_ORDERS, base.dotTextureOrder ?? DEFAULT_PARTICLE.dotTextureOrder),
90
- dotRotation: oneOf(raw.dotRotation, DOT_ROTATIONS, base.dotRotation ?? DEFAULT_PARTICLE.dotRotation)
91
- };
92
- }
93
- var DEFAULT_BRISTLE = { count: 32, stiffness: 0.5, splay: 0.5, thickness: 0.06, load: 0.6, jitter: 0.4 };
94
- function normalizeBristle(raw = {}, base = DEFAULT_BRISTLE) {
95
- return {
96
- count: typeof raw.count === "number" && Number.isFinite(raw.count) ? Math.min(128, Math.max(3, Math.round(raw.count))) : base.count,
97
- stiffness: unit(raw.stiffness, base.stiffness),
98
- splay: unit(raw.splay, base.splay),
99
- thickness: unit(raw.thickness, base.thickness),
100
- load: unit(raw.load, base.load),
101
- jitter: unit(raw.jitter, base.jitter)
102
- };
103
- }
104
-
105
- // src/paint-editor/brushes.ts
106
- var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
107
- 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"];
108
- var WET_TOOLS = ["paint", "water", "dry", "blow", "blend", "smudge", "blot"];
109
- var WET_MEDIA = ["watercolor", "gouache", "oil"];
110
- 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 };
111
- function normalizeWetBrush(raw = {}, base = DEFAULT_WET_BRUSH) {
112
- return {
113
- tool: WET_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
114
- water: unit2(raw.water, base.water),
115
- pigment: unit2(raw.pigment, base.pigment),
116
- body: unit2(raw.body, base.body),
117
- strength: unit2(raw.strength, base.strength),
118
- rewet: unit2(raw.rewet, base.rewet),
119
- medium: WET_MEDIA.includes(raw.medium) ? raw.medium : base.medium ?? "watercolor",
120
- load: unit2(raw.load, base.load ?? 1),
121
- thinner: unit2(raw.thinner, base.thinner ?? 0)
122
- };
123
- }
124
- var IMPASTO_TOOLS = ["brush", "tube", "knife", "roller", "gloop"];
125
- 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 };
126
- var DEFAULT_LIGHT = { amount: 0, pressure: false };
127
- function normalizeLight(raw = {}, base = DEFAULT_LIGHT) {
128
- return { amount: unit2(raw.amount, base.amount), pressure: typeof raw.pressure === "boolean" ? raw.pressure : base.pressure };
129
- }
130
- function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
131
- return {
132
- tool: IMPASTO_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
133
- thickness: unit2(raw.thickness, base.thickness),
134
- gloss: unit2(raw.gloss, base.gloss),
135
- metallic: unit2(raw.metallic, base.metallic),
136
- load: unit2(raw.load, base.load),
137
- thinner: unit2(raw.thinner, base.thinner),
138
- drag: unit2(raw.drag, base.drag),
139
- rim: unit2(raw.rim, base.rim ?? 0),
140
- clarity: unit2(raw.clarity, base.clarity ?? 0)
141
- };
142
- }
143
- var MULTI_COLOR_MODES = ["off", "tip", "dab"];
144
- var DEFAULT_MULTI_COLOR = { mode: "off", colors: [], order: "sequence", length: 12, softness: 1 };
145
- var DEFAULT_BEHAVIOR = { pickup: 0, mix: 0.5, dirty: false, mixing: "pigment", multicolor: DEFAULT_MULTI_COLOR };
146
- var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
147
- var DEFAULT_DYNAMICS = {
148
- minSize: 0,
149
- pressureFlow: 0,
150
- tiltSize: 0,
151
- tiltRoundness: 0,
152
- tiltOpacity: 0,
153
- velocitySize: 0,
154
- velocityOpacity: 0,
155
- sizeJitter: 0,
156
- angleJitter: 0,
157
- opacityJitter: 0,
158
- scatter: 0
159
- };
160
- var unit2 = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
161
- var range = (v, lo, hi, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d;
162
- function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
163
- return {
164
- minSize: unit2(raw.minSize, base.minSize),
165
- pressureFlow: unit2(raw.pressureFlow, base.pressureFlow),
166
- tiltSize: unit2(raw.tiltSize, base.tiltSize),
167
- tiltRoundness: unit2(raw.tiltRoundness, base.tiltRoundness),
168
- tiltOpacity: unit2(raw.tiltOpacity, base.tiltOpacity),
169
- velocitySize: range(raw.velocitySize, -1, 1, base.velocitySize),
170
- velocityOpacity: range(raw.velocityOpacity, -1, 1, base.velocityOpacity),
171
- sizeJitter: unit2(raw.sizeJitter, base.sizeJitter),
172
- angleJitter: unit2(raw.angleJitter, base.angleJitter),
173
- opacityJitter: unit2(raw.opacityJitter, base.opacityJitter),
174
- scatter: range(raw.scatter, 0, 4, base.scatter)
175
- };
176
- }
177
- var BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
178
- var HEX = /^#[0-9a-f]{6}$/i;
179
- function normalizeMultiColor(raw = {}, base = DEFAULT_MULTI_COLOR) {
180
- 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;
181
- return {
182
- mode: MULTI_COLOR_MODES.includes(raw.mode) ? raw.mode : base.mode,
183
- colors: [...colors],
184
- order: raw.order === "random" || raw.order === "sequence" ? raw.order : base.order,
185
- length: typeof raw.length === "number" && Number.isFinite(raw.length) ? Math.min(500, Math.max(1, Math.round(raw.length))) : base.length,
186
- softness: unit2(raw.softness, base.softness)
187
- };
188
- }
189
- function normalizeBehavior(raw = {}, base = DEFAULT_BEHAVIOR) {
190
- return {
191
- pickup: unit2(raw.pickup, base.pickup),
192
- mix: unit2(raw.mix, base.mix),
193
- dirty: typeof raw.dirty === "boolean" ? raw.dirty : base.dirty,
194
- mixing: raw.mixing === "rgb" || raw.mixing === "pigment" ? raw.mixing : base.mixing,
195
- multicolor: normalizeMultiColor(raw.multicolor ?? {}, base.multicolor)
196
- };
197
- }
198
- function isWetCapable(brush) {
199
- return brush.engine === "dab" || brush.engine === "mixer" || brush.engine === "wet" || brush.engine === "impasto";
200
- }
201
- function isWetBrush(brush) {
202
- return brush.engine === "wet";
203
- }
204
- function usesPaintExchange(brush, gradient = brush.paint !== null) {
205
- return brush.engine === "dab" && (brush.behavior.pickup > 0 || gradient);
206
- }
207
- function brushTakesPaint(brush) {
208
- if (brush.engine === "eraser" || brush.engine === "blender" || brush.engine === "smudge") return false;
209
- if (brush.engine === "wet") return brush.wet.tool === "paint";
210
- if (brush.engine === "mixer") return brush.loadsColor;
211
- return true;
212
- }
213
- function paintToMultiColor(g) {
214
- if (!g || g.space === "oklab" || g.stops.some((s) => s.opacity !== void 0 || s.midpoint !== void 0)) return null;
215
- const colors = g.stops.map((s) => s.color);
216
- const own = (list) => list.length === 2 && list[0] === "paint" && list[1] === "secondary" ? [] : list.some((c) => c === "paint" || c === "secondary") ? null : list;
217
- const uniform = (n) => g.stops.every((s, k) => s.offset === k / n);
218
- const softness = g.softness ?? 1;
219
- if (g.map.by === "across-tip" && colors.length <= 8 && uniform(colors.length - 1)) {
220
- const c = own(colors);
221
- return c ? { mode: "tip", colors: c, order: "sequence", length: DEFAULT_MULTI_COLOR.length, softness } : null;
222
- }
223
- if (g.map.by === "random" && colors.length % 2 === 0 && colors.length <= 16) {
224
- const n = colors.length / 2;
225
- const pairs = Array.from({ length: n }, (_, k) => colors[2 * k]);
226
- const hard = g.stops.every((s, i) => s.offset === (Math.floor(i / 2) + i % 2) / n && s.color === pairs[Math.floor(i / 2)]);
227
- const c = hard ? own(pairs) : null;
228
- return c ? { mode: "dab", colors: c, order: "random", length: g.map.every ?? 1, softness } : null;
229
- }
230
- 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)) {
231
- const n = colors.length - 1;
232
- const c = own(colors.slice(0, n));
233
- const length = g.map.length / n;
234
- return c && Number.isInteger(length) ? { mode: "dab", colors: c, order: "sequence", length, softness } : null;
235
- }
236
- return null;
237
- }
238
- function multiColorToPaint(mc, mixing) {
239
- if (mc.mode === "off") return null;
240
- const colors = mc.colors.length >= 2 ? mc.colors : ["paint", "secondary"];
241
- const n = colors.length;
242
- const space = mixing === "rgb" ? "srgb" : "pigment";
243
- const soft = mc.softness < 1 ? { softness: mc.softness } : {};
244
- if (mc.mode === "tip") return { type: "gradient", stops: colors.map((color, k) => ({ offset: k / (n - 1), color })), space, map: { by: "across-tip" }, ...soft };
245
- if (mc.order === "random") {
246
- const stops = colors.flatMap((color, k) => [
247
- { offset: k / n, color },
248
- { offset: (k + 1) / n, color }
249
- ]);
250
- return { type: "gradient", stops, space, map: { by: "random", every: mc.length } };
251
- }
252
- 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 };
253
- }
254
- function normalizePaintBrush(raw = {}, base) {
255
- const b = base ?? BASE_BRUSH;
256
- const id = typeof raw.id === "string" && BRUSH_ID.test(raw.id) ? raw.id : b.id;
257
- const engine = PAINT_BRUSH_ENGINES.includes(raw.engine) ? raw.engine : b.engine;
258
- const rb = raw.behavior ?? {};
259
- const mixerIn = { ...b.mixer, ...raw.mixer ?? {} };
260
- if (engine === "mixer") {
261
- if (rb.pickup !== void 0) mixerIn.wet = rb.pickup;
262
- if (rb.mix !== void 0) mixerIn.mix = rb.mix;
263
- if (rb.dirty !== void 0) mixerIn.cleanEachStroke = !rb.dirty;
264
- if (rb.mixing !== void 0) mixerIn.mixing = rb.mixing;
265
- }
266
- const mixer = normalizeMixer(mixerIn);
267
- let behavior = normalizeBehavior(rb, b.behavior);
268
- if (engine === "mixer") behavior = { ...behavior, pickup: mixer.wet, mix: mixer.mix, dirty: !mixer.cleanEachStroke, mixing: mixer.mixing };
269
- let paint = b.paint ?? null;
270
- if (raw.paint !== void 0) {
271
- const p = readPaintSource(raw.paint);
272
- paint = p && p.type === "gradient" ? p : null;
273
- } else if (rb.multicolor !== void 0) {
274
- const mc = normalizeMultiColor(rb.multicolor, paintToMultiColor(paint) ?? DEFAULT_MULTI_COLOR);
275
- paint = mc.mode === "off" ? paintToMultiColor(paint) ? null : paint : multiColorToPaint(mc, behavior.mixing);
276
- }
277
- behavior = { ...behavior, multicolor: paintToMultiColor(paint) ?? { ...DEFAULT_MULTI_COLOR, colors: [] } };
278
- return {
279
- id,
280
- name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
281
- category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
282
- engine,
283
- tip: normalizeBrush({ ...b.tip, ...raw.tip ?? {} }),
284
- grain: unit2(raw.grain, b.grain),
285
- pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
286
- rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
287
- dynamics: normalizeDynamics(raw.dynamics ?? {}, b.dynamics),
288
- mixer,
289
- loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
290
- behavior,
291
- blender: normalizeBlender(raw.blender ?? {}, b.blender),
292
- smudge: normalizeSmudge(raw.smudge ?? {}, b.smudge),
293
- wet: normalizeWetBrush(raw.wet ?? {}, b.wet ?? DEFAULT_WET_BRUSH),
294
- impasto: normalizeImpasto(raw.impasto ?? {}, b.impasto ?? DEFAULT_IMPASTO),
295
- particle: normalizeParticle(raw.particle ?? {}, b.particle ?? DEFAULT_PARTICLE),
296
- bristle: normalizeBristle(raw.bristle ?? {}, b.bristle ?? DEFAULT_BRISTLE),
297
- sticker: normalizeStickerSettings(raw.sticker ?? {}, b.sticker ?? DEFAULT_STICKER),
298
- light: normalizeLight(raw.light ?? {}, b.light ?? DEFAULT_LIGHT),
299
- paint,
300
- ...raw.builtin ? { builtin: true } : {}
301
- };
302
- }
303
- var BASE_BRUSH = {
304
- id: "round",
305
- name: "Round",
306
- category: "custom",
307
- engine: "dab",
308
- tip: { ...DEFAULT_BRUSH },
309
- grain: 0,
310
- pressureSmoothing: 0,
311
- rotation: "fixed",
312
- dynamics: { ...DEFAULT_DYNAMICS },
313
- mixer: { ...DEFAULT_MIXER },
314
- loadsColor: true,
315
- behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
316
- blender: { ...DEFAULT_BLENDER },
317
- smudge: { ...DEFAULT_SMUDGE },
318
- wet: { ...DEFAULT_WET_BRUSH },
319
- impasto: { ...DEFAULT_IMPASTO },
320
- particle: { ...DEFAULT_PARTICLE },
321
- bristle: { ...DEFAULT_BRISTLE },
322
- sticker: { ...DEFAULT_STICKER, stamps: [...DEFAULT_STICKER.stamps] },
323
- light: { ...DEFAULT_LIGHT },
324
- paint: null
325
- };
326
- var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
327
- var BUILTIN_BRUSHES = [
328
- builtin({
329
- id: "pencil-hb",
330
- name: "HB pencil",
331
- category: "pencil",
332
- tip: { size: 4, hardness: 0.7, spacing: 0.08, opacity: 0.85, flow: 0.55, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
333
- grain: 0.85,
334
- pressureSmoothing: 0.3,
335
- dynamics: { minSize: 0.55, tiltSize: 0.9, tiltRoundness: 0.7, tiltOpacity: 0.5 }
336
- }),
337
- builtin({
338
- id: "pencil-6b",
339
- name: "6B pencil",
340
- category: "pencil",
341
- tip: { size: 7, hardness: 0.55, spacing: 0.07, opacity: 1, flow: 0.75, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
342
- grain: 0.7,
343
- pressureSmoothing: 0.3,
344
- dynamics: { minSize: 0.5, tiltSize: 1, tiltRoundness: 0.75, tiltOpacity: 0.35 }
345
- }),
346
- builtin({
347
- id: "soft-pastel",
348
- name: "Soft pastel",
349
- category: "pastel",
350
- rotation: "stroke",
351
- 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 },
352
- grain: 1,
353
- pressureSmoothing: 0.25,
354
- dynamics: { sizeJitter: 0.06, angleJitter: 0.02 }
355
- }),
356
- builtin({
357
- id: "oil-pastel",
358
- name: "Oil pastel",
359
- category: "pastel",
360
- rotation: "stroke",
361
- 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" },
362
- grain: 0.5,
363
- pressureSmoothing: 0.25,
364
- dynamics: { minSize: 0.6 }
365
- }),
366
- builtin({
367
- id: "charcoal-stick",
368
- name: "Charcoal stick",
369
- category: "charcoal",
370
- rotation: "stroke",
371
- 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 },
372
- grain: 0.95,
373
- pressureSmoothing: 0.2,
374
- dynamics: { sizeJitter: 0.1, angleJitter: 0.03, tiltSize: 0.6 }
375
- }),
376
- builtin({
377
- id: "vine-charcoal",
378
- name: "Vine charcoal",
379
- category: "charcoal",
380
- tip: { size: 10, hardness: 0.3, spacing: 0.06, opacity: 0.75, flow: 0.45, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
381
- grain: 0.9,
382
- pressureSmoothing: 0.2,
383
- dynamics: { minSize: 0.4, tiltSize: 0.8, tiltRoundness: 0.5, tiltOpacity: 0.3, sizeJitter: 0.08 }
384
- }),
385
- builtin({
386
- id: "airbrush",
387
- name: "Airbrush",
388
- category: "airbrush",
389
- tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
390
- grain: 0,
391
- pressureSmoothing: 0.3,
392
- dynamics: { pressureFlow: 1, tiltSize: 0.5, tiltRoundness: 0.6 }
393
- }),
394
- builtin({
395
- id: "marker",
396
- name: "Chisel marker",
397
- category: "marker",
398
- 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" },
399
- grain: 0.1
400
- }),
401
- builtin({
402
- id: "ink-pen",
403
- name: "Ink pen",
404
- category: "ink",
405
- tip: { size: 4, hardness: 1, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
406
- pressureSmoothing: 0.4,
407
- dynamics: { minSize: 0.25, velocitySize: -0.45 }
408
- }),
409
- builtin({
410
- id: "brush-pen",
411
- name: "Brush pen",
412
- category: "ink",
413
- rotation: "stroke",
414
- tip: { size: 14, hardness: 0.9, spacing: 0.04, roundness: 0.8, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
415
- pressureSmoothing: 0.45,
416
- dynamics: { minSize: 0.05, velocitySize: -0.3 }
417
- }),
418
- builtin({
419
- id: "eraser-soft",
420
- name: "Soft eraser",
421
- category: "eraser",
422
- engine: "eraser",
423
- tip: { size: 40, hardness: 0.2, spacing: 0.08, opacity: 1, flow: 0.6, pressureSize: false, pressureOpacity: true }
424
- }),
425
- builtin({
426
- id: "eraser-hard",
427
- name: "Hard eraser",
428
- category: "eraser",
429
- engine: "eraser",
430
- tip: { size: 16, hardness: 0.9, spacing: 0.06, opacity: 1, flow: 1, pressureSize: true, pressureOpacity: false },
431
- grain: 0.3,
432
- dynamics: { minSize: 0.5 }
433
- }),
434
- builtin({
435
- id: "blender",
436
- name: "Blender",
437
- category: "blender",
438
- engine: "blender",
439
- loadsColor: false,
440
- tip: { size: 34, hardness: 0.4, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
441
- blender: { strength: 0.5, distance: 0.35, mixing: "pigment" }
442
- }),
443
- builtin({
444
- id: "smudge",
445
- name: "Smudge",
446
- category: "smudge",
447
- engine: "smudge",
448
- loadsColor: false,
449
- tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
450
- smudge: { strength: 0.75, length: 0.85, mixing: "rgb" }
451
- }),
452
- builtin({
453
- id: "wet-smudge",
454
- name: "Pigment smudge",
455
- category: "smudge",
456
- engine: "smudge",
457
- loadsColor: false,
458
- tip: { size: 30, hardness: 0.5, spacing: 0.06, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
459
- smudge: { strength: 0.7, length: 0.9, mixing: "pigment" },
460
- dynamics: { minSize: 0.6 }
461
- }),
462
- builtin({
463
- id: "dirty-round",
464
- name: "Dirty round",
465
- category: "paint",
466
- tip: { size: 22, hardness: 0.65, spacing: 0.06, opacity: 1, flow: 0.7, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
467
- behavior: { pickup: 0.55, mix: 0.6, dirty: true, mixing: "pigment" },
468
- dynamics: { minSize: 0.5 }
469
- }),
470
- builtin({
471
- id: "multicolor-flat",
472
- name: "Multi-colour flat",
473
- category: "paint",
474
- rotation: "stroke",
475
- 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 },
476
- behavior: { pickup: 0.15, mix: 0.4, mixing: "pigment" },
477
- // The paint and second colours side by side across the tip.
478
- paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "across-tip" }, softness: 0.7 },
479
- dynamics: { minSize: 0.6 }
480
- }),
481
- builtin({
482
- id: "palette-dabber",
483
- name: "Palette dabber",
484
- category: "paint",
485
- tip: { size: 18, hardness: 0.7, spacing: 0.35, opacity: 1, flow: 0.9, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
486
- // From the paint colour to the second colour and back, every 6 dabs.
487
- 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 },
488
- dynamics: { minSize: 0.5, sizeJitter: 0.15, scatter: 0.3 }
489
- }),
490
- builtin({
491
- id: "gradient-round",
492
- name: "Gradient round",
493
- category: "paint",
494
- tip: { size: 24, hardness: 0.8, spacing: 0.05, opacity: 1, flow: 0.9, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
495
- // Changes colour as it is dragged out: paint → second colour over 300 px, and back.
496
- paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 300, repeat: "mirror" } },
497
- dynamics: { minSize: 0.5 }
498
- }),
499
- builtin({
500
- id: "pressure-ombre",
501
- name: "Pressure ombr\xE9",
502
- category: "paint",
503
- tip: { size: 26, hardness: 0.6, spacing: 0.05, opacity: 1, flow: 0.8, smoothing: 0.2, pressureSize: false, pressureOpacity: false },
504
- // Light pressure lays the second colour, full pressure the paint colour.
505
- paint: { type: "gradient", stops: [{ offset: 0, color: "secondary" }, { offset: 1, color: "paint" }], space: "pigment", map: { by: "pressure" } }
506
- }),
507
- builtin({
508
- id: "rainbow-marker",
509
- name: "Rainbow marker",
510
- category: "marker",
511
- rotation: "stroke",
512
- 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 },
513
- paint: {
514
- type: "gradient",
515
- stops: [
516
- { offset: 0, color: "#e11d48" },
517
- { offset: 0.2, color: "#f59e0b" },
518
- { offset: 0.4, color: "#facc15" },
519
- { offset: 0.6, color: "#22c55e" },
520
- { offset: 0.8, color: "#3b82f6" },
521
- { offset: 1, color: "#8b5cf6" }
522
- ],
523
- space: "oklab",
524
- map: { by: "distance", length: 600, repeat: "repeat" }
525
- }
526
- }),
527
- builtin({
528
- id: "mixer-brush",
529
- name: "Mixer Brush",
530
- category: "mixer",
531
- engine: "mixer",
532
- tip: { size: 30, hardness: 0.7, spacing: 0.1, opacity: 1, flow: 0.5, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
533
- mixer: { wet: 0.5, load: 0.95, mix: 0.5, loadEachStroke: true, cleanEachStroke: true, mixing: "pigment" },
534
- dynamics: { minSize: 0.5 }
535
- }),
536
- builtin({
537
- id: "watercolor-round",
538
- name: "Watercolour round",
539
- category: "watercolor",
540
- engine: "wet",
541
- tip: { size: 28, hardness: 0.6, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
542
- dynamics: { minSize: 0.35 },
543
- behavior: { pickup: 0.55, mix: 0.5, mixing: "pigment" },
544
- wet: { tool: "paint", water: 0.55, pigment: 0.5, body: 0, rewet: 0.2 }
545
- }),
546
- builtin({
547
- id: "watercolor-wash",
548
- name: "Wash brush",
549
- category: "watercolor",
550
- engine: "wet",
551
- rotation: "stroke",
552
- 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 },
553
- behavior: { pickup: 0.45, mix: 0.45, mixing: "pigment" },
554
- wet: { tool: "paint", water: 0.8, pigment: 0.45, body: 0, rewet: 0.2 }
555
- }),
556
- builtin({
557
- id: "gradient-wash",
558
- name: "Gradient wash",
559
- category: "watercolor",
560
- engine: "wet",
561
- rotation: "stroke",
562
- 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 },
563
- behavior: { pickup: 0.4, mix: 0.4, mixing: "pigment" },
564
- wet: { tool: "paint", water: 0.8, pigment: 0.45, body: 0, rewet: 0.2 },
565
- // A graded wash: the paint colour runs into the second colour as the brush travels.
566
- paint: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 500, repeat: "mirror" } }
567
- }),
568
- builtin({
569
- id: "watercolor-detail",
570
- name: "Watercolour rigger",
571
- category: "watercolor",
572
- engine: "wet",
573
- tip: { size: 7, hardness: 0.75, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.4, pressureSize: true, pressureOpacity: false },
574
- dynamics: { minSize: 0.2 },
575
- behavior: { pickup: 0.4, mix: 0.45, mixing: "pigment" },
576
- wet: { tool: "paint", water: 0.4, pigment: 0.7, body: 0, rewet: 0.2 }
577
- }),
578
- builtin({
579
- id: "gouache",
580
- name: "Gouache",
581
- category: "watercolor",
582
- engine: "wet",
583
- tip: { size: 26, hardness: 0.7, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
584
- dynamics: { minSize: 0.5 },
585
- behavior: { pickup: 0.65, mix: 0.6, mixing: "pigment" },
586
- wet: { tool: "paint", water: 0.35, pigment: 0.8, body: 1, medium: "gouache", rewet: 0.3 }
587
- }),
588
- builtin({
589
- id: "oil-round",
590
- name: "Wet oil round",
591
- category: "oil",
592
- engine: "wet",
593
- tip: { size: 24, hardness: 0.75, spacing: 0.08, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
594
- dynamics: { minSize: 0.5 },
595
- behavior: { pickup: 0.6, mix: 0.55, mixing: "pigment" },
596
- wet: { tool: "paint", water: 0.4, pigment: 0.9, body: 1, medium: "oil", load: 0.97 }
597
- }),
598
- builtin({
599
- id: "oil-flat",
600
- name: "Wet oil flat",
601
- category: "oil",
602
- engine: "wet",
603
- rotation: "stroke",
604
- 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 },
605
- dynamics: { minSize: 0.6 },
606
- behavior: { pickup: 0.65, mix: 0.6, mixing: "pigment" },
607
- wet: { tool: "paint", water: 0.45, pigment: 0.9, body: 1, medium: "oil", load: 0.95 }
608
- }),
609
- builtin({
610
- id: "impasto-brush",
611
- name: "Impasto brush",
612
- category: "impasto",
613
- engine: "impasto",
614
- rotation: "stroke",
615
- 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 },
616
- dynamics: { minSize: 0.55 },
617
- behavior: { pickup: 0.4, mix: 0.5, mixing: "pigment" },
618
- impasto: { tool: "brush", thickness: 0.6, gloss: 0.45, load: 0.92, drag: 0.4 }
619
- }),
620
- builtin({
621
- id: "paint-tube",
622
- name: "Paint tube",
623
- category: "impasto",
624
- engine: "impasto",
625
- tip: { size: 22, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.35, pressureSize: true, pressureOpacity: false },
626
- dynamics: { minSize: 0.4 },
627
- impasto: { tool: "tube", thickness: 0.9, gloss: 0.5, load: 1, drag: 0.1 }
628
- }),
629
- builtin({
630
- id: "palette-knife",
631
- name: "Palette knife",
632
- category: "impasto",
633
- engine: "impasto",
634
- rotation: "stroke",
635
- tip: { size: 48, hardness: 1, spacing: 0.05, angle: 90, roundness: 0.18, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
636
- impasto: { tool: "knife", thickness: 0.3, gloss: 0.55, load: 1, drag: 0.85 }
637
- }),
638
- builtin({
639
- id: "paint-roller",
640
- name: "Roller",
641
- category: "impasto",
642
- engine: "impasto",
643
- rotation: "stroke",
644
- 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 },
645
- impasto: { tool: "roller", thickness: 0.25, gloss: 0.3, load: 0.9, drag: 0.15 }
646
- }),
647
- builtin({
648
- id: "gloop-pen",
649
- name: "Gloop pen",
650
- category: "impasto",
651
- engine: "impasto",
652
- tip: { size: 14, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
653
- dynamics: { minSize: 0.5 },
654
- impasto: { tool: "gloop", thickness: 1, gloss: 0.9, load: 1, drag: 0 }
655
- }),
656
- builtin({
657
- id: "metallic-paint",
658
- name: "Metallic paint",
659
- category: "impasto",
660
- engine: "impasto",
661
- rotation: "stroke",
662
- 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 },
663
- dynamics: { minSize: 0.5 },
664
- behavior: { pickup: 0.2, mix: 0.4, mixing: "pigment" },
665
- impasto: { tool: "brush", thickness: 0.45, gloss: 0.8, metallic: 1, load: 0.95, drag: 0.3 }
666
- }),
667
- builtin({
668
- id: "hair-strands",
669
- name: "Hair strands",
670
- category: "procedural",
671
- engine: "particle",
672
- tip: { size: 40, hardness: 0.8, opacity: 0.9, flow: 0.6, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
673
- particle: { kind: "string", render: "line", count: 22, size: 0.025, spread: 1, stiffness: 0.08, damping: 0.3 }
674
- }),
675
- builtin({
676
- id: "spring-ink",
677
- name: "Spring ink",
678
- category: "procedural",
679
- engine: "particle",
680
- tip: { size: 30, hardness: 0.9, opacity: 1, flow: 0.8, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
681
- particle: { kind: "spring", render: "line", count: 5, size: 0.08, spread: 0.6, stiffness: 0.2, damping: 0.1 }
682
- }),
683
- builtin({
684
- id: "orbit-curls",
685
- name: "Orbit curls",
686
- category: "procedural",
687
- engine: "particle",
688
- tip: { size: 36, hardness: 0.85, opacity: 0.9, flow: 0.7, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
689
- particle: { kind: "orbit", render: "line", count: 4, size: 0.05, spread: 0.6, speed: 0.45 }
690
- }),
691
- builtin({
692
- id: "flow-fur",
693
- name: "Flow fur",
694
- category: "procedural",
695
- engine: "particle",
696
- tip: { size: 50, hardness: 0.7, opacity: 0.8, flow: 0.5, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
697
- particle: { kind: "flow", render: "line", count: 10, size: 0.03, spread: 0.6, speed: 0.5, life: 0.25 }
698
- }),
699
- builtin({
700
- id: "bead-trail",
701
- name: "Bead trail",
702
- category: "procedural",
703
- engine: "particle",
704
- tip: { size: 34, hardness: 0.9, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
705
- particle: { kind: "spring", render: "sequence", count: 3, size: 0.18, spread: 0.5, stiffness: 0.3, damping: 0.3, interval: 0.4 }
706
- }),
707
- builtin({
708
- id: "sparkle-dust",
709
- name: "Sparkle dust",
710
- category: "procedural",
711
- engine: "particle",
712
- tip: { size: 60, hardness: 0.6, opacity: 1, flow: 0.9, smoothing: 0.1, pressureSize: true, pressureOpacity: true },
713
- particle: { kind: "flow", render: "dot", count: 6, size: 0.05, spread: 0.9, speed: 0.7, life: 0.4 }
714
- }),
715
- builtin({
716
- id: "bristle-round",
717
- name: "Bristle round",
718
- category: "bristle",
719
- engine: "bristle",
720
- rotation: "stroke",
721
- tip: { size: 30, hardness: 0.85, opacity: 1, flow: 0.9, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
722
- dynamics: { minSize: 0.4, tiltSize: 0.6, tiltRoundness: 0.6 },
723
- bristle: { count: 28, stiffness: 0.45, splay: 0.6, thickness: 0.035, load: 0.7, jitter: 0.5 }
724
- }),
725
- builtin({
726
- id: "bristle-flat",
727
- name: "Bristle flat",
728
- category: "bristle",
729
- engine: "bristle",
730
- rotation: "stroke",
731
- tip: { size: 46, hardness: 0.85, angle: 90, roundness: 0.3, opacity: 1, flow: 0.9, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
732
- dynamics: { minSize: 0.5, tiltSize: 0.5 },
733
- bristle: { count: 40, stiffness: 0.6, splay: 0.4, thickness: 0.025, load: 0.65, jitter: 0.45 }
734
- }),
735
- builtin({
736
- id: "dry-brush",
737
- name: "Dry brush",
738
- category: "bristle",
739
- engine: "bristle",
740
- rotation: "stroke",
741
- tip: { size: 44, hardness: 0.9, angle: 90, roundness: 0.45, opacity: 1, flow: 0.8, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
742
- grain: 0.6,
743
- bristle: { count: 48, stiffness: 0.35, splay: 0.7, thickness: 0.05, load: 0.15, jitter: 0.8 }
744
- }),
745
- // Light paint: brushes that lay light with their colour (glow and spill; see light.ts).
746
- builtin({
747
- id: "glow-gel",
748
- name: "Glow gel",
749
- category: "light",
750
- engine: "impasto",
751
- tip: { size: 16, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
752
- dynamics: { minSize: 0.6 },
753
- impasto: { tool: "gloop", thickness: 1, gloss: 0.95, load: 1, drag: 0, rim: 0.8, clarity: 0.5 },
754
- light: { amount: 0.85 }
755
- }),
756
- builtin({
757
- id: "neon-marker",
758
- name: "Neon marker",
759
- category: "light",
760
- tip: { size: 10, hardness: 0.9, spacing: 0.04, opacity: 1, flow: 1, smoothing: 0.45, pressureSize: false, pressureOpacity: false },
761
- light: { amount: 1 }
762
- }),
763
- builtin({
764
- id: "light-airbrush",
765
- name: "Light airbrush",
766
- category: "light",
767
- tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
768
- pressureSmoothing: 0.3,
769
- dynamics: { pressureFlow: 1 },
770
- light: { amount: 0.7, pressure: true }
771
- }),
772
- builtin({
773
- id: "spark-trail",
774
- name: "Spark trail",
775
- category: "light",
776
- engine: "particle",
777
- tip: { size: 50, hardness: 0.7, opacity: 1, flow: 1, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
778
- particle: { kind: "flow", render: "dot", count: 8, size: 0.06, spread: 0.7, speed: 0.9, life: 0.3 },
779
- light: { amount: 1 }
780
- }),
781
- builtin({
782
- // Silk of light: strands dragged behind the pen on slow waves, the bands between them filled.
783
- id: "light-ribbon",
784
- name: "Light ribbon",
785
- category: "light",
786
- engine: "particle",
787
- tip: { size: 80, hardness: 0.8, opacity: 0.9, flow: 1, smoothing: 0.35, pressureSize: false, pressureOpacity: false },
788
- 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 },
789
- light: { amount: 0.8 }
790
- }),
791
- builtin({
792
- // Flames: short-lived bands thrown off the pen, rising and curling in the flow.
793
- id: "flame-ribbon",
794
- name: "Flame ribbon",
795
- category: "light",
796
- engine: "particle",
797
- tip: { size: 50, hardness: 0.6, opacity: 0.85, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
798
- 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 },
799
- light: { amount: 1 }
800
- }),
801
- builtin({
802
- // A neon tube that lags and snaps back, with a slow shimmer.
803
- id: "neon-strand",
804
- name: "Neon strand",
805
- category: "light",
806
- engine: "particle",
807
- tip: { size: 40, hardness: 0.9, opacity: 1, flow: 1, smoothing: 0.4, pressureSize: false, pressureOpacity: false },
808
- particle: { kind: "elastic", render: "line", count: 3, size: 0.05, spread: 0.25, stiffness: 0.6, damping: 0.3, waveAmplitude: 0.06, waveLength: 120 },
809
- light: { amount: 1 }
810
- }),
811
- builtin({
812
- // Smoke lit from within: thin bands thrown into a slow, rising flow.
813
- id: "smoke-ribbon",
814
- name: "Smoke ribbon",
815
- category: "light",
816
- engine: "particle",
817
- tip: { size: 70, hardness: 0.3, opacity: 0.45, flow: 1, smoothing: 0.3, pressureSize: true, pressureOpacity: false },
818
- 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 },
819
- light: { amount: 0.3 }
820
- }),
821
- builtin({
822
- // Out-of-focus lights: soft discs and rings spaced along springy paths.
823
- id: "bokeh-dots",
824
- name: "Bokeh dots",
825
- category: "light",
826
- engine: "particle",
827
- tip: { size: 80, hardness: 0.7, opacity: 0.6, flow: 1, smoothing: 0.3, pressureSize: true, pressureOpacity: false },
828
- 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" },
829
- light: { amount: 0.9 }
830
- }),
831
- builtin({
832
- // Sparks: twinkles and stars thrown into the flow, spaced along it.
833
- id: "twinkle-sparks",
834
- name: "Twinkle sparks",
835
- category: "light",
836
- engine: "particle",
837
- tip: { size: 50, hardness: 0.8, opacity: 1, flow: 1, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
838
- 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" },
839
- light: { amount: 1 }
840
- }),
841
- builtin({
842
- id: "glow-wash",
843
- name: "Glow wash",
844
- category: "light",
845
- engine: "wet",
846
- tip: { size: 40, hardness: 0.55, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
847
- dynamics: { minSize: 0.35 },
848
- behavior: { pickup: 0.4, mix: 0.5, mixing: "pigment" },
849
- wet: { tool: "paint", water: 0.65, pigment: 0.75, body: 0, rewet: 0.1 },
850
- light: { amount: 0.7 }
851
- }),
852
- builtin({
853
- // Fluorescent ink: a transparent chisel marker that multiplies (text under it stays dark) and
854
- // glows a little brighter than the paper. Turn the layer's spill off (set_light_spill radius 0).
855
- id: "highlighter",
856
- name: "Highlighter",
857
- category: "light",
858
- 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" },
859
- grain: 0.05,
860
- light: { amount: 0.5 }
861
- }),
862
- builtin({
863
- id: "sticker-stars",
864
- name: "Star stickers",
865
- category: "sticker",
866
- engine: "sticker",
867
- tip: { size: 36, opacity: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
868
- dynamics: { minSize: 0.5 },
869
- sticker: { stamps: ["star", "sparkle"], density: 0.35, sizeJitter: 0.5, rotationJitter: 1, scatter: 0.6, colorJitter: 0.3 }
870
- }),
871
- builtin({
872
- id: "sticker-leaves",
873
- name: "Leaf stickers",
874
- category: "sticker",
875
- engine: "sticker",
876
- rotation: "stroke",
877
- tip: { size: 40, opacity: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: false },
878
- dynamics: { minSize: 0.5 },
879
- sticker: { stamps: ["leaf"], density: 0.5, sizeJitter: 0.4, rotationJitter: 0.35, scatter: 0.4, colorJitter: 0.35 }
880
- }),
881
- builtin({
882
- id: "sticker-confetti",
883
- name: "Confetti",
884
- category: "sticker",
885
- engine: "sticker",
886
- tip: { size: 22, opacity: 1, smoothing: 0.1, pressureSize: false, pressureOpacity: false },
887
- sticker: { stamps: ["confetti", "dot", "ring"], density: 0.7, sizeJitter: 0.5, rotationJitter: 1, scatter: 1.5, colorJitter: 1 }
888
- }),
889
- builtin({
890
- id: "sticker-flowers",
891
- name: "Flower stickers",
892
- category: "sticker",
893
- engine: "sticker",
894
- tip: { size: 44, opacity: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
895
- sticker: { stamps: ["flower", "heart"], density: 0.3, sizeJitter: 0.5, rotationJitter: 1, scatter: 0.7, colorJitter: 0.4 }
896
- }),
897
- builtin({
898
- id: "sticker-splats",
899
- name: "Paint splats",
900
- category: "sticker",
901
- engine: "sticker",
902
- tip: { size: 50, opacity: 0.95, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
903
- sticker: { stamps: ["splat"], density: 0.25, sizeJitter: 0.6, rotationJitter: 1, scatter: 0.8, colorJitter: 0.15 }
904
- }),
905
- builtin({
906
- id: "add-water",
907
- name: "Add water",
908
- category: "wet-tool",
909
- engine: "wet",
910
- tip: { size: 40, hardness: 0.4, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: true, pressureOpacity: true },
911
- dynamics: { minSize: 0.5 },
912
- wet: { tool: "water", water: 0.6, pigment: 0, rewet: 0.3 }
913
- }),
914
- builtin({
915
- id: "hair-dryer",
916
- name: "Dry",
917
- category: "wet-tool",
918
- engine: "wet",
919
- tip: { size: 80, hardness: 0.1, spacing: 0.15, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: true },
920
- wet: { tool: "dry", strength: 0.5 }
921
- }),
922
- builtin({
923
- id: "blow",
924
- name: "Blow",
925
- category: "wet-tool",
926
- engine: "wet",
927
- tip: { size: 50, hardness: 0.2, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.3, pressureSize: false, pressureOpacity: true },
928
- wet: { tool: "blow", strength: 0.6 }
929
- }),
930
- builtin({
931
- id: "wet-blend",
932
- name: "Wet blend",
933
- category: "wet-tool",
934
- engine: "wet",
935
- tip: { size: 36, hardness: 0.3, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: false, pressureOpacity: true },
936
- wet: { tool: "blend", strength: 0.4 }
937
- }),
938
- builtin({
939
- id: "wet-drag",
940
- name: "Wet smudge",
941
- category: "wet-tool",
942
- engine: "wet",
943
- tip: { size: 30, hardness: 0.5, spacing: 0.08, opacity: 1, flow: 1, smoothing: 0.25, pressureSize: false, pressureOpacity: true },
944
- wet: { tool: "smudge", strength: 0.6 }
945
- }),
946
- builtin({
947
- id: "blot",
948
- name: "Blot (erase wet)",
949
- category: "wet-tool",
950
- engine: "wet",
951
- tip: { size: 40, hardness: 0.5, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
952
- wet: { tool: "blot", strength: 0.7 }
953
- })
954
- ];
955
- var DEFAULT_BRUSH_ID = "pencil-6b";
956
- function brushToJSON(brush) {
957
- const { builtin: _b, ...rest } = brush;
958
- return JSON.parse(JSON.stringify(rest));
959
- }
960
- function parseBrushLibrary(raw, problems = []) {
961
- const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.brushes) ? raw.brushes : null;
962
- if (!list) throw new Error('Not a brush library: expected {"format":"pixerate-brushes","version":1,"brushes":[...]} or an array of brushes.');
963
- const out = [];
964
- list.forEach((item, i) => {
965
- if (!item || typeof item !== "object") {
966
- problems.push(`brushes[${i}] is not an object; skipped.`);
967
- return;
968
- }
969
- const r = item;
970
- if (typeof r.id !== "string" || !BRUSH_ID.test(r.id)) {
971
- problems.push(`brushes[${i}] has no valid id (lowercase letters, digits and dashes); skipped.`);
972
- return;
973
- }
974
- if (typeof r.name !== "string" || !r.name.trim()) {
975
- problems.push(`brushes[${i}] ("${r.id}") has no name; skipped.`);
976
- return;
977
- }
978
- const { builtin: _b, ...rest } = r;
979
- out.push(normalizePaintBrush(rest));
980
- });
981
- return out;
982
- }
983
-
984
- export {
985
- MAX_STICKER_STAMPS,
986
- DEFAULT_STICKER,
987
- normalizeStickerSettings,
988
- PARTICLE_KINDS,
989
- PARTICLE_RENDERS,
990
- RIBBON_PAIRS,
991
- DOT_TEXTURE_ORDERS,
992
- DOT_ROTATIONS,
993
- MAX_DOT_TEXTURES,
994
- DEFAULT_PARTICLE,
995
- normalizeParticle,
996
- DEFAULT_BRISTLE,
997
- normalizeBristle,
998
- PAINT_BRUSH_ENGINES,
999
- PAINT_BRUSH_CATEGORIES,
1000
- WET_TOOLS,
1001
- WET_MEDIA,
1002
- DEFAULT_WET_BRUSH,
1003
- normalizeWetBrush,
1004
- IMPASTO_TOOLS,
1005
- DEFAULT_IMPASTO,
1006
- DEFAULT_LIGHT,
1007
- normalizeLight,
1008
- normalizeImpasto,
1009
- MULTI_COLOR_MODES,
1010
- DEFAULT_MULTI_COLOR,
1011
- DEFAULT_BEHAVIOR,
1012
- BRUSH_ROTATIONS,
1013
- DEFAULT_DYNAMICS,
1014
- normalizeDynamics,
1015
- BRUSH_ID,
1016
- normalizeMultiColor,
1017
- normalizeBehavior,
1018
- isWetCapable,
1019
- isWetBrush,
1020
- usesPaintExchange,
1021
- brushTakesPaint,
1022
- paintToMultiColor,
1023
- multiColorToPaint,
1024
- normalizePaintBrush,
1025
- BUILTIN_BRUSHES,
1026
- DEFAULT_BRUSH_ID,
1027
- brushToJSON,
1028
- parseBrushLibrary
1029
- };