@pixerate/editor 0.266.2 → 0.268.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 (149) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.js +1 -1
  3. package/dist/animation-editor/controller.d.cts +8 -0
  4. package/dist/animation-editor/controller.d.ts +8 -0
  5. package/dist/animation-editor/effect-tools.d.cts +1 -1
  6. package/dist/animation-editor/effect-tools.d.ts +1 -1
  7. package/dist/animation-editor/index.cjs +175 -108
  8. package/dist/animation-editor/index.js +94 -27
  9. package/dist/brushes/index.cjs +201 -75
  10. package/dist/brushes/index.d.cts +4 -3
  11. package/dist/brushes/index.d.ts +4 -3
  12. package/dist/brushes/index.js +138 -12
  13. package/dist/brushes/library.d.cts +27 -1
  14. package/dist/brushes/library.d.ts +27 -1
  15. package/dist/brushes/profiles.d.cts +10 -1
  16. package/dist/brushes/profiles.d.ts +10 -1
  17. package/dist/brushes/schema.d.cts +4 -0
  18. package/dist/brushes/schema.d.ts +4 -0
  19. package/dist/brushes/sources.d.cts +114 -0
  20. package/dist/brushes/sources.d.ts +114 -0
  21. package/dist/brushes/types.d.cts +37 -5
  22. package/dist/brushes/types.d.ts +37 -5
  23. package/dist/brushes/ui.d.cts +40 -4
  24. package/dist/brushes/ui.d.ts +40 -4
  25. package/dist/{brushstrokeRender-KD453OBX.js → brushstrokeRender-A727TTNQ.js} +7 -7
  26. package/dist/{brushstrokeRender-FSUWXUZY.cjs → brushstrokeRender-WR2N5HHY.cjs} +23 -23
  27. package/dist/canvas/brushTypes.d.cts +12 -0
  28. package/dist/canvas/brushTypes.d.ts +12 -0
  29. package/dist/canvas/brushstroke.d.cts +1 -1
  30. package/dist/canvas/brushstroke.d.ts +1 -1
  31. package/dist/canvas/drawController.d.cts +19 -4
  32. package/dist/canvas/drawController.d.ts +19 -4
  33. package/dist/canvas/index.cjs +144 -105
  34. package/dist/canvas/index.js +46 -7
  35. package/dist/{chunk-TOID45I2.cjs → chunk-2WOGXEDX.cjs} +1 -1
  36. package/dist/{chunk-XORJ7UZO.cjs → chunk-3VHH6V67.cjs} +13 -12
  37. package/dist/{chunk-3C33YTNN.js → chunk-3XDSNIGF.js} +73 -6
  38. package/dist/chunk-62MFUHSI.cjs +802 -0
  39. package/dist/{chunk-R44Y3EFS.js → chunk-74ODPZLX.js} +17 -345
  40. package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
  41. package/dist/chunk-ADMRNWR7.js +340 -0
  42. package/dist/{chunk-MVUQSK5J.cjs → chunk-B3G7BKUV.cjs} +111 -439
  43. package/dist/{chunk-DXYHQ2VN.js → chunk-BAZMCFVL.js} +1 -1
  44. package/dist/{chunk-VRK322ZC.cjs → chunk-BPJSI4NT.cjs} +2 -2
  45. package/dist/{chunk-RFJZGTHA.cjs → chunk-EU2ZZA6L.cjs} +400 -69
  46. package/dist/{chunk-XC6DPRYI.js → chunk-GSTSZPSR.js} +154 -949
  47. package/dist/{chunk-EZOJBP4D.js → chunk-HFY5LG5J.js} +1 -1
  48. package/dist/chunk-HVO6XCI4.js +802 -0
  49. package/dist/{chunk-YHDHFJUC.cjs → chunk-J5VQYK7T.cjs} +217 -1012
  50. package/dist/{chunk-XRXFVRGY.cjs → chunk-JBB3GDBB.cjs} +2483 -208
  51. package/dist/{chunk-ACFSFUQH.js → chunk-JL6YH5WD.js} +4 -0
  52. package/dist/{chunk-CIJBSUM5.js → chunk-KZI3YWKW.js} +353 -22
  53. package/dist/{chunk-4ODRXQRW.js → chunk-LETF75CA.js} +1 -1
  54. package/dist/{chunk-FQPUF2W2.cjs → chunk-NIBFKXMD.cjs} +217 -35
  55. package/dist/{chunk-AQZQWXEU.cjs → chunk-NPWWSCWT.cjs} +35 -35
  56. package/dist/{chunk-DOZ6F2K2.js → chunk-NSETJI74.js} +3 -2
  57. package/dist/{chunk-PE6YJMQT.cjs → chunk-PFHJS4HE.cjs} +20 -19
  58. package/dist/{chunk-7KNW3WXZ.cjs → chunk-QEVLRLUN.cjs} +63 -0
  59. package/dist/{chunk-LK4SXL3V.js → chunk-QNY7BDFP.js} +1 -1
  60. package/dist/{chunk-OTN6VB5S.cjs → chunk-RDFHWAHL.cjs} +5 -1
  61. package/dist/{chunk-PXDIAYB6.cjs → chunk-STLTOISW.cjs} +34 -214
  62. package/dist/{chunk-T2LDTEXK.js → chunk-UVTO3D4E.js} +2333 -58
  63. package/dist/{chunk-6JL2D5SG.js → chunk-VE6UKYQ2.js} +3 -2
  64. package/dist/{chunk-TXFWZ5ZJ.js → chunk-W6JDUXP3.js} +4 -184
  65. package/dist/{chunk-3SBLVGOM.cjs → chunk-YOVXQHA3.cjs} +23 -23
  66. package/dist/{chunk-WLUVZKVD.cjs → chunk-YXP6X3SY.cjs} +103 -36
  67. package/dist/chunk-YZWM6ZIP.cjs +340 -0
  68. package/dist/{chunk-472INMWH.js → chunk-ZA7L5YJB.js} +63 -0
  69. package/dist/commands/builtins.d.cts +12 -1
  70. package/dist/commands/builtins.d.ts +12 -1
  71. package/dist/commands/index.cjs +2 -2
  72. package/dist/commands/index.js +1 -1
  73. package/dist/image-editor/controller.d.cts +1 -1
  74. package/dist/image-editor/controller.d.ts +1 -1
  75. package/dist/image-editor/index.cjs +868 -864
  76. package/dist/image-editor/index.js +123 -119
  77. package/dist/paint/index.cjs +8 -6
  78. package/dist/paint/index.js +56 -54
  79. package/dist/paint-editor/brush-library-tools.d.cts +12 -2
  80. package/dist/paint-editor/brush-library-tools.d.ts +12 -2
  81. package/dist/paint-editor/brush-model.d.cts +29 -1
  82. package/dist/paint-editor/brush-model.d.ts +29 -1
  83. package/dist/paint-editor/brush-source-art.d.cts +32 -0
  84. package/dist/paint-editor/brush-source-art.d.ts +32 -0
  85. package/dist/paint-editor/controller.d.cts +11 -4
  86. package/dist/paint-editor/controller.d.ts +11 -4
  87. package/dist/paint-editor/dab-stroke.d.cts +2 -0
  88. package/dist/paint-editor/dab-stroke.d.ts +2 -0
  89. package/dist/paint-editor/engine.d.cts +11 -2
  90. package/dist/paint-editor/engine.d.ts +11 -2
  91. package/dist/paint-editor/index.cjs +967 -2335
  92. package/dist/paint-editor/index.d.cts +3 -1
  93. package/dist/paint-editor/index.d.ts +3 -1
  94. package/dist/paint-editor/index.js +394 -1762
  95. package/dist/paint-editor/path-samples.d.cts +2 -0
  96. package/dist/paint-editor/path-samples.d.ts +2 -0
  97. package/dist/paint-editor/presence.d.cts +20 -1
  98. package/dist/paint-editor/presence.d.ts +20 -1
  99. package/dist/paint-editor/stamp-tip.d.cts +8 -5
  100. package/dist/paint-editor/stamp-tip.d.ts +8 -5
  101. package/dist/paint-editor/thumbnails.d.cts +6 -0
  102. package/dist/paint-editor/thumbnails.d.ts +6 -0
  103. package/dist/paint-editor/tip-grain.d.cts +34 -0
  104. package/dist/paint-editor/tip-grain.d.ts +34 -0
  105. package/dist/raster/index.cjs +2 -2
  106. package/dist/raster/index.js +9 -9
  107. package/dist/stickers/helpers.d.cts +2 -0
  108. package/dist/stickers/helpers.d.ts +2 -0
  109. package/dist/stickers/index.cjs +41 -35
  110. package/dist/stickers/index.js +22 -16
  111. package/dist/stickers/types.d.cts +4 -2
  112. package/dist/stickers/types.d.ts +4 -2
  113. package/dist/vector/backdrop.d.cts +13 -0
  114. package/dist/vector/backdrop.d.ts +13 -0
  115. package/dist/vector/brushstroke.d.cts +54 -7
  116. package/dist/vector/brushstroke.d.ts +54 -7
  117. package/dist/vector/brushstrokeMedia.d.cts +96 -0
  118. package/dist/vector/brushstrokeMedia.d.ts +96 -0
  119. package/dist/vector/controller.d.cts +17 -3
  120. package/dist/vector/controller.d.ts +17 -3
  121. package/dist/vector/index.cjs +1850 -1390
  122. package/dist/vector/index.d.cts +2 -0
  123. package/dist/vector/index.d.ts +2 -0
  124. package/dist/vector/index.js +520 -60
  125. package/dist/vector/render.d.cts +6 -0
  126. package/dist/vector/render.d.ts +6 -0
  127. package/dist/vector/types.d.cts +14 -0
  128. package/dist/vector/types.d.ts +14 -0
  129. package/package.json +1 -1
  130. package/skills/pixerate-animation-editor/SKILL.md +1 -0
  131. package/skills/pixerate-brushes/SKILL.md +37 -10
  132. package/skills/pixerate-brushes-design/SKILL.md +15 -5
  133. package/skills/pixerate-brushes-design/evals/port-to-vector.json +8 -8
  134. package/skills/pixerate-canvas/SKILL.md +11 -1
  135. package/skills/pixerate-image-editor/SKILL.md +19 -64
  136. package/skills/pixerate-image-editor/references/color-management.md +2 -1
  137. package/skills/pixerate-image-editor/references/free-transform.md +12 -0
  138. package/skills/pixerate-image-editor/references/layer-effects.md +10 -0
  139. package/skills/pixerate-image-editor/references/painting.md +17 -0
  140. package/skills/pixerate-image-editor/references/photoshop-files.md +1 -1
  141. package/skills/pixerate-image-editor/references/shapes-and-type.md +14 -0
  142. package/skills/pixerate-image-editor/references/smart-objects.md +10 -0
  143. package/skills/pixerate-paint-editor/SKILL.md +14 -4
  144. package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
  145. package/skills/pixerate-paint-editor/references/wet-media.md +9 -0
  146. package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
  147. package/skills/pixerate-vector/SKILL.md +16 -6
  148. package/skills/pixerate-vector/references/best-practices.md +1 -0
  149. package/skills/pixerate-vector/references/commands.md +19 -0
