@pixerate/editor 0.249.0 → 0.251.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 (158) hide show
  1. package/dist/agent/index.cjs +40 -41
  2. package/dist/agent/index.js +9 -10
  3. package/dist/animation/index.cjs +6 -7
  4. package/dist/animation/index.js +5 -6
  5. package/dist/animation-editor/index.cjs +124 -123
  6. package/dist/animation-editor/index.js +15 -14
  7. package/dist/brushes/index.cjs +1406 -0
  8. package/dist/brushes/index.d.cts +12 -0
  9. package/dist/brushes/index.d.ts +12 -0
  10. package/dist/brushes/index.js +1406 -0
  11. package/dist/brushes/migrate.d.cts +17 -0
  12. package/dist/brushes/migrate.d.ts +17 -0
  13. package/dist/brushes/normalize.d.cts +15 -0
  14. package/dist/brushes/normalize.d.ts +15 -0
  15. package/dist/brushes/presets.d.cts +12 -0
  16. package/dist/brushes/presets.d.ts +12 -0
  17. package/dist/brushes/resolve.d.cts +56 -0
  18. package/dist/brushes/resolve.d.ts +56 -0
  19. package/dist/brushes/schema.d.cts +29 -0
  20. package/dist/brushes/schema.d.ts +29 -0
  21. package/dist/brushes/types.d.cts +323 -0
  22. package/dist/brushes/types.d.ts +323 -0
  23. package/dist/{brushstrokeRender-T3JZST44.js → brushstrokeRender-5PYSKZIJ.js} +9 -8
  24. package/dist/{brushstrokeRender-GCKK7V2I.cjs → brushstrokeRender-7FLLSQEC.cjs} +15 -14
  25. package/dist/canvas/index.cjs +120 -65
  26. package/dist/canvas/index.d.cts +1 -0
  27. package/dist/canvas/index.d.ts +1 -0
  28. package/dist/canvas/index.js +88 -33
  29. package/dist/canvas/stickers.d.cts +35 -0
  30. package/dist/canvas/stickers.d.ts +35 -0
  31. package/dist/chart/index.cjs +159 -160
  32. package/dist/chart/index.js +9 -10
  33. package/dist/{chunk-RXZL4NG5.js → chunk-226QDRO3.js} +1 -1
  34. package/dist/{chunk-DOIU7VJG.js → chunk-2ZJ4DT3M.js} +1 -1
  35. package/dist/{chunk-XZNU4DUX.cjs → chunk-3NS5C7FB.cjs} +76 -76
  36. package/dist/{chunk-UTI5IITZ.cjs → chunk-4ZC5QJE2.cjs} +3 -3
  37. package/dist/{chunk-L3JFOTBQ.js → chunk-5E2Q7ZHI.js} +1 -1
  38. package/dist/{chunk-6BIDVPED.js → chunk-5JNA6LLY.js} +1 -1
  39. package/dist/{chunk-RD7RX52G.cjs → chunk-5UEFBM2M.cjs} +207 -18
  40. package/dist/{chunk-WA44NKPF.js → chunk-62Z5WLRA.js} +1 -1
  41. package/dist/{chunk-BVRRIW7F.js → chunk-6GRUVGKU.js} +3 -3
  42. package/dist/{chunk-QARCM66V.js → chunk-6ZZKRH7R.js} +3 -3
  43. package/dist/{chunk-BP6KSGVD.js → chunk-7272D3K2.js} +4 -6
  44. package/dist/{chunk-O75V4ZS3.js → chunk-7P6VF3FG.js} +2 -2
  45. package/dist/{chunk-AU6MWQKZ.js → chunk-ABL7IWFZ.js} +1 -1
  46. package/dist/{chunk-5RH3VXAK.js → chunk-APX63ILU.js} +4 -100
  47. package/dist/{chunk-W7L4HUT2.cjs → chunk-CJ6TBL2N.cjs} +16 -16
  48. package/dist/{chunk-7PT4RJL5.cjs → chunk-EUIDTZNQ.cjs} +111 -113
  49. package/dist/{chunk-CXXXAZ7H.js → chunk-F6R67I3A.js} +1 -1
  50. package/dist/{chunk-I7NJPHWV.cjs → chunk-FQPUF2W2.cjs} +35 -606
  51. package/dist/{chunk-D5KGAO4Z.js → chunk-GCVFEPJG.js} +2 -573
  52. package/dist/{chunk-LEQKZGQI.js → chunk-GOBOCZZI.js} +1 -1
  53. package/dist/{chunk-PZP2SZIZ.js → chunk-GYMMNCFT.js} +3 -3
  54. package/dist/{chunk-NUTMINJZ.cjs → chunk-H7ET2ISK.cjs} +52 -52
  55. package/dist/chunk-HS6QDWDF.cjs +1029 -0
  56. package/dist/{chunk-T7KQEMAE.cjs → chunk-HUAISCUX.cjs} +18 -18
  57. package/dist/{chunk-YGQVCQTG.js → chunk-ILP77UD6.js} +1 -1
  58. package/dist/{chunk-GHCSC2WP.cjs → chunk-ISIO63Q5.cjs} +21 -15
  59. package/dist/{chunk-S6OT6CTS.cjs → chunk-IWEVV63O.cjs} +31 -33
  60. package/dist/{chunk-OFH6Q67U.js → chunk-JA57JIT5.js} +9 -3
  61. package/dist/{chunk-LP2MCSBE.cjs → chunk-JFHBXNR2.cjs} +88 -88
  62. package/dist/{chunk-IOFVM3L3.cjs → chunk-JPEHXIB5.cjs} +10 -10
  63. package/dist/chunk-JSDND7DF.cjs +173 -0
  64. package/dist/{chunk-36ZTV7WO.js → chunk-L6Y7TSQ4.js} +1 -1
  65. package/dist/{chunk-KYX7NZ5V.cjs → chunk-LFMKWGFL.cjs} +1 -1
  66. package/dist/{chunk-V6DT2CDH.js → chunk-M2TPMYJE.js} +1 -1
  67. package/dist/{chunk-LJZRHVZV.cjs → chunk-M5GLQQWG.cjs} +2 -2
  68. package/dist/{chunk-JEGJP4OW.js → chunk-MHLU6GRJ.js} +196 -7
  69. package/dist/{chunk-KMPM5JM5.js → chunk-MONXNVZK.js} +4 -6
  70. package/dist/chunk-MTKY6K3Q.js +1029 -0
  71. package/dist/{chunk-MOW3JMNG.cjs → chunk-N76AMOKE.cjs} +42 -42
  72. package/dist/{chunk-75K3W4D6.cjs → chunk-NKGB64CW.cjs} +16 -16
  73. package/dist/{chunk-SGPIVHJB.js → chunk-P3K2JCJA.js} +3 -3
  74. package/dist/chunk-PXDIAYB6.cjs +594 -0
  75. package/dist/{chunk-UBRJOIR5.js → chunk-QC3OXRE4.js} +9 -9
  76. package/dist/{chunk-QOBCBGGI.cjs → chunk-QKXO6JPP.cjs} +14 -14
  77. package/dist/{chunk-WLJZZHUZ.cjs → chunk-QLSGTCYT.cjs} +23 -23
  78. package/dist/{chunk-7YW7T5TJ.js → chunk-RHPSE2YR.js} +2 -2
  79. package/dist/{chunk-44BMJJVA.cjs → chunk-RUONPPMC.cjs} +8 -8
  80. package/dist/{chunk-LJRCU3BJ.cjs → chunk-SABJMH6M.cjs} +150 -15
  81. package/dist/{chunk-XWOLPNBW.cjs → chunk-SOSBOS5T.cjs} +22 -22
  82. package/dist/chunk-SZTI6EER.js +13 -0
  83. package/dist/{chunk-PPFJHTW5.cjs → chunk-TFF2GTGN.cjs} +22 -24
  84. package/dist/{chunk-2SMMZDMG.cjs → chunk-TLOGFIIV.cjs} +37 -37
  85. package/dist/chunk-TXFWZ5ZJ.js +594 -0
  86. package/dist/chunk-U67EN7GG.cjs +13 -0
  87. package/dist/{chunk-GXAK6Q5J.js → chunk-UFEDHACQ.js} +2 -2
  88. package/dist/{chunk-GA4EU4P3.cjs → chunk-UXETV2NC.cjs} +4 -4
  89. package/dist/{chunk-N4DFDI73.js → chunk-WKMUBATU.js} +1 -1
  90. package/dist/{chunk-3R5P3DVY.cjs → chunk-WRZ6C4TU.cjs} +6 -6
  91. package/dist/{chunk-EJLRBS5Q.js → chunk-WZOGDKCQ.js} +137 -2
  92. package/dist/{chunk-6ADHGQ73.js → chunk-YBAPT7NL.js} +2 -4
  93. package/dist/{chunk-K5DS2JTA.cjs → chunk-Z3BRH4JK.cjs} +6 -6
  94. package/dist/{chunk-XNZFVYC2.cjs → chunk-ZBCT5MEA.cjs} +7 -7
  95. package/dist/color/index.cjs +4 -6
  96. package/dist/color/index.js +7 -9
  97. package/dist/effects/helpers.d.cts +6 -0
  98. package/dist/effects/helpers.d.ts +6 -0
  99. package/dist/effects/index.cjs +62 -60
  100. package/dist/effects/index.js +5 -3
  101. package/dist/effects/tools.d.cts +6 -0
  102. package/dist/effects/tools.d.ts +6 -0
  103. package/dist/export/index.cjs +7 -8
  104. package/dist/export/index.js +6 -7
  105. package/dist/extensions/index.cjs +10 -11
  106. package/dist/extensions/index.js +9 -10
  107. package/dist/gradient/index.cjs +33 -34
  108. package/dist/gradient/index.js +4 -5
  109. package/dist/image-editor/effects.d.cts +1 -1
  110. package/dist/image-editor/effects.d.ts +1 -1
  111. package/dist/image-editor/index.cjs +887 -863
  112. package/dist/image-editor/index.d.cts +1 -0
  113. package/dist/image-editor/index.d.ts +1 -0
  114. package/dist/image-editor/index.js +69 -45
  115. package/dist/image-editor/stickers.d.cts +9 -0
  116. package/dist/image-editor/stickers.d.ts +9 -0
  117. package/dist/index.cjs +16 -17
  118. package/dist/index.js +10 -11
  119. package/dist/paint/index.cjs +6 -4
  120. package/dist/paint/index.js +21 -19
  121. package/dist/paint-editor/brushes.d.cts +1 -2
  122. package/dist/paint-editor/brushes.d.ts +1 -2
  123. package/dist/paint-editor/index.cjs +2199 -699
  124. package/dist/paint-editor/index.js +1577 -77
  125. package/dist/paint-editor/mark-settings.d.cts +104 -0
  126. package/dist/paint-editor/mark-settings.d.ts +104 -0
  127. package/dist/paint-editor/procedural.d.cts +2 -75
  128. package/dist/paint-editor/procedural.d.ts +2 -75
  129. package/dist/paint-editor/stickers.d.cts +1 -22
  130. package/dist/paint-editor/stickers.d.ts +1 -22
  131. package/dist/raster/index.cjs +2 -2
  132. package/dist/raster/index.js +9 -9
  133. package/dist/stickers/index.cjs +22 -16
  134. package/dist/stickers/index.d.cts +1 -1
  135. package/dist/stickers/index.d.ts +1 -1
  136. package/dist/stickers/index.js +14 -8
  137. package/dist/stickers/tools.d.cts +57 -1
  138. package/dist/stickers/tools.d.ts +57 -1
  139. package/dist/tokens/index.cjs +3 -4
  140. package/dist/tokens/index.js +2 -3
  141. package/dist/typography/index.cjs +7 -8
  142. package/dist/typography/index.js +2 -3
  143. package/dist/vector/index.cjs +1011 -977
  144. package/dist/vector/index.d.cts +1 -0
  145. package/dist/vector/index.d.ts +1 -0
  146. package/dist/vector/index.js +47 -13
  147. package/dist/vector/stickers.d.cts +11 -0
  148. package/dist/vector/stickers.d.ts +11 -0
  149. package/dist/video-editor/index.cjs +349 -350
  150. package/dist/video-editor/index.js +9 -10
  151. package/package.json +11 -1
  152. package/skills/pixerate-image-editor/SKILL.md +1 -1
  153. package/skills/pixerate-stickers/SKILL.md +13 -1
  154. package/dist/chunk-7AHBW2CQ.cjs +0 -200
  155. package/dist/chunk-F67XMVDI.js +0 -200
  156. package/dist/chunk-H4AOUUBE.cjs +0 -2635
  157. package/dist/chunk-J7T5QZ45.js +0 -2635
  158. package/dist/chunk-ZEYMKDYS.cjs +0 -269
