@pixerate/editor 0.267.0 → 0.268.1

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 (83) hide show
  1. package/dist/animation-editor/index.cjs +62 -62
  2. package/dist/animation-editor/index.js +6 -6
  3. package/dist/brushes/index.cjs +198 -72
  4. package/dist/brushes/index.d.cts +4 -3
  5. package/dist/brushes/index.d.ts +4 -3
  6. package/dist/brushes/index.js +136 -10
  7. package/dist/brushes/library.d.cts +27 -1
  8. package/dist/brushes/library.d.ts +27 -1
  9. package/dist/brushes/profiles.d.cts +4 -0
  10. package/dist/brushes/profiles.d.ts +4 -0
  11. package/dist/brushes/schema.d.cts +4 -0
  12. package/dist/brushes/schema.d.ts +4 -0
  13. package/dist/brushes/sources.d.cts +114 -0
  14. package/dist/brushes/sources.d.ts +114 -0
  15. package/dist/brushes/types.d.cts +37 -5
  16. package/dist/brushes/types.d.ts +37 -5
  17. package/dist/brushes/ui.d.cts +40 -4
  18. package/dist/brushes/ui.d.ts +40 -4
  19. package/dist/{brushstrokeRender-NLFPEBGI.js → brushstrokeRender-A727TTNQ.js} +6 -6
  20. package/dist/{brushstrokeRender-L7JYRR2R.cjs → brushstrokeRender-WR2N5HHY.cjs} +22 -22
  21. package/dist/canvas/index.cjs +13 -13
  22. package/dist/canvas/index.js +1 -1
  23. package/dist/{chunk-J2TNIZ55.cjs → chunk-2WOGXEDX.cjs} +1 -1
  24. package/dist/{chunk-JGWVZPT6.js → chunk-3XDSNIGF.js} +72 -5
  25. package/dist/{chunk-EXB2EGBE.cjs → chunk-62MFUHSI.cjs} +16 -16
  26. package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
  27. package/dist/{chunk-EF7LZAXQ.js → chunk-BAZMCFVL.js} +1 -1
  28. package/dist/{chunk-HIO6E7LC.cjs → chunk-BPJSI4NT.cjs} +2 -2
  29. package/dist/{chunk-XVOP3CGQ.cjs → chunk-EU2ZZA6L.cjs} +393 -62
  30. package/dist/{chunk-YIC6MAJ2.js → chunk-HFY5LG5J.js} +1 -1
  31. package/dist/{chunk-7UKFXXWL.js → chunk-HVO6XCI4.js} +1 -1
  32. package/dist/{chunk-LHPEG5AU.cjs → chunk-J4GXP53R.cjs} +140 -91
  33. package/dist/{chunk-OKHKA43L.cjs → chunk-JBB3GDBB.cjs} +330 -216
  34. package/dist/{chunk-AGNUD5BI.js → chunk-KZI3YWKW.js} +351 -20
  35. package/dist/{chunk-FGJHJIMN.js → chunk-LDOD7VCT.js} +59 -10
  36. package/dist/{chunk-EYSHTYLG.js → chunk-LETF75CA.js} +1 -1
  37. package/dist/{chunk-FQPUF2W2.cjs → chunk-NIBFKXMD.cjs} +217 -35
  38. package/dist/{chunk-G7NHE2VD.cjs → chunk-NPWWSCWT.cjs} +35 -35
  39. package/dist/{chunk-OSEXT7JL.js → chunk-QNY7BDFP.js} +1 -1
  40. package/dist/{chunk-PXDIAYB6.cjs → chunk-STLTOISW.cjs} +34 -214
  41. package/dist/{chunk-4IDUYB45.js → chunk-UVTO3D4E.js} +130 -16
  42. package/dist/{chunk-TXFWZ5ZJ.js → chunk-W6JDUXP3.js} +4 -184
  43. package/dist/{chunk-LA5Y4ETA.cjs → chunk-YOVXQHA3.cjs} +23 -23
  44. package/dist/{chunk-DT4YQUGT.cjs → chunk-YXP6X3SY.cjs} +101 -34
  45. package/dist/image-editor/adjustment-layers.d.cts +8 -0
  46. package/dist/image-editor/adjustment-layers.d.ts +8 -0
  47. package/dist/image-editor/index.cjs +857 -853
  48. package/dist/image-editor/index.js +30 -26
  49. package/dist/paint/index.cjs +5 -5
  50. package/dist/paint/index.js +11 -11
  51. package/dist/paint-editor/brush-library-tools.d.cts +12 -2
  52. package/dist/paint-editor/brush-library-tools.d.ts +12 -2
  53. package/dist/paint-editor/brush-model.d.cts +29 -1
  54. package/dist/paint-editor/brush-model.d.ts +29 -1
  55. package/dist/paint-editor/brush-source-art.d.cts +32 -0
  56. package/dist/paint-editor/brush-source-art.d.ts +32 -0
  57. package/dist/paint-editor/controller.d.cts +6 -1
  58. package/dist/paint-editor/controller.d.ts +6 -1
  59. package/dist/paint-editor/dab-stroke.d.cts +2 -0
  60. package/dist/paint-editor/dab-stroke.d.ts +2 -0
  61. package/dist/paint-editor/engine.d.cts +11 -2
  62. package/dist/paint-editor/engine.d.ts +11 -2
  63. package/dist/paint-editor/index.cjs +576 -481
  64. package/dist/paint-editor/index.d.cts +3 -1
  65. package/dist/paint-editor/index.d.ts +3 -1
  66. package/dist/paint-editor/index.js +127 -32
  67. package/dist/paint-editor/stamp-tip.d.cts +8 -5
  68. package/dist/paint-editor/stamp-tip.d.ts +8 -5
  69. package/dist/paint-editor/thumbnails.d.cts +6 -0
  70. package/dist/paint-editor/thumbnails.d.ts +6 -0
  71. package/dist/paint-editor/tip-grain.d.cts +34 -0
  72. package/dist/paint-editor/tip-grain.d.ts +34 -0
  73. package/dist/raster/index.cjs +2 -2
  74. package/dist/raster/index.js +9 -9
  75. package/dist/vector/index.cjs +1002 -1002
  76. package/dist/vector/index.js +7 -7
  77. package/package.json +110 -1
  78. package/skills/pixerate-brushes/SKILL.md +35 -8
  79. package/skills/pixerate-brushes-design/SKILL.md +8 -1
  80. package/skills/pixerate-image-editor/SKILL.md +1 -1
  81. package/skills/pixerate-paint-editor/SKILL.md +6 -0
  82. package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
  83. package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
@@ -18,11 +18,17 @@ var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
18
18
 
19
19
 
20
20
 
21
- var _chunkFQPUF2W2cjs = require('./chunk-FQPUF2W2.cjs');
21
+
22
+ var _chunkNIBFKXMDcjs = require('./chunk-NIBFKXMD.cjs');
22
23
 
23
24
 
24
25
  var _chunk2VX55NFMcjs = require('./chunk-2VX55NFM.cjs');
25
26
 
27
+
28
+
29
+
30
+ var _chunkW5M3J7PVcjs = require('./chunk-W5M3J7PV.cjs');
31
+
26
32
  // src/brushes/presets.ts
27
33
  var BRUSH_PRESETS = [
28
34
  {
@@ -908,6 +914,9 @@ var MULTI_COLOR_MODES = ["off", "tip", "dab"];
908
914
  var DEFAULT_MULTI_COLOR = { mode: "off", colors: [], order: "sequence", length: 12, softness: 1 };
909
915
  var DEFAULT_BEHAVIOR = { pickup: 0, mix: 0.5, dirty: false, mixing: "pigment", multicolor: DEFAULT_MULTI_COLOR };
910
916
  var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
917
+ var DAB_FLIPS = ["none", "x", "y", "xy"];
918
+ var TIP_GRAIN_MODES = ["moving", "texturised"];
919
+ var DEFAULT_TIP_GRAIN = { mode: "texturised", scale: 1, depth: 0.6, contrast: 1, invert: false };
911
920
  var DEFAULT_DYNAMICS = {
912
921
  minSize: 0,
913
922
  pressureFlow: 0,
@@ -935,7 +944,8 @@ function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
935
944
  sizeJitter: unit2(raw.sizeJitter, base.sizeJitter),
936
945
  angleJitter: unit2(raw.angleJitter, base.angleJitter),
937
946
  opacityJitter: unit2(raw.opacityJitter, base.opacityJitter),
938
- scatter: range(raw.scatter, 0, 4, base.scatter)
947
+ scatter: range(raw.scatter, 0, 4, base.scatter),
948
+ ...countAndFlip(raw, base)
939
949
  };
940
950
  }
941
951
  var BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
@@ -959,6 +969,28 @@ function normalizeBehavior(raw = {}, base = DEFAULT_BEHAVIOR) {
959
969
  multicolor: normalizeMultiColor(_nullishCoalesce(raw.multicolor, () => ( {})), base.multicolor)
960
970
  };
961
971
  }