@@ -115,6 +115,9 @@ function stickerCollections(items, names = {}) {
115
115
  }
116
116
  return [...out.values()];
117
117
  }
118
+ function providerCollections(provider) {
119
+ return provider.collections ? provider.collections() : provider.listCollections?.();
120
+ }
118
121
  function parseStickerRef(ref) {
119
122
  if (typeof ref !== "string" || !ref) throw new StickerError("invalid_argument", 'A sticker ref is a "provider:id" string, e.g. "editor:star" (find_stickers lists them).');
120
123
  const i = ref.indexOf(":");
@@ -194,6 +197,7 @@ export {
194
197
  checkStickerVariant,
195
198
  stickerVariantFor,
196
199
  stickerCollections,
200
+ providerCollections,
197
201
  parseStickerRef,
198
202
  normalizeStickerRef,
199
203
  editorStampOf,
@@ -2,7 +2,7 @@ import {
2
2
  STAMP_KINDS,
3
3
  editorStampOf,
4
4
  normalizeStickerRef
5
- } from "./chunk-ACFSFUQH.js";
5
+ } from "./chunk-JL6YH5WD.js";
6
6
  import {
7
7
  validateJsonSchema
8
8
  } from "./chunk-V2ZCJRDM.js";
@@ -11,6 +11,7 @@ import {
11
11
  DEFAULT_BRUSH,
12
12
  DEFAULT_MIXER,
13
13
  DEFAULT_SMUDGE,
14
+ GRAIN_PATTERNS,
14
15
  MAX_BRUSH_SIZE,
15
16
  PAINT_GRADIENT_JSON_SCHEMA,
16
17
  normalizeBlender,
@@ -18,10 +19,15 @@ import {
18
19
  normalizeMixer,
19
20
  normalizeSmudge,
20
21
  readPaintSource
21
- } from "./chunk-GCVFEPJG.js";
22
+ } from "./chunk-ACTHSQOI.js";
22
23
  import {
23
24
  BLEND_MODES
24
25
  } from "./chunk-IRSTHLQQ.js";
26
+ import {
27
+ decodePng,
28
+ parseDataUrl,
29
+ pngDataUrl
30
+ } from "./chunk-OTTT5N3K.js";
25
31
 
26
32
  // src/brushes/presets.ts
27
33
  var BRUSH_PRESETS = [
@@ -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(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))) : base.count ?? 1;
974
+ const flip = DAB_FLIPS.includes(raw.flip) ? raw.flip : 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 : base?.source;
983
+ if (!source) return base;
984
+ const b = 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;
@@ -1076,6 +1108,7 @@ function normalizePaintBrush(raw = {}, base) {
1076
1108
  light: normalizeLight(raw.light ?? {}, b.light ?? DEFAULT_LIGHT),
1077
1109
  stamp: readStamp(raw.stamp, b.stamp ?? null),
1078
1110
  spray: readSpray(raw.spray, b.spray ?? null),
1111
+ ...withTipGrain(readTipGrain(raw.tipGrain, b.tipGrain ?? null)),
1079
1112
  paint,
1080
1113
  ...raw.builtin ? { builtin: true } : {}
1081
1114
  };