@@ -0,0 +1,1029 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+
4
+ var _chunkJSDND7DFcjs = require('./chunk-JSDND7DF.cjs');
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+ var _chunkFQPUF2W2cjs = require('./chunk-FQPUF2W2.cjs');
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 [_chunkJSDND7DFcjs.normalizeStickerRef.call(void 0, s)];
26
+ } catch (e) {
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) => _chunkJSDND7DFcjs.STAMP_KINDS.includes(t)).slice(0, MAX_DOT_TEXTURES) : [..._nullishCoalesce(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, _nullishCoalesce(base.ribbonFill, () => ( DEFAULT_PARTICLE.ribbonFill))),
83
+ ribbonPairs: oneOf(raw.ribbonPairs, RIBBON_PAIRS, _nullishCoalesce(base.ribbonPairs, () => ( DEFAULT_PARTICLE.ribbonPairs))),
84
+ waveAmplitude: unit(raw.waveAmplitude, _nullishCoalesce(base.waveAmplitude, () => ( 0))),
85
+ waveLength: typeof raw.waveLength === "number" && Number.isFinite(raw.waveLength) ? Math.min(2e3, Math.max(4, raw.waveLength)) : _nullishCoalesce(base.waveLength, () => ( DEFAULT_PARTICLE.waveLength)),
86
+ waveSpeed: unit(raw.waveSpeed, _nullishCoalesce(base.waveSpeed, () => ( 0))),
87
+ wavePhaseJitter: unit(raw.wavePhaseJitter, _nullishCoalesce(base.wavePhaseJitter, () => ( 0))),
88
+ dotTextures: textures,
89
+ dotTextureOrder: oneOf(raw.dotTextureOrder, DOT_TEXTURE_ORDERS, _nullishCoalesce(base.dotTextureOrder, () => ( DEFAULT_PARTICLE.dotTextureOrder))),
90
+ dotRotation: oneOf(raw.dotRotation, DOT_ROTATIONS, _nullishCoalesce(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 : _nullishCoalesce(base.medium, () => ( "watercolor")),
120
+ load: unit2(raw.load, _nullishCoalesce(base.load, () => ( 1))),
121
+ thinner: unit2(raw.thinner, _nullishCoalesce(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, _nullishCoalesce(base.rim, () => ( 0))),
140
+ clarity: unit2(raw.clarity, _nullishCoalesce(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(_nullishCoalesce(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 = _nullishCoalesce(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: _nullishCoalesce(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 = _nullishCoalesce(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 = _nullishCoalesce(raw.behavior, () => ( {}));
259
+ const mixerIn = { ...b.mixer, ..._nullishCoalesce(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 = _chunkFQPUF2W2cjs.normalizeMixer.call(void 0, 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 = _nullishCoalesce(b.paint, () => ( null));
270
+ if (raw.paint !== void 0) {
271
+ const p = _chunkFQPUF2W2cjs.readPaintSource.call(void 0, raw.paint);
272
+ paint = p && p.type === "gradient" ? p : null;
273
+ } else if (rb.multicolor !== void 0) {
274
+ const mc = normalizeMultiColor(rb.multicolor, _nullishCoalesce(paintToMultiColor(paint), () => ( DEFAULT_MULTI_COLOR)));
275
+ paint = mc.mode === "off" ? paintToMultiColor(paint) ? null : paint : multiColorToPaint(mc, behavior.mixing);
276
+ }
277
+ behavior = { ...behavior, multicolor: _nullishCoalesce(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: _chunkFQPUF2W2cjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(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(_nullishCoalesce(raw.dynamics, () => ( {})), b.dynamics),
288
+ mixer,
289
+ loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
290
+ behavior,
291
+ blender: _chunkFQPUF2W2cjs.normalizeBlender.call(void 0, _nullishCoalesce(raw.blender, () => ( {})), b.blender),
292
+ smudge: _chunkFQPUF2W2cjs.normalizeSmudge.call(void 0, _nullishCoalesce(raw.smudge, () => ( {})), b.smudge),
293
+ wet: normalizeWetBrush(_nullishCoalesce(raw.wet, () => ( {})), _nullishCoalesce(b.wet, () => ( DEFAULT_WET_BRUSH))),
294
+ impasto: normalizeImpasto(_nullishCoalesce(raw.impasto, () => ( {})), _nullishCoalesce(b.impasto, () => ( DEFAULT_IMPASTO))),
295
+ particle: normalizeParticle(_nullishCoalesce(raw.particle, () => ( {})), _nullishCoalesce(b.particle, () => ( DEFAULT_PARTICLE))),
296
+ bristle: normalizeBristle(_nullishCoalesce(raw.bristle, () => ( {})), _nullishCoalesce(b.bristle, () => ( DEFAULT_BRISTLE))),
297
+ sticker: normalizeStickerSettings(_nullishCoalesce(raw.sticker, () => ( {})), _nullishCoalesce(b.sticker, () => ( DEFAULT_STICKER))),
298
+ light: normalizeLight(_nullishCoalesce(raw.light, () => ( {})), _nullishCoalesce(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: { ..._chunkFQPUF2W2cjs.DEFAULT_BRUSH },
309
+ grain: 0,
310
+ pressureSmoothing: 0,
311
+ rotation: "fixed",
312
+ dynamics: { ...DEFAULT_DYNAMICS },
313
+ mixer: { ..._chunkFQPUF2W2cjs.DEFAULT_MIXER },
314
+ loadsColor: true,
315
+ behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
316
+ blender: { ..._chunkFQPUF2W2cjs.DEFAULT_BLENDER },
317
+ smudge: { ..._chunkFQPUF2W2cjs.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
+
985
+
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
+
995
+
996
+
997
+
998
+
999
+
1000
+
1001
+
1002
+
1003
+
1004
+
1005
+
1006
+
1007
+
1008
+
1009
+
1010
+
1011
+
1012
+
1013
+
1014
+
1015
+
1016
+
1017
+
1018
+
1019
+
1020
+
1021
+
1022
+
1023
+
1024
+
1025
+
1026
+
1027
+
1028
+
1029
+ 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;