972
+ function countAndFlip(raw, base) {
973
+ const n = typeof raw.count === "number" && Number.isFinite(raw.count) ? Math.min(16, Math.max(1, Math.round(raw.count))) : _nullishCoalesce(base.count, () => ( 1));
974
+ const flip = DAB_FLIPS.includes(raw.flip) ? raw.flip : _nullishCoalesce(base.flip, () => ( "none"));
975
+ return { ...n > 1 ? { count: n } : {}, ...flip !== "none" ? { flip } : {} };
976
+ }
977
+ var withTipGrain = (g) => g ? { tipGrain: g } : {};
978
+ function readTipGrain(raw, base) {
979
+ if (raw === null) return null;
980
+ if (!raw || typeof raw !== "object") return base;
981
+ const r = raw;
982
+ const source = typeof r.source === "string" && /^\S{1,200}$/.test(r.source) ? r.source : _optionalChain([base, 'optionalAccess', _2 => _2.source]);
983
+ if (!source) return base;
984
+ const b = _nullishCoalesce(base, () => ( { source, ...DEFAULT_TIP_GRAIN }));
985
+ return {
986
+ source,
987
+ mode: TIP_GRAIN_MODES.includes(r.mode) ? r.mode : b.mode,
988
+ scale: range(r.scale, 0.05, 20, b.scale),
989
+ depth: unit2(r.depth, b.depth),
990
+ contrast: range(r.contrast, 0, 3, b.contrast),
991
+ invert: typeof r.invert === "boolean" ? r.invert : b.invert
992
+ };
993
+ }
962
994
  var DEFAULT_SPRAY = { density: 0.5, size: 0.12 };
963
995
  function readStamp(raw, base) {
964
996
  if (raw === null) return null;
@@ -1041,12 +1073,12 @@ function normalizePaintBrush(raw = {}, base) {
1041
1073
  if (rb.dirty !== void 0) mixerIn.cleanEachStroke = !rb.dirty;
1042
1074
  if (rb.mixing !== void 0) mixerIn.mixing = rb.mixing;
1043
1075
  }
1044
- const mixer = _chunkFQPUF2W2cjs.normalizeMixer.call(void 0, mixerIn);
1076
+ const mixer = _chunkNIBFKXMDcjs.normalizeMixer.call(void 0, mixerIn);
1045
1077
  let behavior = normalizeBehavior(rb, b.behavior);
1046
1078
  if (engine === "mixer") behavior = { ...behavior, pickup: mixer.wet, mix: mixer.mix, dirty: !mixer.cleanEachStroke, mixing: mixer.mixing };
1047
1079
  let paint = _nullishCoalesce(b.paint, () => ( null));
1048
1080
  if (raw.paint !== void 0) {
1049
- const p = _chunkFQPUF2W2cjs.readPaintSource.call(void 0, raw.paint);
1081
+ const p = _chunkNIBFKXMDcjs.readPaintSource.call(void 0, raw.paint);
1050
1082
  paint = p && p.type === "gradient" ? p : null;
1051
1083
  } else if (rb.multicolor !== void 0) {
1052
1084
  const mc = normalizeMultiColor(rb.multicolor, _nullishCoalesce(paintToMultiColor(paint), () => ( DEFAULT_MULTI_COLOR)));
@@ -1058,7 +1090,7 @@ function normalizePaintBrush(raw = {}, base) {
1058
1090
  name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
1059
1091
  category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
1060
1092
  engine,
1061
- tip: _chunkFQPUF2W2cjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(raw.tip, () => ( {})) }),
1093
+ tip: _chunkNIBFKXMDcjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(raw.tip, () => ( {})) }),
1062
1094
  grain: unit2(raw.grain, b.grain),
1063
1095
  pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
1064
1096
  rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