@@ -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.
@@ -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+|${STAMP_KINDS.join("|")})$`;
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 ${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 ${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 = 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."),
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."),
1393
1446
  size: num(1, MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
1394
- hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1)."),
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)."),
@@ -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 = [...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 = 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: pngDataUrl(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 = 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 = parseDataUrl(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 = decodePng(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 = 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 = 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 = parseDataUrl(source.image);
1788
+ if (!parsed) throw new BrushSourceError("The source image is not a data: URL.");
1789
+ const png = decodePng(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);
@@ -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) => normalizeStickerRef(s));
1980
+ let stamps = brush.tip.stamps.map((s) => normalizeStickerRef(s));
1981
+ if (brush.marks?.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 = brush.behaviour?.surface;
1993
+ const marks = brush.marks?.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);
@@ -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,9 +2091,11 @@ 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
- var VECTOR_PROFILE = { ...CANVAS_PROFILE, id: "vector", name: "Vector editor", light: "ignored" };
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 };
1891
2100
  function brushColourKind(paint) {
1892
2101
  if (typeof paint === "string") return "solid";
@@ -1908,7 +2117,10 @@ 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)}.`);
2120
+ if (!profile.tipShapes.includes(brush.tip.shape)) {
2121
+ const image = brush.tip.shape === "stamp" && (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
+ }
1912
2124
  const marks = brush.marks?.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
2126
  const surface = brush.behaviour?.surface ?? "dry";
@@ -1921,6 +2133,8 @@ function checkBrush(brush, profile) {
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 = brush.dynamics ?? {};
2136
+ feature(profile.tipGrain ?? "none", !!brush.tip.grain, "tip.grain", "tip grain", "Remove tip.grain (behaviour.grain textures with the paper instead).");
2137
+ feature(profile.dabCount ?? "none", (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;
@@ -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;
@@ -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 {
@@ -2066,8 +2295,8 @@ function matches(entry, f2) {
2066
2295
  if (f2.tool && b.tool !== f2.tool) return false;
2067
2296
  if (f2.tag && !(b.tags ?? []).includes(f2.tag)) return false;
2068
2297
  if (f2.text) {
2069
- const text = words([b.id, b.name, b.description ?? "", b.variety ?? "", b.medium ?? "", b.tool ?? "", ...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, b.description ?? "", b.variety ?? "", b.medium ?? "", b.tool ?? "", ...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 = (options.presets ?? BRUSH_PRESETS).map(copy3);
2078
2307
  const builtinIds = new Set(presets.map((b) => b.id));
2079
2308
  const key = `${options.namespace ?? "pixerate.brushes"}:user`;
2309
+ const sourcesKey = `${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 {
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 () => {
@@ -2274,6 +2581,9 @@ export {
2274
2581
  DEFAULT_MULTI_COLOR,
2275
2582
  DEFAULT_BEHAVIOR,
2276
2583
  BRUSH_ROTATIONS,
2584
+ DAB_FLIPS,
2585
+ TIP_GRAIN_MODES,
2586
+ DEFAULT_TIP_GRAIN,
2277
2587
  DEFAULT_DYNAMICS,
2278
2588
  normalizeDynamics,
2279
2589
  BRUSH_ID,
@@ -2305,10 +2615,30 @@ export {
2305
2615
  BRUSH_TIP_SHAPES,
2306
2616
  BRUSH_MARKS_KINDS,
2307
2617
  BRUSH_SURFACES,
2618
+ BRUSH_GRAIN_MODES,
2619
+ BRUSH_FLIPS,
2308
2620
  MAX_BRUSH_PALETTE,
2309
2621
  BRUSH_WORD,
2622
+ TIP_GRAIN_PATTERNS,
2623
+ BRUSH_GRAIN_SOURCE_PATTERN,
2310
2624
  BRUSH_JSON_SCHEMA,
2311
2625
  BRUSH_FACET_SCHEMAS,
2626
+ BRUSH_SOURCE_PROVIDER,
2627
+ MAX_BRUSH_SOURCE_SIZE,
2628
+ MAX_BRUSH_SOURCE_INPUT_PIXELS,
2629
+ MAX_BRUSH_SOURCES,
2630
+ BRUSH_SOURCE_CHANNELS,
2631
+ BrushSourceError,
2632
+ brushSourceRef,
2633
+ isBrushSourceRef,
2634
+ brushSourceIdOf,
2635
+ brushSourceIds,
2636
+ summarizeBrushSource,
2637
+ brushSourceIdFromName,
2638
+ brushSourceFromMask,
2639
+ makeBrushSource,
2640
+ readBrushSource,
2641
+ decodeBrushSource,
2312
2642
  WET_BRUSH_TOOLS,
2313
2643
  normalizeBrushSpec,
2314
2644
  PAINT_PROFILE,
@@ -2324,6 +2654,7 @@ export {
2324
2654
  degrade,
2325
2655
  BrushLibraryError,
2326
2656
  isBrushLibrary,
2657
+ parseBrushLibrarySources,
2327
2658
  parseBrushLibrary2,
2328
2659
  createBrushLibrary,
2329
2660
  closest,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PAINT_PROFILE
3
- } from "./chunk-CIJBSUM5.js";
3
+ } from "./chunk-KZI3YWKW.js";
4
4
  import {
5
5
  brushColourHex
6
6
  } from "./chunk-DVPQFMH6.js";