@@ -1066,8 +1098,8 @@ function normalizePaintBrush(raw = {}, base) {
1066
1098
  mixer,
1067
1099
  loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
1068
1100
  behavior,
1069
- blender: _chunkFQPUF2W2cjs.normalizeBlender.call(void 0, _nullishCoalesce(raw.blender, () => ( {})), b.blender),
1070
- smudge: _chunkFQPUF2W2cjs.normalizeSmudge.call(void 0, _nullishCoalesce(raw.smudge, () => ( {})), b.smudge),
1101
+ blender: _chunkNIBFKXMDcjs.normalizeBlender.call(void 0, _nullishCoalesce(raw.blender, () => ( {})), b.blender),
1102
+ smudge: _chunkNIBFKXMDcjs.normalizeSmudge.call(void 0, _nullishCoalesce(raw.smudge, () => ( {})), b.smudge),
1071
1103
  wet: normalizeWetBrush(_nullishCoalesce(raw.wet, () => ( {})), _nullishCoalesce(b.wet, () => ( DEFAULT_WET_BRUSH))),
1072
1104
  impasto: normalizeImpasto(_nullishCoalesce(raw.impasto, () => ( {})), _nullishCoalesce(b.impasto, () => ( DEFAULT_IMPASTO))),
1073
1105
  particle: normalizeParticle(_nullishCoalesce(raw.particle, () => ( {})), _nullishCoalesce(b.particle, () => ( DEFAULT_PARTICLE))),
@@ -1076,6 +1108,7 @@ function normalizePaintBrush(raw = {}, base) {
1076
1108
  light: normalizeLight(_nullishCoalesce(raw.light, () => ( {})), _nullishCoalesce(b.light, () => ( DEFAULT_LIGHT))),
1077
1109
  stamp: readStamp(raw.stamp, _nullishCoalesce(b.stamp, () => ( null))),
1078
1110
  spray: readSpray(raw.spray, _nullishCoalesce(b.spray, () => ( null))),
1111
+ ...withTipGrain(readTipGrain(raw.tipGrain, _nullishCoalesce(b.tipGrain, () => ( null)))),
1079
1112
  paint,
1080
1113
  ...raw.builtin ? { builtin: true } : {}
1081
1114
  };
@@ -1085,16 +1118,16 @@ var BASE_BRUSH = {
1085
1118
  name: "Round",
1086
1119
  category: "custom",
1087
1120
  engine: "dab",
1088
- tip: { ..._chunkFQPUF2W2cjs.DEFAULT_BRUSH },
1121
+ tip: { ..._chunkNIBFKXMDcjs.DEFAULT_BRUSH },
1089
1122
  grain: 0,
1090
1123
  pressureSmoothing: 0,
1091
1124
  rotation: "fixed",
1092
1125
  dynamics: { ...DEFAULT_DYNAMICS },
1093
- mixer: { ..._chunkFQPUF2W2cjs.DEFAULT_MIXER },
1126
+ mixer: { ..._chunkNIBFKXMDcjs.DEFAULT_MIXER },
1094
1127
  loadsColor: true,
1095
1128
  behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
1096
- blender: { ..._chunkFQPUF2W2cjs.DEFAULT_BLENDER },
1097
- smudge: { ..._chunkFQPUF2W2cjs.DEFAULT_SMUDGE },
1129
+ blender: { ..._chunkNIBFKXMDcjs.DEFAULT_BLENDER },
1130
+ smudge: { ..._chunkNIBFKXMDcjs.DEFAULT_SMUDGE },
1098
1131
  wet: { ...DEFAULT_WET_BRUSH },
1099
1132
  impasto: { ...DEFAULT_IMPASTO },
1100
1133
  particle: { ...DEFAULT_PARTICLE },
@@ -1109,6 +1142,7 @@ var BASE_BRUSH = {
1109
1142
  // src/brushes/resolve.ts
1110
1143
  var ALL = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
1111
1144
  var EXCHANGE = ["dab", "wet", "impasto"];
1145
+ var DABS = ["dab", "eraser", "mixer", "blender", "smudge"];
1112
1146
  var NOT = { toEngine: (v) => !v, toFacet: (v) => !v };
1113
1147
  var EDITOR_STAMPS = {
1114
1148
  toEngine: (v) => v.flatMap((ref) => {
@@ -1133,6 +1167,9 @@ var BRUSH_FIELDS = [
1133
1167
  f("dynamics.pressureSmoothing", "pressureSmoothing"),
1134
1168
  f("dynamics.rotation", "rotation"),
1135
1169
  ...each("dynamics", "dynamics", ["minSize", "pressureFlow", "tiltSize", "tiltRoundness", "tiltOpacity", "velocitySize", "velocityOpacity", "sizeJitter", "angleJitter", "opacityJitter", "scatter"]),
1170
+ f("dynamics.count", "dynamics.count", DABS),
1171
+ f("dynamics.flip", "dynamics.flip", DABS),
1172
+ f("tip.grain", "tipGrain", DABS),
1136
1173
  f("behaviour.grain", "grain"),
1137
1174
  ...each("light", "light", ["amount", "pressure"]),
1138
1175
  // Paint exchange: the dab, wet and impasto engines' behaviour, or the Mixer Brush's settings.
@@ -1195,13 +1232,13 @@ function setPath(o, path, value) {
1195
1232
  var copy = (v) => v !== null && typeof v === "object" ? JSON.parse(JSON.stringify(v)) : v;
1196
1233
  function resolveEngine(brush) {
1197
1234
  if (brush.kind === "tool") return brush.tool === "eraser" || brush.tool === "blender" || brush.tool === "smudge" ? brush.tool : "wet";
1198
- if (_optionalChain([brush, 'access', _2 => _2.marks, 'optionalAccess', _3 => _3.kind]) === "particles") return "particle";
1199
- if (_optionalChain([brush, 'access', _4 => _4.marks, 'optionalAccess', _5 => _5.kind]) === "objects") return "sticker";
1235
+ if (_optionalChain([brush, 'access', _3 => _3.marks, 'optionalAccess', _4 => _4.kind]) === "particles") return "particle";
1236
+ if (_optionalChain([brush, 'access', _5 => _5.marks, 'optionalAccess', _6 => _6.kind]) === "objects") return "sticker";
1200
1237
  if (brush.tip.shape === "bristle") return "bristle";
1201
- const surface = _optionalChain([brush, 'access', _6 => _6.behaviour, 'optionalAccess', _7 => _7.surface]);
1238
+ const surface = _optionalChain([brush, 'access', _7 => _7.behaviour, 'optionalAccess', _8 => _8.surface]);
1202
1239
  if (surface === "thick") return "impasto";
1203
1240
  if (surface === "wet") return "wet";
1204
- if (_optionalChain([brush, 'access', _8 => _8.behaviour, 'optionalAccess', _9 => _9.exchange, 'optionalAccess', _10 => _10.reservoir])) return "mixer";
1241
+ if (_optionalChain([brush, 'access', _9 => _9.behaviour, 'optionalAccess', _10 => _10.exchange, 'optionalAccess', _11 => _11.reservoir])) return "mixer";
1205
1242
  return "dab";
1206
1243
  }
1207
1244
  var VARIETY_CATEGORY = {
@@ -1217,11 +1254,11 @@ var VARIETY_CATEGORY = {
1217
1254
  };
1218
1255
  var MEDIUM_CATEGORY = { paint: "paint", ink: "ink", dry: "pencil", spray: "airbrush" };
1219
1256
  function resolveWetMedium(brush) {
1220
- return _nullishCoalesce(_optionalChain([brush, 'access', _11 => _11.behaviour, 'optionalAccess', _12 => _12.wet, 'optionalAccess', _13 => _13.medium]), () => ( (WET_MEDIA.includes(brush.variety) ? brush.variety : void 0)));
1257
+ return _nullishCoalesce(_optionalChain([brush, 'access', _12 => _12.behaviour, 'optionalAccess', _13 => _13.wet, 'optionalAccess', _14 => _14.medium]), () => ( (WET_MEDIA.includes(brush.variety) ? brush.variety : void 0)));
1221
1258
  }
1222
1259
  function resolveCategory(brush, engine = resolveEngine(brush)) {
1223
1260
  if (brush.kind === "tool") return brush.tool === "eraser" || brush.tool === "blender" || brush.tool === "smudge" ? brush.tool : "wet-tool";
1224
- if ((_nullishCoalesce(_optionalChain([brush, 'access', _14 => _14.light, 'optionalAccess', _15 => _15.amount]), () => ( 0))) > 0) return "light";
1261
+ if ((_nullishCoalesce(_optionalChain([brush, 'access', _15 => _15.light, 'optionalAccess', _16 => _16.amount]), () => ( 0))) > 0) return "light";
1225
1262
  switch (engine) {
1226
1263
  case "particle":
1227
1264
  return "procedural";
@@ -1243,18 +1280,18 @@ function resolveBrush(brush, options = {}) {
1243
1280
  const engine = resolveEngine(brush);
1244
1281
  const input = { id: brush.id, name: brush.name, category: resolveCategory(brush, engine), engine };
1245
1282
  for (const field of BRUSH_FIELDS) {
1246
- if (!field.engines.includes(engine) || field.shapes && !field.shapes.includes(brush.tip.shape) || field.marks && !field.marks.includes(_nullishCoalesce(_optionalChain([brush, 'access', _16 => _16.marks, 'optionalAccess', _17 => _17.kind]), () => ( "continuous")))) continue;
1283
+ if (!field.engines.includes(engine) || field.shapes && !field.shapes.includes(brush.tip.shape) || field.marks && !field.marks.includes(_nullishCoalesce(_optionalChain([brush, 'access', _17 => _17.marks, 'optionalAccess', _18 => _18.kind]), () => ( "continuous")))) continue;
1247
1284
  const v = getPath(brush, field.facet);
1248
1285
  if (v !== void 0) setPath(input, field.engine, field.map ? field.map.toEngine(v) : copy(v));
1249
1286
  }
1250
- if (engine === "dab" && _optionalChain([brush, 'access', _18 => _18.marks, 'optionalAccess', _19 => _19.kind]) === "scatter") input.spray = { ..._nullishCoalesce(input.spray, () => ( {})) };
1287
+ if (engine === "dab" && _optionalChain([brush, 'access', _19 => _19.marks, 'optionalAccess', _20 => _20.kind]) === "scatter") input.spray = { ..._nullishCoalesce(input.spray, () => ( {})) };
1251
1288
  if (engine === "wet") {
1252
1289
  setPath(input, "wet.tool", brush.kind === "tool" ? WET_TOOL_OF[brush.tool] : "paint");
1253
1290
  const medium = resolveWetMedium(brush);
1254
1291
  if (medium) setPath(input, "wet.medium", medium);
1255
1292
  }
1256
1293
  if (engine === "blender" || engine === "smudge") input.loadsColor = false;
1257
- const paint = _optionalChain([brush, 'access', _20 => _20.colour, 'optionalAccess', _21 => _21.default]);
1294
+ const paint = _optionalChain([brush, 'access', _21 => _21.colour, 'optionalAccess', _22 => _22.default]);
1258
1295
  if (paint && typeof paint === "object" && "type" in paint && paint.type === "gradient") input.paint = copy(paint);
1259
1296
  return normalizePaintBrush({ ...input, ...options.builtin ? { builtin: true } : {} });
1260
1297
  }
@@ -1340,7 +1377,7 @@ function migrateBrushV1(raw) {
1340
1377
  const brush = skeleton(b);
1341
1378
  const defaults = resolveBrush(brush);
1342
1379
  for (const field of BRUSH_FIELDS) {
1343
- if (!field.engines.includes(b.engine) || field.shapes && !field.shapes.includes(brush.tip.shape) || field.marks && !field.marks.includes(_nullishCoalesce(_optionalChain([brush, 'access', _22 => _22.marks, 'optionalAccess', _23 => _23.kind]), () => ( "continuous")))) continue;
1380
+ if (!field.engines.includes(b.engine) || field.shapes && !field.shapes.includes(brush.tip.shape) || field.marks && !field.marks.includes(_nullishCoalesce(_optionalChain([brush, 'access', _23 => _23.marks, 'optionalAccess', _24 => _24.kind]), () => ( "continuous")))) continue;
1344
1381
  const v = getPath(b, field.engine);
1345
1382
  if (!equal(v, getPath(defaults, field.engine))) setPath(brush, field.facet, field.map ? field.map.toFacet(v) : copy2(v));
1346
1383
  }
@@ -1355,6 +1392,8 @@ var BRUSH_TOOLS = ["eraser", "blender", "smudge", "water", "dryer", "blow", "blo
1355
1392
  var BRUSH_TIP_SHAPES = ["round", "flat", "chisel", "bristle", "stamp"];
1356
1393
  var BRUSH_MARKS_KINDS = ["continuous", "scatter", "particles", "objects"];
1357
1394
  var BRUSH_SURFACES = ["dry", "wet", "thick"];
1395
+ var BRUSH_GRAIN_MODES = ["moving", "texturised"];
1396
+ var BRUSH_FLIPS = ["none", "x", "y", "xy"];
1358
1397
 
1359
1398
  // src/brushes/schema.ts
1360
1399
  var MAX_BRUSH_PALETTE = 16;
@@ -1376,8 +1415,22 @@ var BRUSH_STAMP_PATTERN = `^(?:[^:\\s]+:\\S+|${_chunkRDFHWAHLcjs.STAMP_KINDS.joi
1376
1415
  var STAMP = {
1377
1416
  type: "string",
1378
1417
  pattern: BRUSH_STAMP_PATTERN,
1379
- description: `A sticker ref provider:id (find_stickers lists them; the editor's own are ${_chunkRDFHWAHLcjs.STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}). A bare editor stamp (star) means editor:star.`
1418
+ description: `A sticker ref provider:id (find_stickers lists them; the editor's own are ${_chunkRDFHWAHLcjs.STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}), or an image source source:<id> (list_brush_sources lists them; add_brush_source makes one from an image). A bare editor stamp (star) means editor:star.`
1380
1419
  };
1420
+ var TIP_GRAIN_PATTERNS = _chunkNIBFKXMDcjs.GRAIN_PATTERNS.filter((p) => p !== "smooth");
1421
+ var BRUSH_GRAIN_SOURCE_PATTERN = `^(?:source:[a-z0-9][a-z0-9-]{0,63}|${TIP_GRAIN_PATTERNS.join("|")})$`;
1422
+ var TIP_GRAIN = object(
1423
+ "Texture inside the tip (a grain source), separate from the paper's grain: each dab is multiplied by it. Continuous and scatter marks of round and stamp tips.",
1424
+ {
1425
+ source: { type: "string", pattern: BRUSH_GRAIN_SOURCE_PATTERN, description: `The texture: a built-in pattern (${TIP_GRAIN_PATTERNS.join(", ")}) or an image source source:<id> (list_brush_sources); light parts are peaks, which paint.` },
1426
+ mode: oneOf2(BRUSH_GRAIN_MODES, "texturised (default): the grain stays put on the canvas, a texture under the paint; moving: it rolls along with the tip, turning with it."),
1427
+ scale: num(0.05, 20, "Grain size: 1 is the pattern's or image's own size, 2 twice as coarse (default 1)."),
1428
+ depth: unit3("How much the grain takes out of each dab: 0 none, 1 only the peaks paint (default 0.6)."),
1429
+ contrast: num(0, 3, "Height contrast around the middle: below 1 softer, above 1 harsher (default 1)."),
1430
+ invert: bool("Swap peaks and valleys (default false).")
1431
+ },
1432
+ ["source"]
1433
+ );
1381
1434
  var BRISTLES = object("Bristle tips: the tuft. Missing fields take their defaults.", {
1382
1435
  count: int(3, 128, "Bristles in the tuft (default 32)."),
1383
1436
  stiffness: unit3("Stiff bristles follow the tip; soft ones lag and bend into curves at turns (default 0.5)."),
@@ -1389,9 +1442,9 @@ var BRISTLES = object("Bristle tips: the tuft. Missing fields take their default
1389
1442
  var TIP = object(
1390
1443
  "The tip: its shape and the dab it lays.",
1391
1444
  {
1392
- shape: oneOf2(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp: textured stamps (see stamps), laid as objects or particles."),
1393
- size: num(1, _chunkFQPUF2W2cjs.MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
1394
- hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1)."),
1445
+ shape: oneOf2(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp: dabs shaped like a stamp, a sticker or an image source (see stamps), also laid as sprays, objects or particles."),
1446
+ size: num(1, _chunkNIBFKXMDcjs.MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
1447
+ hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1). Image-source stamps fade towards their edges below 1."),
1395
1448
  spacing: num(0.01, 10, "Dab spacing as a fraction of the diameter: 0.05 smooth, 1 dotted (default 0.1)."),
1396
1449
  angle: num(-360, 360, "Tip angle in degrees, counter-clockwise; added to the stroke direction or barrel with dynamics.rotation (default 0)."),
1397
1450
  roundness: num(0.01, 1, "Roundness: 1 round (default), lower a flat ellipse (pastel, chisel)."),
@@ -1407,8 +1460,9 @@ var TIP = object(
1407
1460
  minItems: 1,
1408
1461
  maxItems: MAX_STICKER_STAMPS,
1409
1462
  items: STAMP,
1410
- description: `Stamp tips: the stickers. Objects take any sticker and pick one at random per mark (default editor:star; repeats count once); particles take only the editor's stamps, use the first ${MAX_DOT_TEXTURES} in marks.stampOrder, and repeats weight the order.`
1411
- }
1463
+ description: `Stamp tips: stickers or image sources (source:<id>). Continuous dabs and sprays lay the first; objects take only stickers and pick one at random per mark (default editor:star; repeats count once); particles use the first ${MAX_DOT_TEXTURES} in marks.stampOrder, and repeats weight the order.`
1464
+ },
1465
+ grain: TIP_GRAIN
1412
1466
  },
1413
1467
  ["shape"]
1414
1468
  );
@@ -1478,7 +1532,9 @@ var DYNAMICS = object("Pen feel: pressure, tilt, velocity, jitter, rotation and
1478
1532
  sizeJitter: unit3("Random size change per dab (default 0)."),
1479
1533
  angleJitter: unit3("Random angle per dab, as a fraction of \xB1180\xB0 (default 0)."),
1480
1534
  opacityJitter: unit3("Random opacity drop per dab (default 0)."),
1481
- scatter: num(0, 4, "Random offset across the stroke, in diameters (default 0).")
1535
+ scatter: num(0, 4, "Random offset across the stroke, in diameters (default 0)."),
1536
+ count: int(1, 16, "Dabs laid at each spacing step, each with its own jitter and scatter (default 1). Dab brushes."),
1537
+ flip: oneOf2(BRUSH_FLIPS, "Mirror each dab at random across x, y or both (default none): variety for image and stamp tips.")
1482
1538
  });
1483
1539
  var EXCHANGE2 = object("Paint exchange with the canvas: picking up paint and mixing it into what the brush lays. Paint brushes on a dry, wet or thick surface and Mixer Brushes.", {
1484
1540
  pickup: unit3("Pick-up ratio: how much of the paint under each dab the brush lifts and carries (default 0; with a reservoir 0.5)."),
@@ -1564,7 +1620,7 @@ var EFFECT = object(
1564
1620
  );
1565
1621
  var PAINT = {
1566
1622
  description: "A colour a brush can carry: a solid colour, a light / dark pair, a gradient or effect paint.",
1567
- anyOf: [SOLID, DUAL, _chunkFQPUF2W2cjs.PAINT_GRADIENT_JSON_SCHEMA, EFFECT]
1623
+ anyOf: [SOLID, DUAL, _chunkNIBFKXMDcjs.PAINT_GRADIENT_JSON_SCHEMA, EFFECT]
1568
1624
  };
1569
1625
  var COLOUR = object("The brush's colours. The user's colour still wins unless own is set.", {
1570
1626
  default: {
@@ -1600,6 +1656,7 @@ var BRUSH_JSON_SCHEMA = object(
1600
1656
  );
1601
1657
  var BRUSH_FACET_SCHEMAS = {
1602
1658
  tip: TIP,
1659
+ tipGrain: TIP_GRAIN,
1603
1660
  marks: { continuous: CONTINUOUS, scatter: SCATTER, particles: PARTICLES, objects: OBJECTS },
1604
1661
  dynamics: DYNAMICS,
1605
1662
  behaviour: BEHAVIOUR,
@@ -1609,6 +1666,139 @@ var BRUSH_FACET_SCHEMAS = {
1609
1666
  paint: PAINT
1610
1667
  };
1611
1668
 
1669
+ // src/brushes/sources.ts
1670
+ var BRUSH_SOURCE_PROVIDER = "source";
1671
+ var MAX_BRUSH_SOURCE_SIZE = 512;
1672
+ var MAX_BRUSH_SOURCE_INPUT_PIXELS = 4096 * 4096;
1673
+ var MAX_BRUSH_SOURCES = 500;
1674
+ var BRUSH_SOURCE_CHANNELS = ["auto", "alpha", "darkness", "lightness"];
1675
+ var BrushSourceError = class extends Error {
1676
+ constructor(message) {
1677
+ super(message);
1678
+ this.name = "BrushSourceError";
1679
+ }
1680
+ };
1681
+ function brushSourceRef(id) {
1682
+ return `${BRUSH_SOURCE_PROVIDER}:${id}`;
1683
+ }
1684
+ function isBrushSourceRef(ref) {
1685
+ return typeof ref === "string" && ref.startsWith(`${BRUSH_SOURCE_PROVIDER}:`);
1686
+ }
1687
+ function brushSourceIdOf(ref) {
1688
+ return isBrushSourceRef(ref) ? ref.slice(BRUSH_SOURCE_PROVIDER.length + 1) : null;
1689
+ }
1690
+ function brushSourceIds(brush) {
1691
+ const refs = [..._nullishCoalesce(brush.tip.stamps, () => ( [])), ...brush.tip.grain ? [brush.tip.grain.source] : []];
1692
+ return [...new Set(refs.map(brushSourceIdOf).filter((id) => id !== null))];
1693
+ }
1694
+ function summarizeBrushSource(s) {
1695
+ const { image: _i, ...rest } = s;
1696
+ return { ...rest, ref: brushSourceRef(s.id) };
1697
+ }
1698
+ function brushSourceIdFromName(name) {
1699
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "source";
1700
+ }
1701
+ var text = (v, max) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : void 0;
1702
+ function fit(width, height, mask, max) {
1703
+ const scale = Math.min(1, max / Math.max(width, height));
1704
+ if (scale >= 1) return { width, height, mask: Uint8Array.from(mask) };
1705
+ const w = Math.max(1, Math.round(width * scale));
1706
+ const h = Math.max(1, Math.round(height * scale));
1707
+ const out = new Uint8Array(w * h);
1708
+ for (let y = 0; y < h; y++) {
1709
+ const y0 = Math.floor(y * height / h);
1710
+ const y1 = Math.max(y0 + 1, Math.floor((y + 1) * height / h));
1711
+ for (let x = 0; x < w; x++) {
1712
+ const x0 = Math.floor(x * width / w);
1713
+ const x1 = Math.max(x0 + 1, Math.floor((x + 1) * width / w));
1714
+ let sum = 0;
1715
+ for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) sum += mask[yy * width + xx];
1716
+ out[y * w + x] = Math.round(sum / ((y1 - y0) * (x1 - x0)));
1717
+ }
1718
+ }
1719
+ return { width: w, height: h, mask: out };
1720
+ }
1721
+ function brushSourceFromMask(meta, width, height, mask) {
1722
+ if (!BRUSH_ID.test(meta.id)) throw new BrushSourceError(`Source id ${JSON.stringify(meta.id)} is not valid: use lowercase letters, digits and dashes (up to 64).`);
1723
+ if (!(Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0) || mask.length !== width * height) throw new BrushSourceError("The mask is not width \xD7 height bytes.");
1724
+ let any = false;
1725
+ for (let i = 0; i < mask.length && !any; i++) any = mask[i] > 0;
1726
+ if (!any) throw new BrushSourceError(`"${meta.name}" has no marks: every pixel is empty. Pick another channel (alpha, darkness or lightness) or set invert.`);
1727
+ const f2 = fit(width, height, mask, MAX_BRUSH_SOURCE_SIZE);
1728
+ const name = _nullishCoalesce(text(meta.name, 80), () => ( meta.id));
1729
+ const licence = text(meta.licence, 2e3);
1730
+ const origin = text(meta.origin, 200);
1731
+ return { id: meta.id, name, width: f2.width, height: f2.height, image: _chunkW5M3J7PVcjs.pngDataUrl.call(void 0, f2.width, f2.height, f2.mask, 1), ...licence ? { licence } : {}, ...origin ? { origin } : {} };
1732
+ }
1733
+ function makeBrushSource(input, problems = []) {
1734
+ const name = text(input.name, 80);
1735
+ if (!name) throw new BrushSourceError("A brush source needs a name.");
1736
+ const id = _nullishCoalesce(input.id, () => ( brushSourceIdFromName(name)));
1737
+ if (!BRUSH_ID.test(id)) throw new BrushSourceError(`Source id ${JSON.stringify(id)} is not valid: use lowercase letters, digits and dashes (up to 64), starting with a letter or digit.`);
1738
+ let bytes;
1739
+ if (typeof input.image === "string") {
1740
+ const parsed = _chunkW5M3J7PVcjs.parseDataUrl.call(void 0, input.image);
1741
+ if (!parsed) throw new BrushSourceError("image must be a data: URL (data:image/png;base64,\u2026) or PNG bytes.");
1742
+ bytes = parsed.bytes;
1743
+ } else bytes = input.image;
1744
+ let png;
1745
+ try {
1746
+ png = _chunkW5M3J7PVcjs.decodePng.call(void 0, bytes, { maxPixels: MAX_BRUSH_SOURCE_INPUT_PIXELS });
1747
+ } catch (e) {
1748
+ throw new BrushSourceError(`The image can't be read as a PNG (${e.message}); convert it to PNG, at most ${MAX_BRUSH_SOURCE_INPUT_PIXELS} pixels.`);
1749
+ }
1750
+ const { width, height, data } = png;
1751
+ const n = width * height;
1752
+ let channel = _nullishCoalesce(input.channel, () => ( "auto"));
1753
+ if (channel === "auto") {
1754
+ let transparent = false;
1755
+ for (let i = 0; i < n && !transparent; i++) transparent = data[i * 4 + 3] < 250;
1756
+ channel = transparent ? "alpha" : "darkness";
1757
+ problems.push(`channel auto read the marks from ${channel === "alpha" ? "the transparency" : "the dark parts (dark marks on a light background)"}.`);
1758
+ }
1759
+ const mask = new Uint8Array(n);
1760
+ for (let i = 0; i < n; i++) {
1761
+ const a = data[i * 4 + 3];
1762
+ const lum = (0.2126 * data[i * 4] + 0.7152 * data[i * 4 + 1] + 0.0722 * data[i * 4 + 2]) * (a / 255) + 255 * (1 - a / 255);
1763
+ const v = channel === "alpha" ? a : channel === "darkness" ? 255 - lum : lum;
1764
+ mask[i] = Math.round(input.invert ? 255 - v : v);
1765
+ }
1766
+ const source = brushSourceFromMask({ id, name, licence: input.licence, origin: input.origin }, width, height, mask);
1767
+ if (source.width !== width) problems.push(`The image was scaled from ${width}\xD7${height} to ${source.width}\xD7${source.height} (sources are kept at most ${MAX_BRUSH_SOURCE_SIZE} px).`);
1768
+ return source;
1769
+ }
1770
+ var GREY_PNG = /^data:image\/png;base64,[A-Za-z0-9+/]+=*$/;
1771
+ function readBrushSource(raw, at, problems) {
1772
+ const r = raw;
1773
+ if (!r || typeof r !== "object" || Array.isArray(r)) return void problems.push(`${at} is not a brush source; skipped.`), null;
1774
+ if (typeof r.id !== "string" || !BRUSH_ID.test(r.id)) return void problems.push(`${at} has no valid id; skipped.`), null;
1775
+ const ok = (v) => Number.isInteger(v) && v >= 1 && v <= MAX_BRUSH_SOURCE_SIZE;
1776
+ if (!ok(r.width) || !ok(r.height)) return void problems.push(`${at} ("${r.id}") has no valid width and height (1..${MAX_BRUSH_SOURCE_SIZE}); skipped.`), null;
1777
+ if (typeof r.image !== "string" || !GREY_PNG.test(r.image)) return void problems.push(`${at} ("${r.id}") has no PNG data: URL image; skipped.`), null;
1778
+ const name = _nullishCoalesce(text(r.name, 80), () => ( r.id));
1779
+ const licence = text(r.licence, 2e3);
1780
+ const origin = text(r.origin, 200);
1781
+ return { id: r.id, name, width: r.width, height: r.height, image: r.image, ...licence ? { licence } : {}, ...origin ? { origin } : {} };
1782
+ }
1783
+ var decoded = /* @__PURE__ */ new Map();
1784
+ function decodeBrushSource(source) {
1785
+ const hit = decoded.get(source.image);
1786
+ if (hit) return hit;
1787
+ const parsed = _chunkW5M3J7PVcjs.parseDataUrl.call(void 0, source.image);
1788
+ if (!parsed) throw new BrushSourceError("The source image is not a data: URL.");
1789
+ const png = _chunkW5M3J7PVcjs.decodePng.call(void 0, parsed.bytes, { keepGray: true, maxPixels: MAX_BRUSH_SOURCE_SIZE * MAX_BRUSH_SOURCE_SIZE });
1790
+ let mask;
1791
+ if (png.channels === 1) mask = png.data;
1792
+ else {
1793
+ mask = new Uint8ClampedArray(png.width * png.height);
1794
+ for (let i = 0; i < mask.length; i++) mask[i] = Math.round(png.data[i * 4] * png.data[i * 4 + 3] / 255);
1795
+ }
1796
+ const out = { width: png.width, height: png.height, mask };
1797
+ if (decoded.size > 256) decoded.delete(decoded.keys().next().value);
1798
+ decoded.set(source.image, out);
1799
+ return out;
1800
+ }
1801
+
1612
1802
  // src/brushes/normalize.ts
1613
1803
  var WET_BRUSH_TOOLS = ["water", "dryer", "blow", "blot", "wet-blend", "wet-drag"];
1614
1804
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -1676,7 +1866,7 @@ function readObject(value, schema, path, problems) {
1676
1866
  const out = {};
1677
1867
  for (const [key, v] of Object.entries(value)) {
1678
1868
  if (v === void 0) continue;
1679
- const child = _optionalChain([schema, 'access', _24 => _24.properties, 'optionalAccess', _25 => _25[key]]);
1869
+ const child = _optionalChain([schema, 'access', _25 => _25.properties, 'optionalAccess', _26 => _26[key]]);
1680
1870
  if (!child) {
1681
1871
  problems.push(`${path}.${key} is not a brush field; dropped.`);
1682
1872
  continue;
@@ -1690,7 +1880,7 @@ function readPaint(value, path, problems) {
1690
1880
  if (typeof value === "string") {
1691
1881
  if (SOLID2.test(value.trim())) return value.trim().toLowerCase();
1692
1882
  } else if (isObject(value) && value.type === "gradient") {
1693
- const g = _chunkFQPUF2W2cjs.readPaintSource.call(void 0, value);
1883
+ const g = _chunkNIBFKXMDcjs.readPaintSource.call(void 0, value);
1694
1884
  if (g && g.type === "gradient") return g;
1695
1885
  } else if (isObject(value) && value.type === "effect") {
1696
1886
  if (!_chunkWOFXZ3XUcjs.validateJsonSchema.call(void 0, value, BRUSH_FACET_SCHEMAS.paint.anyOf[3]).length) return clone(value);
@@ -1766,15 +1956,15 @@ function normalizeBrushSpec(raw, problems = []) {
1766
1956
  delete brush.marks;
1767
1957
  }
1768
1958
  }
1769
- if (_optionalChain([brush, 'access', _26 => _26.marks, 'optionalAccess', _27 => _27.kind]) === "scatter") {
1770
- const surface = _optionalChain([brush, 'access', _28 => _28.behaviour, 'optionalAccess', _29 => _29.surface]);
1771
- const why = brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : surface === "wet" || surface === "thick" ? `${surface} paint is not sprayed` : _optionalChain([brush, 'access', _30 => _30.behaviour, 'optionalAccess', _31 => _31.exchange, 'optionalAccess', _32 => _32.reservoir]) ? "the Mixer Brush is not sprayed" : null;
1959
+ if (_optionalChain([brush, 'access', _27 => _27.marks, 'optionalAccess', _28 => _28.kind]) === "scatter") {
1960
+ const surface = _optionalChain([brush, 'access', _29 => _29.behaviour, 'optionalAccess', _30 => _30.surface]);
1961
+ const why = brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : surface === "wet" || surface === "thick" ? `${surface} paint is not sprayed` : _optionalChain([brush, 'access', _31 => _31.behaviour, 'optionalAccess', _32 => _32.exchange, 'optionalAccess', _33 => _33.reservoir]) ? "the Mixer Brush is not sprayed" : null;
1772
1962
  if (why) {
1773
1963
  drop("marks", why);
1774
1964
  delete brush.marks;
1775
1965
  }
1776
1966
  }
1777
- if (_optionalChain([brush, 'access', _33 => _33.marks, 'optionalAccess', _34 => _34.kind]) === "objects" && brush.tip.shape !== "stamp") {
1967
+ if (_optionalChain([brush, 'access', _34 => _34.marks, 'optionalAccess', _35 => _35.kind]) === "objects" && brush.tip.shape !== "stamp") {
1778
1968
  problems.push("marks objects lay stamps: tip.shape set to stamp.");
1779
1969
  brush.tip.shape = "stamp";
1780
1970
  }
@@ -1787,13 +1977,26 @@ function normalizeBrushSpec(raw, problems = []) {
1787
1977
  delete brush.tip.stamps;
1788
1978
  }
1789
1979
  if (brush.tip.stamps) {
1790
- const stamps = brush.tip.stamps.map((s) => _chunkRDFHWAHLcjs.normalizeStickerRef.call(void 0, s));
1980
+ let stamps = brush.tip.stamps.map((s) => _chunkRDFHWAHLcjs.normalizeStickerRef.call(void 0, s));
1981
+ if (_optionalChain([brush, 'access', _36 => _36.marks, 'optionalAccess', _37 => _37.kind]) === "objects" && stamps.some(isBrushSourceRef)) {
1982
+ problems.push("tip.stamps: image sources (source:\u2026) dropped: objects marks lay stickers; use continuous, scatter or particles marks for an image tip.");
1983
+ stamps = stamps.filter((s) => !isBrushSourceRef(s));
1984
+ }
1791
1985
  if (stamps.length) brush.tip.stamps = stamps;
1792
1986
  else {
1793
1987
  drop("tip.stamps", "none of the stamps can be laid");
1794
1988
  delete brush.tip.stamps;
1795
1989
  }
1796
1990
  }
1991
+ if (brush.tip.grain) {
1992
+ const surface = _optionalChain([brush, 'access', _38 => _38.behaviour, 'optionalAccess', _39 => _39.surface]);
1993
+ const marks = _optionalChain([brush, 'access', _40 => _40.marks, 'optionalAccess', _41 => _41.kind]);
1994
+ const why = !brush.tip.grain.source ? "it needs a source (a grain pattern or source:<id>)" : brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : marks === "particles" || marks === "objects" ? `${marks} marks are not dabs` : surface === "wet" || surface === "thick" ? `${surface} paint lays its own texture` : kind === "tool" && WET_BRUSH_TOOLS.includes(brush.tool) ? "the wet tools work on the wet layer" : null;
1995
+ if (why) {
1996
+ drop("tip.grain", why);
1997
+ delete brush.tip.grain;
1998
+ }
1999
+ }
1797
2000
  const b = brush.behaviour;
1798
2001
  if (b) {
1799
2002
  const wetTool = kind === "tool" && WET_BRUSH_TOOLS.includes(brush.tool);
@@ -1824,7 +2027,7 @@ function normalizeBrushSpec(raw, problems = []) {
1824
2027
  if (b.exchange && kind === "tool" && !wetTool) {
1825
2028
  drop("behaviour.exchange", `the ${brush.tool} does not pick paint up`);
1826
2029
  delete b.exchange;
1827
- } else if (b.exchange && (_optionalChain([brush, 'access', _35 => _35.marks, 'optionalAccess', _36 => _36.kind]) === "particles" || _optionalChain([brush, 'access', _37 => _37.marks, 'optionalAccess', _38 => _38.kind]) === "objects" || brush.tip.shape === "bristle")) {
2030
+ } else if (b.exchange && (_optionalChain([brush, 'access', _42 => _42.marks, 'optionalAccess', _43 => _43.kind]) === "particles" || _optionalChain([brush, 'access', _44 => _44.marks, 'optionalAccess', _45 => _45.kind]) === "objects" || brush.tip.shape === "bristle")) {
1828
2031
  drop("behaviour.exchange", "particle, object and bristle marks do not pick paint up");
1829
2032
  delete b.exchange;
1830
2033
  }
@@ -1850,7 +2053,9 @@ var PAINT_PROFILE = {
1850
2053
  motion: "full",
1851
2054
  colours: ALL_COLOURS,
1852
2055
  velocity: "full",
1853
- tilt: "full"
2056
+ tilt: "full",
2057
+ tipGrain: "full",
2058
+ dabCount: "full"
1854
2059
  };
1855
2060
  var IMAGE_PROFILE = {
1856
2061
  id: "image",
@@ -1867,7 +2072,9 @@ var IMAGE_PROFILE = {
1867
2072
  motion: "none",
1868
2073
  colours: ["solid", "dual", "gradient"],
1869
2074
  velocity: "ignored",
1870
- tilt: "ignored"
2075
+ tilt: "ignored",
2076
+ tipGrain: "ignored",
2077
+ dabCount: "ignored"
1871
2078
  };
1872
2079
  var CANVAS_PROFILE = {
1873
2080
  id: "canvas",
@@ -1884,7 +2091,9 @@ var CANVAS_PROFILE = {
1884
2091
  motion: "none",
1885
2092
  colours: ["solid", "dual", "gradient"],
1886
2093
  velocity: "ignored",
1887
- tilt: "full"
2094
+ tilt: "full",
2095
+ tipGrain: "ignored",
2096
+ dabCount: "ignored"
1888
2097
  };
1889
2098
  var VECTOR_PROFILE = { ...CANVAS_PROFILE, id: "vector", name: "Vector editor", kinds: ["mark", "tool"], tools: ["blender", "smudge"], surfaces: ["dry", "wet"], reservoir: "full", light: "ignored" };
1890
2099
  var BRUSH_HOST_PROFILES = { paint: PAINT_PROFILE, image: IMAGE_PROFILE, canvas: CANVAS_PROFILE, vector: VECTOR_PROFILE };
@@ -1908,32 +2117,37 @@ function checkBrush(brush, profile) {
1908
2117
  else if (brush.kind === "tool" && brush.tool && !profile.tools.includes(brush.tool)) {
1909
2118
  add("tool", "error", `${host} has no ${brush.tool} tool.`, profile.tools.length ? `Use one of: ${list(profile.tools)}.` : `${host} has no tools; use a mark brush.`);
1910
2119
  }
1911
- if (!profile.tipShapes.includes(brush.tip.shape)) add("tip.shape", "error", `${host} can't lay a ${brush.tip.shape} tip.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
1912
- const marks = _nullishCoalesce(_optionalChain([brush, 'access', _39 => _39.marks, 'optionalAccess', _40 => _40.kind]), () => ( "continuous"));
2120
+ if (!profile.tipShapes.includes(brush.tip.shape)) {
2121
+ const image = brush.tip.shape === "stamp" && (_nullishCoalesce(brush.tip.stamps, () => ( []))).some(isBrushSourceRef);
2122
+ add("tip.shape", "error", `${host} can't lay a ${image ? "stamp tip (image tips are stamps)" : `${brush.tip.shape} tip`}.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
2123
+ }
2124
+ const marks = _nullishCoalesce(_optionalChain([brush, 'access', _46 => _46.marks, 'optionalAccess', _47 => _47.kind]), () => ( "continuous"));
1913
2125
  if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
1914
- const surface = _nullishCoalesce(_optionalChain([brush, 'access', _41 => _41.behaviour, 'optionalAccess', _42 => _42.surface]), () => ( "dry"));
2126
+ const surface = _nullishCoalesce(_optionalChain([brush, 'access', _48 => _48.behaviour, 'optionalAccess', _49 => _49.surface]), () => ( "dry"));
1915
2127
  if (brush.kind === "mark" && !profile.surfaces.includes(surface)) add("behaviour.surface", "error", `${host} has no ${surface} surface.`, `Set behaviour.surface to ${list(profile.surfaces)}.`);
1916
- const ex = _optionalChain([brush, 'access', _43 => _43.behaviour, 'optionalAccess', _44 => _44.exchange]);
1917
- feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _45 => _45.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
1918
- feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _46 => _46.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _47 => _47.pickup])) || !!_optionalChain([ex, 'optionalAccess', _48 => _48.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
1919
- feature(profile.grain, nonZero(_optionalChain([brush, 'access', _49 => _49.behaviour, 'optionalAccess', _50 => _50.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
1920
- feature(profile.light, nonZero(_optionalChain([brush, 'access', _51 => _51.light, 'optionalAccess', _52 => _52.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
2128
+ const ex = _optionalChain([brush, 'access', _50 => _50.behaviour, 'optionalAccess', _51 => _51.exchange]);
2129
+ feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _52 => _52.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
2130
+ feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _53 => _53.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _54 => _54.pickup])) || !!_optionalChain([ex, 'optionalAccess', _55 => _55.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
2131
+ feature(profile.grain, nonZero(_optionalChain([brush, 'access', _56 => _56.behaviour, 'optionalAccess', _57 => _57.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
2132
+ feature(profile.light, nonZero(_optionalChain([brush, 'access', _58 => _58.light, 'optionalAccess', _59 => _59.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
1921
2133
  const m = brush.motion;
1922
2134
  feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
1923
2135
  const d = _nullishCoalesce(brush.dynamics, () => ( {}));
2136
+ feature(_nullishCoalesce(profile.tipGrain, () => ( "none")), !!brush.tip.grain, "tip.grain", "tip grain", "Remove tip.grain (behaviour.grain textures with the paper instead).");
2137
+ feature(_nullishCoalesce(profile.dabCount, () => ( "none")), (_nullishCoalesce(d.count, () => ( 1))) > 1, "dynamics.count", "several dabs per step", "Set dynamics.count to 1.");
1924
2138
  feature(profile.velocity, nonZero(d.velocitySize) || nonZero(d.velocityOpacity), "dynamics.velocity", "velocity dynamics (its strokes carry no timing)", "Set dynamics.velocitySize and velocityOpacity to 0.");
1925
2139
  feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
1926
2140
  const colour = brush.colour;
1927
- if (_optionalChain([colour, 'optionalAccess', _53 => _53.default]) !== void 0) {
2141
+ if (_optionalChain([colour, 'optionalAccess', _60 => _60.default]) !== void 0) {
1928
2142
  const kind = brushColourKind(colour.default);
1929
2143
  if (!profile.colours.includes(kind)) add("colour.default", "error", `${host} can't paint ${kind} colours.`, `Use a ${list(profile.colours)} colour, or remove colour.default.`);
1930
2144
  }
1931
- const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _54 => _54.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
2145
+ const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _61 => _61.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
1932
2146
  if (foreign.length) add("colour.palette", "warning", `${host} skips the palette's ${list(foreign)} colours.`, `Nothing to do; or keep to ${list(profile.colours)} colours.`);
1933
2147
  return { ok: !issues.some((i) => i.severity === "error"), issues };
1934
2148
  }
1935
2149
  function withKeptIssues(check, before) {
1936
- const had = new Set((_nullishCoalesce(_optionalChain([before, 'optionalAccess', _55 => _55.issues]), () => ( []))).filter((i) => i.severity === "error").map((i) => i.facet));
2150
+ const had = new Set((_nullishCoalesce(_optionalChain([before, 'optionalAccess', _62 => _62.issues]), () => ( []))).filter((i) => i.severity === "error").map((i) => i.facet));
1937
2151
  const kept = check.issues.filter((i) => i.severity === "error" && had.has(i.facet));
1938
2152
  const issues = check.issues.filter((i) => !kept.includes(i));
1939
2153
  return { ok: !issues.some((i) => i.severity === "error"), issues, kept };
@@ -1976,6 +2190,12 @@ function degrade(brush, profile, options = {}) {
1976
2190
  case "behaviour.grain":
1977
2191
  delete b.behaviour.grain;
1978
2192
  break;
2193
+ case "tip.grain":
2194
+ delete b.tip.grain;
2195
+ break;
2196
+ case "dynamics.count":
2197
+ delete b.dynamics.count;
2198
+ break;
1979
2199
  case "light":
1980
2200
  delete b.light;
1981
2201
  break;
@@ -2003,7 +2223,7 @@ function degrade(brush, profile, options = {}) {
2003
2223
  }
2004
2224
  dropped.push(issue);
2005
2225
  }
2006
- if (_optionalChain([b, 'access', _56 => _56.behaviour, 'optionalAccess', _57 => _57.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
2226
+ if (_optionalChain([b, 'access', _63 => _63.behaviour, 'optionalAccess', _64 => _64.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
2007
2227
  if (b.colour && !Object.keys(b.colour).length) delete b.colour;
2008
2228
  const after = checkBrush(b, profile).issues.filter((i) => i.severity === "error");
2009
2229
  return { brush: b, dropped, issues: after };
@@ -2024,6 +2244,15 @@ function isBrushLibrary(library) {
2024
2244
  }
2025
2245
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2026
2246
  var isV1 = (item) => "engine" in item || "category" in item || !("kind" in item);
2247
+ function parseBrushLibrarySources(raw, problems = []) {
2248
+ const file = isObject2(raw) ? raw : null;
2249
+ if (!file || file.sources === void 0) return [];
2250
+ if (!Array.isArray(file.sources)) {
2251
+ problems.push("sources is not a list; skipped.");
2252
+ return [];
2253
+ }
2254
+ return file.sources.map((s, i) => readBrushSource(s, `sources[${i}]`, problems)).filter((s) => s !== null);
2255
+ }
2027
2256
  function parseBrushLibrary2(raw, problems = []) {
2028
2257
  if (typeof raw === "string") {
2029
2258
  try {
@@ -2038,7 +2267,7 @@ function parseBrushLibrary2(raw, problems = []) {
2038
2267
  if (file && file.version !== void 0 && file.version !== 1 && file.version !== 2) {
2039
2268
  throw new BrushLibraryError("invalid", `Brush library version ${JSON.stringify(file.version)} is not supported; this library reads versions 1 and 2.`);
2040
2269
  }
2041
- const v1File = _optionalChain([file, 'optionalAccess', _58 => _58.version]) === 1;
2270
+ const v1File = _optionalChain([file, 'optionalAccess', _65 => _65.version]) === 1;
2042
2271
  const out = [];
2043
2272
  items.forEach((item, i) => {
2044
2273
  const at = `brushes[${i}]`;
@@ -2066,8 +2295,8 @@ function matches(entry, f2) {
2066
2295
  if (f2.tool && b.tool !== f2.tool) return false;
2067
2296
  if (f2.tag && !(_nullishCoalesce(b.tags, () => ( []))).includes(f2.tag)) return false;
2068
2297
  if (f2.text) {
2069
- const text = words([b.id, b.name, _nullishCoalesce(b.description, () => ( "")), _nullishCoalesce(b.variety, () => ( "")), _nullishCoalesce(b.medium, () => ( "")), _nullishCoalesce(b.tool, () => ( "")), ..._nullishCoalesce(b.tags, () => ( []))].join(" "));
2070
- if (!words(f2.text).every((w) => text.some((t) => t.startsWith(w)))) return false;
2298
+ const text2 = words([b.id, b.name, _nullishCoalesce(b.description, () => ( "")), _nullishCoalesce(b.variety, () => ( "")), _nullishCoalesce(b.medium, () => ( "")), _nullishCoalesce(b.tool, () => ( "")), ..._nullishCoalesce(b.tags, () => ( []))].join(" "));
2299
+ if (!words(f2.text).every((w) => text2.some((t) => t.startsWith(w)))) return false;
2071
2300
  }
2072
2301
  if (f2.profile && !checkBrush(b, f2.profile).ok) return false;
2073
2302
  return true;
@@ -2077,8 +2306,10 @@ function createBrushLibrary(options = {}) {
2077
2306
  const presets = (_nullishCoalesce(options.presets, () => ( BRUSH_PRESETS))).map(copy3);
2078
2307
  const builtinIds = new Set(presets.map((b) => b.id));
2079
2308
  const key = `${_nullishCoalesce(options.namespace, () => ( "pixerate.brushes"))}:user`;
2309
+ const sourcesKey = `${_nullishCoalesce(options.namespace, () => ( "pixerate.brushes"))}:sources`;
2080
2310
  const listeners = /* @__PURE__ */ new Set();
2081
2311
  let user = null;
2312
+ let sources = null;
2082
2313
  let queue = Promise.resolve();
2083
2314
  const run = (task) => {
2084
2315
  const result = queue.then(task, task);
@@ -2104,6 +2335,44 @@ function createBrushLibrary(options = {}) {
2104
2335
  if (options.storage) await options.storage.set(key, JSON.stringify(file));
2105
2336
  user = brushes;
2106
2337
  };
2338
+ const loadSources = async () => {
2339
+ if (sources) return sources;
2340
+ const raw = options.storage ? await options.storage.get(sourcesKey) : null;
2341
+ let list2 = [];
2342
+ if (raw) {
2343
+ try {
2344
+ list2 = parseBrushLibrarySources(JSON.parse(raw));
2345
+ } catch (e6) {
2346
+ }
2347
+ }
2348
+ sources = list2;
2349
+ return sources;
2350
+ };
2351
+ const storeSources = async (list2) => {
2352
+ if (options.storage) await options.storage.set(sourcesKey, JSON.stringify({ format: "pixerate-brush-sources", version: 1, sources: list2 }));
2353
+ sources = list2;
2354
+ };
2355
+ const putSources = async (incoming, overwrite, problems) => {
2356
+ let list2 = [...await loadSources()];
2357
+ const changed = [];
2358
+ for (const src of incoming) {
2359
+ const at = list2.findIndex((x) => x.id === src.id);
2360
+ if (at === -1) {
2361
+ if (list2.length >= MAX_BRUSH_SOURCES) {
2362
+ problems.push(`Source "${src.id}" skipped: the library holds at most ${MAX_BRUSH_SOURCES} sources.`);
2363
+ continue;
2364
+ }
2365
+ list2.push(src);
2366
+ changed.push(src.id);
2367
+ } else if (list2[at].image === src.image) continue;
2368
+ else if (overwrite) {
2369
+ list2 = list2.map((x, i) => i === at ? src : x);
2370
+ changed.push(src.id);
2371
+ } else problems.push(`Source "${src.id}" exists already with another image; skipped (pass overwrite to replace it).`);
2372
+ }
2373
+ if (changed.length) await storeSources(list2);
2374
+ return changed;
2375
+ };
2107
2376
  const entries = async () => [...presets.map((brush) => ({ brush, builtin: true })), ...(await load()).map((brush) => ({ brush, builtin: false }))];
2108
2377
  const find = async (id) => {
2109
2378
  const entry = (await entries()).find((e) => e.brush.id === id);
@@ -2165,12 +2434,17 @@ function createBrushLibrary(options = {}) {
2165
2434
  }),
2166
2435
  exportFile: (ids) => run(async () => {
2167
2436
  const brushes = ids ? await Promise.all(ids.map(async (id) => (await find(id)).brush)) : await load();
2168
- return { format: "pixerate-brushes", version: 2, brushes: brushes.map(copy3) };
2437
+ const used = new Set(brushes.flatMap(brushSourceIds));
2438
+ const withSources = (await loadSources()).filter((s) => used.has(s.id));
2439
+ return { format: "pixerate-brushes", version: 2, brushes: brushes.map(copy3), ...withSources.length ? { sources: withSources.map(copy3) } : {} };
2169
2440
  }),
2170
2441
  importFile: (raw, o = {}) => run(async () => {
2171
2442
  const problems = [];
2172
2443
  const incoming = parseBrushLibrary2(raw, problems);
2173
2444
  const result = { added: [], replaced: [], skipped: [], problems };
2445
+ const fileSources = parseBrushLibrarySources(typeof raw === "string" ? JSON.parse(raw) : raw, problems);
2446
+ const changedSources = fileSources.length ? await putSources(fileSources, !!o.overwrite, problems) : [];
2447
+ if (changedSources.length) result.sources = changedSources;
2174
2448
  let brushes = [...await load()];
2175
2449
  for (const brush of incoming) {
2176
2450
  if (builtinIds.has(brush.id)) {
@@ -2190,12 +2464,45 @@ function createBrushLibrary(options = {}) {
2190
2464
  problems.push(`"${brush.id}" exists already; skipped (pass overwrite to replace it).`);
2191
2465
  }
2192
2466
  }
2193
- if (result.added.length || result.replaced.length) {
2194
- await store(brushes);
2195
- notify();
2196
- }
2467
+ if (result.added.length || result.replaced.length) await store(brushes);
2468
+ if (result.added.length || result.replaced.length || changedSources.length) notify();
2197
2469
  return result;
2198
2470
  }),
2471
+ listSources: () => run(async () => (await loadSources()).map(copy3)),
2472
+ getSource: (id) => run(async () => {
2473
+ const s = (await loadSources()).find((x) => x.id === id);
2474
+ return s ? copy3(s) : null;
2475
+ }),
2476
+ addSource: (input, o = {}) => run(async () => {
2477
+ const problems = [];
2478
+ let source;
2479
+ if (isObject2(input) && "width" in input) {
2480
+ const read2 = readBrushSource(input, "source", problems);
2481
+ if (!read2) throw new BrushLibraryError("invalid", problems.join(" "));
2482
+ source = read2;
2483
+ } else {
2484
+ const given = input;
2485
+ const taken = new Set((await loadSources()).map((x) => x.id));
2486
+ let id = given.id;
2487
+ if (id === void 0 && typeof given.name === "string") {
2488
+ const stem = brushSourceIdFromName(given.name);
2489
+ id = stem;
2490
+ for (let n = 2; taken.has(id); n++) id = `${stem}-${n}`;
2491
+ }
2492
+ try {
2493
+ source = makeBrushSource({ ...given, id }, problems);
2494
+ } catch (e) {
2495
+ throw new BrushLibraryError("invalid", e.message);
2496
+ }
2497
+ }
2498
+ const list2 = await loadSources();
2499
+ const at = list2.findIndex((x) => x.id === source.id);
2500
+ if (at !== -1 && !o.overwrite) throw new BrushLibraryError("exists", `A source "${source.id}" exists already; pick another id, or pass overwrite to replace its image (brushes using it change too).`);
2501
+ if (at === -1 && list2.length >= MAX_BRUSH_SOURCES) throw new BrushLibraryError("invalid", `The library holds at most ${MAX_BRUSH_SOURCES} sources.`);
2502
+ await storeSources(at === -1 ? [...list2, source] : list2.map((x, i) => i === at ? source : x));
2503
+ notify();
2504
+ return { source: copy3(source), problems, replaced: at !== -1 };
2505
+ }),
2199
2506
  subscribe(listener) {
2200
2507
  listeners.add(listener);
2201
2508
  return () => {
@@ -2221,7 +2528,7 @@ function snapshotBrushLibrary(library, onChange) {
2221
2528
  (list2) => {
2222
2529
  entries = list2;
2223
2530
  user = list2.filter((e) => !e.builtin).map((e) => e.brush);
2224
- _optionalChain([onChange, 'optionalCall', _59 => _59()]);
2531
+ _optionalChain([onChange, 'optionalCall', _66 => _66()]);
2225
2532
  },
2226
2533
  () => void 0
2227
2534
  );
@@ -2328,4 +2635,28 @@ function snapshotBrushLibrary(library, onChange) {
2328
2635
 
2329
2636
 
2330
2637
 
2331
- exports.BRUSH_PRESETS = BRUSH_PRESETS; 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.DEFAULT_SPRAY = DEFAULT_SPRAY; exports.isWetCapable = isWetCapable; exports.isWetBrush = isWetBrush; exports.usesPaintExchange = usesPaintExchange; exports.brushTakesPaint = brushTakesPaint; exports.paintToMultiColor = paintToMultiColor; exports.multiColorToPaint = multiColorToPaint; exports.normalizePaintBrush = normalizePaintBrush; exports.BRUSH_FIELDS = BRUSH_FIELDS; exports.getPath = getPath; exports.setPath = setPath; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium; exports.resolveCategory = resolveCategory; exports.resolveBrush = resolveBrush; exports.BUILTIN_BRUSHES = BUILTIN_BRUSHES; exports.DEFAULT_BRUSH_ID = DEFAULT_BRUSH_ID; exports.brushToJSON = brushToJSON; exports.parseBrushLibrary = parseBrushLibrary; exports.migrateBrushV1 = migrateBrushV1; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.BRUSH_WORD = BRUSH_WORD; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.normalizeBrushSpec = normalizeBrushSpec; exports.PAINT_PROFILE = PAINT_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.withKeptIssues = withKeptIssues; exports.checkBrushEdit = checkBrushEdit; exports.listPresets = listPresets; exports.degrade = degrade; exports.BrushLibraryError = BrushLibraryError; exports.isBrushLibrary = isBrushLibrary; exports.parseBrushLibrary2 = parseBrushLibrary2; exports.createBrushLibrary = createBrushLibrary; exports.closest = closest; exports.snapshotBrushLibrary = snapshotBrushLibrary;
2638
+
2639
+
2640
+
2641
+
2642
+
2643
+
2644
+
2645
+
2646
+
2647
+
2648
+
2649
+
2650
+
2651
+
2652
+
2653
+
2654
+
2655
+
2656
+
2657
+
2658
+
2659
+
2660
+
2661
+
2662
+ exports.BRUSH_PRESETS = BRUSH_PRESETS; 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.DAB_FLIPS = DAB_FLIPS; exports.TIP_GRAIN_MODES = TIP_GRAIN_MODES; exports.DEFAULT_TIP_GRAIN = DEFAULT_TIP_GRAIN; exports.DEFAULT_DYNAMICS = DEFAULT_DYNAMICS; exports.normalizeDynamics = normalizeDynamics; exports.BRUSH_ID = BRUSH_ID; exports.normalizeMultiColor = normalizeMultiColor; exports.normalizeBehavior = normalizeBehavior; exports.DEFAULT_SPRAY = DEFAULT_SPRAY; exports.isWetCapable = isWetCapable; exports.isWetBrush = isWetBrush; exports.usesPaintExchange = usesPaintExchange; exports.brushTakesPaint = brushTakesPaint; exports.paintToMultiColor = paintToMultiColor; exports.multiColorToPaint = multiColorToPaint; exports.normalizePaintBrush = normalizePaintBrush; exports.BRUSH_FIELDS = BRUSH_FIELDS; exports.getPath = getPath; exports.setPath = setPath; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium; exports.resolveCategory = resolveCategory; exports.resolveBrush = resolveBrush; exports.BUILTIN_BRUSHES = BUILTIN_BRUSHES; exports.DEFAULT_BRUSH_ID = DEFAULT_BRUSH_ID; exports.brushToJSON = brushToJSON; exports.parseBrushLibrary = parseBrushLibrary; exports.migrateBrushV1 = migrateBrushV1; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.BRUSH_GRAIN_MODES = BRUSH_GRAIN_MODES; exports.BRUSH_FLIPS = BRUSH_FLIPS; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.BRUSH_WORD = BRUSH_WORD; exports.TIP_GRAIN_PATTERNS = TIP_GRAIN_PATTERNS; exports.BRUSH_GRAIN_SOURCE_PATTERN = BRUSH_GRAIN_SOURCE_PATTERN; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.BRUSH_SOURCE_PROVIDER = BRUSH_SOURCE_PROVIDER; exports.MAX_BRUSH_SOURCE_SIZE = MAX_BRUSH_SOURCE_SIZE; exports.MAX_BRUSH_SOURCE_INPUT_PIXELS = MAX_BRUSH_SOURCE_INPUT_PIXELS; exports.MAX_BRUSH_SOURCES = MAX_BRUSH_SOURCES; exports.BRUSH_SOURCE_CHANNELS = BRUSH_SOURCE_CHANNELS; exports.BrushSourceError = BrushSourceError; exports.brushSourceRef = brushSourceRef; exports.isBrushSourceRef = isBrushSourceRef; exports.brushSourceIdOf = brushSourceIdOf; exports.brushSourceIds = brushSourceIds; exports.summarizeBrushSource = summarizeBrushSource; exports.brushSourceIdFromName = brushSourceIdFromName; exports.brushSourceFromMask = brushSourceFromMask; exports.makeBrushSource = makeBrushSource; exports.readBrushSource = readBrushSource; exports.decodeBrushSource = decodeBrushSource; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.normalizeBrushSpec = normalizeBrushSpec; exports.PAINT_PROFILE = PAINT_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.withKeptIssues = withKeptIssues; exports.checkBrushEdit = checkBrushEdit; exports.listPresets = listPresets; exports.degrade = degrade; exports.BrushLibraryError = BrushLibraryError; exports.isBrushLibrary = isBrushLibrary; exports.parseBrushLibrarySources = parseBrushLibrarySources; exports.parseBrushLibrary2 = parseBrushLibrary2; exports.createBrushLibrary = createBrushLibrary; exports.closest = closest; exports.snapshotBrushLibrary = snapshotBrushLibrary;