@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
@@ -8,8 +8,8 @@ import {
8
8
  createBrushLibraryToolProvider,
9
9
  createBrushLibraryTools,
10
10
  mergeBrushPatch
11
- } from "../chunk-3C33YTNN.js";
12
- import "../chunk-6JL2D5SG.js";
11
+ } from "../chunk-3XDSNIGF.js";
12
+ import "../chunk-VE6UKYQ2.js";
13
13
  import {
14
14
  BRUSH_PREVIEW_DARK_PAPER,
15
15
  BRUSH_PREVIEW_FALLBACKS,
@@ -17,62 +17,84 @@ import {
17
17
  brushPreviewColours,
18
18
  brushPreviewStrength,
19
19
  brushPreviewsOnDark
20
- } from "../chunk-4ODRXQRW.js";
20
+ } from "../chunk-LETF75CA.js";
21
21
  import {
22
22
  BRUSH_FACET_SCHEMAS,
23
23
  BRUSH_FIELDS,
24
+ BRUSH_FLIPS,
25
+ BRUSH_GRAIN_MODES,
26
+ BRUSH_GRAIN_SOURCE_PATTERN,
24
27
  BRUSH_HOST_PROFILES,
28
+ BRUSH_ID,
25
29
  BRUSH_JSON_SCHEMA,
26
30
  BRUSH_KINDS,
27
31
  BRUSH_MARKS_KINDS,
28
32
  BRUSH_MEDIA,
29
33
  BRUSH_PRESETS,
30
34
  BRUSH_ROTATIONS,
35
+ BRUSH_SOURCE_CHANNELS,
36
+ BRUSH_SOURCE_PROVIDER,
31
37
  BRUSH_SURFACES,
32
38
  BRUSH_TIP_SHAPES,
33
39
  BRUSH_TOOLS,
34
40
  BRUSH_WORD,
35
41
  BUILTIN_BRUSHES,
36
42
  BrushLibraryError,
43
+ BrushSourceError,
37
44
  CANVAS_PROFILE,
38
45
  DOT_ROTATIONS,
39
46
  DOT_TEXTURE_ORDERS,
40
47
  IMAGE_PROFILE,
41
48
  IMPASTO_TOOLS,
42
49
  MAX_BRUSH_PALETTE,
50
+ MAX_BRUSH_SOURCES,
51
+ MAX_BRUSH_SOURCE_INPUT_PIXELS,
52
+ MAX_BRUSH_SOURCE_SIZE,
43
53
  MAX_DOT_TEXTURES,
44
54
  MAX_STICKER_STAMPS,
45
55
  PAINT_PROFILE,
46
56
  PARTICLE_KINDS,
47
57
  PARTICLE_RENDERS,
48
58
  RIBBON_PAIRS,
59
+ TIP_GRAIN_PATTERNS,
49
60
  VECTOR_PROFILE,
50
61
  WET_BRUSH_TOOLS,
51
62
  WET_MEDIA,
52
63
  brushColourKind,
64
+ brushSourceFromMask,
65
+ brushSourceIdFromName,
66
+ brushSourceIdOf,
67
+ brushSourceIds,
68
+ brushSourceRef,
53
69
  brushToJSON,
54
70
  checkBrush,
55
71
  checkBrushEdit,
56
72
  createBrushLibrary,
73
+ decodeBrushSource,
57
74
  degrade,
58
75
  getPath,
59
76
  isBrushLibrary,
77
+ isBrushSourceRef,
60
78
  listPresets,
79
+ makeBrushSource,
61
80
  migrateBrushV1,
62
81
  normalizeBrushSpec,
63
82
  normalizePaintBrush,
64
83
  parseBrushLibrary2 as parseBrushLibrary,
84
+ parseBrushLibrarySources,
85
+ readBrushSource,
65
86
  resolveBrush,
66
87
  resolveCategory,
67
88
  resolveEngine,
68
89
  resolveWetMedium,
69
90
  setPath,
70
91
  snapshotBrushLibrary,
92
+ summarizeBrushSource,
71
93
  withKeptIssues
72
- } from "../chunk-CIJBSUM5.js";
94
+ } from "../chunk-KZI3YWKW.js";
73
95
  import {
74
96
  STAMP_KINDS
75
- } from "../chunk-ACFSFUQH.js";
97
+ } from "../chunk-JL6YH5WD.js";
76
98
  import {
77
99
  brushColourHex
78
100
  } from "../chunk-DVPQFMH6.js";
@@ -80,12 +102,17 @@ import "../chunk-SZTI6EER.js";
80
102
  import "../chunk-V2ZCJRDM.js";
81
103
  import {
82
104
  MAX_BRUSH_SIZE
83
- } from "../chunk-GCVFEPJG.js";
105
+ } from "../chunk-ACTHSQOI.js";
84
106
  import "../chunk-4D6KP5BV.js";
85
107
  import {
86
108
  BLEND_MODES
87
109
  } from "../chunk-IRSTHLQQ.js";
110
+ import {
111
+ bytesToBase64,
112
+ isPng
113
+ } from "../chunk-OTTT5N3K.js";
88
114
  import "../chunk-HVVR5PZW.js";
115
+ import "../chunk-QGHEDC7B.js";
89
116
  import {
90
117
  isDualColor
91
118
  } from "../chunk-VR6OGHU5.js";
@@ -107,6 +134,9 @@ var RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads", ribbon: "Ri
107
134
  var APPLICATOR_LABELS = { brush: "Brush", tube: "Paint tube", knife: "Palette knife", roller: "Roller", gloop: "Gloop pen" };
108
135
  var WET_MEDIUM_LABELS = { watercolor: "Watercolour", gouache: "Gouache", oil: "Oil" };
109
136
  var STAMP_OPTIONS = STAMP_KINDS.map((k) => ({ value: `editor:${k}`, label: title(k) }));
137
+ var GRAIN_MODE_LABELS = { texturised: "Texturised (stays put)", moving: "Moving (rolls with the tip)" };
138
+ var FLIP_LABELS = { none: "None", x: "Across", y: "Along", xy: "Both" };
139
+ var DAB_ENGINES = /* @__PURE__ */ new Set(["dab", "eraser", "mixer", "blender", "smudge"]);
110
140
  var ARIA = {
111
141
  "dynamics.rotation": "Brush rotation",
112
142
  "tip.blend": "Brush blend",
@@ -119,10 +149,12 @@ var ARIA = {
119
149
  "marks.stampRotation": "Dot rotation",
120
150
  "behaviour.wet.medium": "Wet medium",
121
151
  "behaviour.thick.applicator": "Impasto tool",
122
- "light.pressure": "Light follows pressure"
152
+ "light.pressure": "Light follows pressure",
153
+ "tip.grain.source": "Tip grain",
154
+ "dynamics.angleJitter": "Brush rotation jitter"
123
155
  };
124
156
  var aria = (path, label) => ARIA[path] ?? `Brush ${label.toLowerCase()}`;
125
- function brushEditorSections(brush, profile = PAINT_PROFILE) {
157
+ function brushEditorSections(brush, profile = PAINT_PROFILE, context = {}) {
126
158
  const resolved = resolveBrush(brush);
127
159
  const engine = resolveEngine(brush);
128
160
  const fieldOf = (facet) => BRUSH_FIELDS.find((f) => f.facet === facet && f.engines.includes(engine));
@@ -160,6 +192,7 @@ function brushEditorSections(brush, profile = PAINT_PROFILE) {
160
192
  };
161
193
  const isTool = brush.kind === "tool";
162
194
  const shape = brush.tip.shape;
195
+ const sourceOptions = (context.sources ?? []).map((x) => ({ value: brushSourceRef(x.id), label: x.name }));
163
196
  const marks = brush.marks?.kind ?? "continuous";
164
197
  const exchangeFull = full(brush.behaviour?.exchange?.reservoir ? profile.reservoir : profile.exchange);
165
198
  add("medium", "Medium", [
@@ -175,8 +208,20 @@ function brushEditorSections(brush, profile = PAINT_PROFILE) {
175
208
  ...tipSliders.map((c) => slider(c)),
176
209
  engine === "dab" && choice("tip.blend", "Blend", BLEND_MODES.map((m) => ({ value: m.id, label: m.label })), "normal"),
177
210
  ...shape === "bristle" && engineOk && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("bristle.")).map((c) => slider(c)) : [],
178
- shape === "stamp" && engineOk && withNote({ kind: "stamps", path: "tip.stamps", label: "Stamps", ariaLabel: "Brush stamps", value: [...value("tip.stamps") ?? []], max: marks === "particles" ? MAX_DOT_TEXTURES : MAX_STICKER_STAMPS, options: STAMP_OPTIONS })
211
+ shape === "stamp" && engineOk && withNote({ kind: "stamps", path: "tip.stamps", label: "Stamps", ariaLabel: "Brush stamps", value: [...value("tip.stamps") ?? []], max: marks === "particles" ? MAX_DOT_TEXTURES : MAX_STICKER_STAMPS, options: marks === "objects" ? STAMP_OPTIONS : [...STAMP_OPTIONS, ...sourceOptions] })
179
212
  ]);
213
+ const dabs = DAB_ENGINES.has(engine) && engineOk && shape !== "bristle" && marks !== "particles" && marks !== "objects";
214
+ const grain = brush.tip.grain;
215
+ if (dabs && full(profile.tipGrain ?? "none")) {
216
+ add("grain", "Tip grain", [
217
+ choice("tip.grain.source", "Texture", [{ value: "", label: "None" }, ...options(TIP_GRAIN_PATTERNS), ...sourceOptions], ""),
218
+ !!grain && choice("tip.grain.mode", "Grain moves", options(BRUSH_GRAIN_MODES, GRAIN_MODE_LABELS), "texturised"),
219
+ !!grain && withNote({ kind: "slider", path: "tip.grain.scale", label: "Grain scale", ariaLabel: "Brush grain scale", min: 0.1, max: 8, step: 0.05, value: grain.scale ?? 1 }),
220
+ !!grain && withNote({ kind: "slider", path: "tip.grain.depth", label: "Grain depth", ariaLabel: "Brush grain depth", min: 0, max: 1, step: 0.01, value: grain.depth ?? 0.6 }),
221
+ !!grain && withNote({ kind: "slider", path: "tip.grain.contrast", label: "Grain contrast", ariaLabel: "Brush grain contrast", min: 0, max: 3, step: 0.05, value: grain.contrast ?? 1 }),
222
+ !!grain && toggle("tip.grain.invert", "Invert grain")
223
+ ]);
224
+ }
180
225
  if (!isTool && profile.marks.length > 1 && engineOk) {
181
226
  const particles = marks === "particles" && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("particle.") && c.path !== "particle.waveSpeed") : [];
182
227
  const objects = marks === "objects" && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("sticker.")) : [];
@@ -193,11 +238,18 @@ function brushEditorSections(brush, profile = PAINT_PROFILE) {
193
238
  ]);
194
239
  }
195
240
  const feel = BRUSH_SLIDERS.filter((c) => ["tip.smoothing", "pressureSmoothing", "dynamics.minSize", "dynamics.tiltSize", "dynamics.velocitySize"].includes(c.path)).filter((c) => !(c.path === "dynamics.tiltSize" && !full(profile.tilt)) && !(c.path === "dynamics.velocitySize" && !full(profile.velocity)));
241
+ const stampDabs = dabs && shape === "stamp";
242
+ const jitter = (path, label, max = 1, step = 0.01) => withNote({ kind: "slider", path, label, ariaLabel: aria(path, label), min: 0, max, step, value: Number(value(path) ?? 0) });
196
243
  add("feel", "Feel", [
197
244
  ...feel.map((c) => slider(c)),
198
245
  toggle("dynamics.pressureSize", "Pressure \u2192 size"),
199
246
  toggle("dynamics.pressureOpacity", "Pressure \u2192 opacity"),
200
- choice("dynamics.rotation", "Rotation", options(BRUSH_ROTATIONS, ROTATION_LABELS))
247
+ choice("dynamics.rotation", "Rotation", options(BRUSH_ROTATIONS, ROTATION_LABELS)),
248
+ stampDabs && full(profile.dabCount ?? "none") && withNote({ kind: "slider", path: "dynamics.count", label: "Count", ariaLabel: "Brush dab count", min: 1, max: 16, step: 1, value: brush.dynamics?.count ?? 1 }),
249
+ stampDabs && jitter("dynamics.scatter", "Scatter", 4),
250
+ stampDabs && jitter("dynamics.angleJitter", "Rotation jitter"),
251
+ stampDabs && jitter("dynamics.sizeJitter", "Size jitter"),
252
+ stampDabs && choice("dynamics.flip", "Flip", options(BRUSH_FLIPS, FLIP_LABELS), "none")
201
253
  ]);
202
254
  const surfaces = profile.surfaces;
203
255
  const engineSliders = (engineOk ? engineControls?.sliders ?? [] : []).filter((c) => !/^(bristle|particle|sticker)\./.test(c.path) && c.path !== "light.amount" && c.path !== "impasto.rim" && c.path !== "impasto.clarity");
@@ -259,6 +311,10 @@ function hiddenLabel(issue, b) {
259
311
  return "paint pick-up";
260
312
  case "behaviour.grain":
261
313
  return "paper grain";
314
+ case "tip.grain":
315
+ return "tip grain";
316
+ case "dynamics.count":
317
+ return "several dabs per step";
262
318
  case "light":
263
319
  return "light";
264
320
  case "motion":
@@ -330,6 +386,8 @@ function setBrushEditorValue(brush, path, value) {
330
386
  next.marks = value === "continuous" ? void 0 : { kind: value };
331
387
  if (value !== "continuous" && next.tip.shape === "bristle") next.tip = { ...next.tip, shape: "round", bristle: void 0 };
332
388
  if (value === "objects") next.tip = { ...next.tip, shape: "stamp" };
389
+ } else if (path === "tip.grain.source" && (value === void 0 || value === "")) {
390
+ delete next.tip.grain;
333
391
  } else if (path === "tip.shape" && value !== "stamp") {
334
392
  next.tip = { ...next.tip, shape: value, stamps: void 0 };
335
393
  if (value !== "bristle") delete next.tip.bristle;
@@ -369,6 +427,28 @@ function brushLibraryGroups(entries, filter = {}) {
369
427
  const order = ["dry", "ink", "paint", "spray", "other", "tools"];
370
428
  return [...groups.values()].sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
371
429
  }
430
+ async function imageFileToPngDataUrl(file) {
431
+ const bytes = new Uint8Array(await file.arrayBuffer());
432
+ if (isPng(bytes)) return `data:image/png;base64,${bytesToBase64(bytes)}`;
433
+ const g = globalThis;
434
+ if (!g.createImageBitmap || !g.OffscreenCanvas) throw new BrushSourceError("Only PNG images can be read here; convert the image to PNG.");
435
+ const bitmap = await g.createImageBitmap(file);
436
+ const canvas = new g.OffscreenCanvas(bitmap.width, bitmap.height);
437
+ canvas.getContext("2d").drawImage(bitmap, 0, 0);
438
+ const png = new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer());
439
+ return `data:image/png;base64,${bytesToBase64(png)}`;
440
+ }
441
+ async function addBrushSourceFromFile(library, file, options2 = {}) {
442
+ const name = (options2.name ?? file.name?.replace(/\.[a-z0-9]+$/i, "") ?? "Image").trim() || "Image";
443
+ const result = await library.addSource({ name, image: await imageFileToPngDataUrl(file), origin: file.name, ...options2.channel ? { channel: options2.channel } : {} });
444
+ return { ...result, ref: brushSourceRef(result.source.id) };
445
+ }
446
+ function withBrushSource(brush, ref, as) {
447
+ if (as === "grain") return setBrushEditorValue(brush, "tip.grain.source", ref);
448
+ const stamp = brush.tip.shape === "stamp" ? { brush, problems: [] } : setBrushEditorValue(brush, "tip.shape", "stamp");
449
+ const r = setBrushEditorValue(stamp.brush, "tip.stamps", [ref]);
450
+ return { brush: r.brush, problems: [...stamp.problems, ...r.problems] };
451
+ }
372
452
 
373
453
  // src/brushes/contract.ts
374
454
  var BRUSH_CONTRACT_FORMAT_VERSION = 1;
@@ -391,7 +471,9 @@ var BRUSH_PROFILE_JSON_SCHEMA = {
391
471
  motion: { ...SUPPORT, description: "Motion after painting." },
392
472
  colours: { type: "array", items: { type: "string", enum: ["solid", "dual", "gradient", "effect"] }, description: "Colour kinds a default colour may be." },
393
473
  velocity: { ...SUPPORT, description: "Velocity dynamics." },
394
- tilt: { ...SUPPORT, description: "Tilt dynamics." }
474
+ tilt: { ...SUPPORT, description: "Tilt dynamics." },
475
+ tipGrain: { ...SUPPORT, description: "Texture inside the tip (tip.grain); missing: none." },
476
+ dabCount: { ...SUPPORT, description: "Several dabs per spacing step (dynamics.count); missing: none." }
395
477
  },
396
478
  required: ["id", "name", "kinds", "tools", "tipShapes", "marks", "surfaces", "exchange", "reservoir", "grain", "light", "motion", "colours", "velocity", "tilt"],
397
479
  additionalProperties: false
@@ -402,7 +484,27 @@ var BRUSH_LIBRARY_FILE_JSON_SCHEMA = {
402
484
  properties: {
403
485
  format: { type: "string", const: "pixerate-brushes", description: 'Always "pixerate-brushes".' },
404
486
  version: { type: "integer", const: 2, description: "Always 2." },
405
- brushes: { type: "array", items: { $ref: "brush.schema.json" }, description: "The brushes, each a facet brush (brush.schema.json)." }
487
+ brushes: { type: "array", items: { $ref: "brush.schema.json" }, description: "The brushes, each a facet brush (brush.schema.json)." },
488
+ sources: {
489
+ type: "array",
490
+ maxItems: MAX_BRUSH_SOURCES,
491
+ description: "The image sources the brushes name (source:<id> in tip.stamps or tip.grain.source), bytes included.",
492
+ items: {
493
+ type: "object",
494
+ description: "An image source: a grey mask, 255 paints (or is a grain peak).",
495
+ properties: {
496
+ id: { type: "string", pattern: BRUSH_ID.source, description: "Source id." },
497
+ name: { type: "string", maxLength: 80, description: "Display name." },
498
+ width: { type: "integer", minimum: 1, maximum: MAX_BRUSH_SOURCE_SIZE, description: "Width in px." },
499
+ height: { type: "integer", minimum: 1, maximum: MAX_BRUSH_SOURCE_SIZE, description: "Height in px." },
500
+ image: { type: "string", pattern: "^data:image/png;base64,", description: "The mask as a greyscale PNG data: URL." },
501
+ licence: { type: "string", maxLength: 2e3, description: "The licence or terms it came with." },
502
+ origin: { type: "string", maxLength: 200, description: "Where it came from (a file or pack name)." }
503
+ },
504
+ required: ["id", "name", "width", "height", "image"],
505
+ additionalProperties: false
506
+ }
507
+ }
406
508
  },
407
509
  required: ["format", "version", "brushes"],
408
510
  additionalProperties: false
@@ -480,6 +582,9 @@ function brushContracts() {
480
582
  export {
481
583
  BRUSH_CONTRACT_FORMAT_VERSION,
482
584
  BRUSH_FACET_SCHEMAS,
585
+ BRUSH_FLIPS,
586
+ BRUSH_GRAIN_MODES,
587
+ BRUSH_GRAIN_SOURCE_PATTERN,
483
588
  BRUSH_HOST_PROFILES,
484
589
  BRUSH_JSON_SCHEMA,
485
590
  BRUSH_KINDS,
@@ -492,19 +597,27 @@ export {
492
597
  BRUSH_PREVIEW_FALLBACKS,
493
598
  BRUSH_PREVIEW_MIN_CONTRAST,
494
599
  BRUSH_PROFILE_JSON_SCHEMA,
600
+ BRUSH_SOURCE_CHANNELS,
601
+ BRUSH_SOURCE_PROVIDER,
495
602
  BRUSH_SURFACES,
496
603
  BRUSH_TIP_SHAPES,
497
604
  BRUSH_TOOLS,
498
605
  BRUSH_UNSET_PROPERTY,
499
606
  BRUSH_WORD,
500
607
  BrushLibraryError,
608
+ BrushSourceError,
501
609
  CANVAS_PROFILE,
502
610
  IMAGE_PROFILE,
503
611
  MAX_BRUSH_PALETTE,
612
+ MAX_BRUSH_SOURCES,
613
+ MAX_BRUSH_SOURCE_INPUT_PIXELS,
614
+ MAX_BRUSH_SOURCE_SIZE,
504
615
  PAINT_PROFILE,
616
+ TIP_GRAIN_PATTERNS,
505
617
  UNSETTABLE_BRUSH_FACETS,
506
618
  VECTOR_PROFILE,
507
619
  WET_BRUSH_TOOLS,
620
+ addBrushSourceFromFile,
508
621
  brushColourHex,
509
622
  brushColourKind,
510
623
  brushContracts,
@@ -515,20 +628,31 @@ export {
515
628
  brushPreviewColours,
516
629
  brushPreviewStrength,
517
630
  brushPreviewsOnDark,
631
+ brushSourceFromMask,
632
+ brushSourceIdFromName,
633
+ brushSourceIdOf,
634
+ brushSourceIds,
635
+ brushSourceRef,
518
636
  changedBrushPaths,
519
637
  checkBrush,
520
638
  checkBrushEdit,
521
639
  createBrushLibrary,
522
640
  createBrushLibraryToolProvider,
523
641
  createBrushLibraryTools,
642
+ decodeBrushSource,
524
643
  degrade,
644
+ imageFileToPngDataUrl,
525
645
  isBrushLibrary,
646
+ isBrushSourceRef,
526
647
  listPresets,
648
+ makeBrushSource,
527
649
  mergeBrushPatch,
528
650
  migrateBrushV1,
529
651
  newBrushFor,
530
652
  normalizeBrushSpec,
531
653
  parseBrushLibrary,
654
+ parseBrushLibrarySources,
655
+ readBrushSource,
532
656
  resolveBrush,
533
657
  resolveCategory,
534
658
  resolveEngine,
@@ -536,5 +660,7 @@ export {
536
660
  saveBrushEdit,
537
661
  setBrushEditorValue,
538
662
  snapshotBrushLibrary,
663
+ summarizeBrushSource,
664
+ withBrushSource,
539
665
  withKeptIssues
540
666
  };
@@ -9,14 +9,17 @@
9
9
  */
10
10
  import type { StorageAdapter } from '../persistence/storage.cjs';
11
11
  import { type BrushHostProfile } from './profiles.cjs';
12
+ import { type BrushSource, type BrushSourceInput } from './sources.cjs';
12
13
  import type { Brush, BrushKind, BrushMedium, BrushTool } from './types.cjs';
13
14
  /** The JSON file a brush library exports to and imports from. */
14
15
  export interface BrushLibraryFile {
15
16
  format: 'pixerate-brushes';
16
17
  version: 2;
17
18
  brushes: Brush[];
19
+ /** The image sources the brushes lay or texture with (`source:<id>`), bytes included. */
20
+ sources?: BrushSource[];
18
21
  }
19
- export type BrushLibraryErrorCode = 'unknown_brush' | 'builtin' | 'exists' | 'invalid';
22
+ export type BrushLibraryErrorCode = 'unknown_brush' | 'unknown_source' | 'builtin' | 'exists' | 'invalid';
20
23
  /** A refusal; the message says what to do instead. */
21
24
  export declare class BrushLibraryError extends Error {
22
25
  readonly code: BrushLibraryErrorCode;
@@ -54,6 +57,16 @@ export interface ImportBrushesResult {
54
57
  replaced: string[];
55
58
  skipped: string[];
56
59
  problems: string[];
60
+ /** Image sources added or replaced (only when there were any). */
61
+ sources?: string[];
62
+ }
63
+ export interface AddBrushSourceResult {
64
+ /** The source as stored. */
65
+ source: BrushSource;
66
+ /** What reading the image changed (the channel picked, scaling). */
67
+ problems: string[];
68
+ /** It replaced a source of the same id. */
69
+ replaced: boolean;
57
70
  }
58
71
  export interface BrushLibraryOptions {
59
72
  /** Where the user's brushes are kept (default: memory only, gone on reload). */
@@ -88,11 +101,24 @@ export interface BrushLibrary {
88
101
  importFile(raw: unknown, options?: {
89
102
  overwrite?: boolean;
90
103
  }): Promise<ImportBrushesResult>;
104
+ /** The image sources brushes lay (`tip.stamps` `source:<id>`) or texture with (`tip.grain.source`), in the order added. */
105
+ listSources(): Promise<BrushSource[]>;
106
+ getSource(id: string): Promise<BrushSource | null>;
107
+ /**
108
+ * Stores an image source: an uploaded image (`BrushSourceInput`, read by `makeBrushSource`) or a
109
+ * source as stored. Without an id it gets one from its name; a taken id is refused unless
110
+ * `overwrite`.
111
+ */
112
+ addSource(input: BrushSourceInput | BrushSource, options?: {
113
+ overwrite?: boolean;
114
+ }): Promise<AddBrushSourceResult>;
91
115
  /** Called after every change. Returns an unsubscribe function. */
92
116
  subscribe(listener: () => void): () => void;
93
117
  }
94
118
  /** Whether `library` is a whole `BrushLibrary` (it can save), not only a list to read: hosts offer brush editing on one. */
95
119
  export declare function isBrushLibrary(library: unknown): library is BrushLibrary;
120
+ /** The image sources of a library file (`sources`), each checked; bad ones are skipped and reported. */
121
+ export declare function parseBrushLibrarySources(raw: unknown, problems?: string[]): BrushSource[];
96
122
  /**
97
123
  * Brushes read from a library file: `{ format: 'pixerate-brushes', version: 1 | 2, brushes }` or
98
124
  * a bare array. Version 1 brushes are migrated (`migrateBrushV1`); every brush is normalised.
@@ -9,14 +9,17 @@
9
9
  */
10
10
  import type { StorageAdapter } from '../persistence/storage.js';
11
11
  import { type BrushHostProfile } from './profiles.js';
12
+ import { type BrushSource, type BrushSourceInput } from './sources.js';
12
13
  import type { Brush, BrushKind, BrushMedium, BrushTool } from './types.js';
13
14
  /** The JSON file a brush library exports to and imports from. */
14
15
  export interface BrushLibraryFile {
15
16
  format: 'pixerate-brushes';
16
17
  version: 2;
17
18
  brushes: Brush[];
19
+ /** The image sources the brushes lay or texture with (`source:<id>`), bytes included. */
20
+ sources?: BrushSource[];
18
21
  }
19
- export type BrushLibraryErrorCode = 'unknown_brush' | 'builtin' | 'exists' | 'invalid';
22
+ export type BrushLibraryErrorCode = 'unknown_brush' | 'unknown_source' | 'builtin' | 'exists' | 'invalid';
20
23
  /** A refusal; the message says what to do instead. */
21
24
  export declare class BrushLibraryError extends Error {
22
25
  readonly code: BrushLibraryErrorCode;
@@ -54,6 +57,16 @@ export interface ImportBrushesResult {
54
57
  replaced: string[];
55
58
  skipped: string[];
56
59
  problems: string[];
60
+ /** Image sources added or replaced (only when there were any). */
61
+ sources?: string[];
62
+ }
63
+ export interface AddBrushSourceResult {
64
+ /** The source as stored. */
65
+ source: BrushSource;
66
+ /** What reading the image changed (the channel picked, scaling). */
67
+ problems: string[];
68
+ /** It replaced a source of the same id. */
69
+ replaced: boolean;
57
70
  }
58
71
  export interface BrushLibraryOptions {
59
72
  /** Where the user's brushes are kept (default: memory only, gone on reload). */
@@ -88,11 +101,24 @@ export interface BrushLibrary {
88
101
  importFile(raw: unknown, options?: {
89
102
  overwrite?: boolean;
90
103
  }): Promise<ImportBrushesResult>;
104
+ /** The image sources brushes lay (`tip.stamps` `source:<id>`) or texture with (`tip.grain.source`), in the order added. */
105
+ listSources(): Promise<BrushSource[]>;
106
+ getSource(id: string): Promise<BrushSource | null>;
107
+ /**
108
+ * Stores an image source: an uploaded image (`BrushSourceInput`, read by `makeBrushSource`) or a
109
+ * source as stored. Without an id it gets one from its name; a taken id is refused unless
110
+ * `overwrite`.
111
+ */
112
+ addSource(input: BrushSourceInput | BrushSource, options?: {
113
+ overwrite?: boolean;
114
+ }): Promise<AddBrushSourceResult>;
91
115
  /** Called after every change. Returns an unsubscribe function. */
92
116
  subscribe(listener: () => void): () => void;
93
117
  }
94
118
  /** Whether `library` is a whole `BrushLibrary` (it can save), not only a list to read: hosts offer brush editing on one. */
95
119
  export declare function isBrushLibrary(library: unknown): library is BrushLibrary;
120
+ /** The image sources of a library file (`sources`), each checked; bad ones are skipped and reported. */
121
+ export declare function parseBrushLibrarySources(raw: unknown, problems?: string[]): BrushSource[];
96
122
  /**
97
123
  * Brushes read from a library file: `{ format: 'pixerate-brushes', version: 1 | 2, brushes }` or
98
124
  * a bare array. Version 1 brushes are migrated (`migrateBrushV1`); every brush is normalised.
@@ -35,6 +35,10 @@ export interface BrushHostProfile {
35
35
  velocity: BrushSupport;
36
36
  /** Tilt dynamics (needs pen tilt). */
37
37
  tilt: BrushSupport;
38
+ /** Texture inside the tip (`tip.grain`); missing: `none`. */
39
+ tipGrain?: BrushSupport;
40
+ /** Several dabs per spacing step (`dynamics.count` above 1); missing: `none`. */
41
+ dabCount?: BrushSupport;
38
42
  }
39
43
  export interface BrushIssue {
40
44
  /** The facet path the issue is about, e.g. `tip.shape`, `light`, `behaviour.exchange.reservoir`. */
@@ -58,7 +62,12 @@ export declare const IMAGE_PROFILE: BrushHostProfile;
58
62
  * light channel (glow and spill), which canvas strokes don't have, so it is refused.
59
63
  */
60
64
  export declare const CANVAS_PROFILE: BrushHostProfile;
61
- /** Vector brushstrokes: dry dab brushes along a path; light brushes paint as plain colour. */
65
+ /**
66
+ * Vector brushstrokes: dab brushes along a path; light brushes paint as plain colour. Wet paint
67
+ * settles in its own small wet area and dries to a cached image; the Mixer Brush, smudge and
68
+ * blender act on the shapes beneath the stroke. The wet tools (water, hair dryer, blow, wet blend,
69
+ * wet drag, blot) act on wet paint, which a vector document never holds, so they are refused.
70
+ */
62
71
  export declare const VECTOR_PROFILE: BrushHostProfile;
63
72
  /** The built-in profiles by id. */
64
73
  export declare const BRUSH_HOST_PROFILES: Readonly<Record<string, BrushHostProfile>>;
@@ -35,6 +35,10 @@ export interface BrushHostProfile {
35
35
  velocity: BrushSupport;
36
36
  /** Tilt dynamics (needs pen tilt). */
37
37
  tilt: BrushSupport;
38
+ /** Texture inside the tip (`tip.grain`); missing: `none`. */
39
+ tipGrain?: BrushSupport;
40
+ /** Several dabs per spacing step (`dynamics.count` above 1); missing: `none`. */
41
+ dabCount?: BrushSupport;
38
42
  }
39
43
  export interface BrushIssue {
40
44
  /** The facet path the issue is about, e.g. `tip.shape`, `light`, `behaviour.exchange.reservoir`. */
@@ -58,7 +62,12 @@ export declare const IMAGE_PROFILE: BrushHostProfile;
58
62
  * light channel (glow and spill), which canvas strokes don't have, so it is refused.
59
63
  */
60
64
  export declare const CANVAS_PROFILE: BrushHostProfile;
61
- /** Vector brushstrokes: dry dab brushes along a path; light brushes paint as plain colour. */
65
+ /**
66
+ * Vector brushstrokes: dab brushes along a path; light brushes paint as plain colour. Wet paint
67
+ * settles in its own small wet area and dries to a cached image; the Mixer Brush, smudge and
68
+ * blender act on the shapes beneath the stroke. The wet tools (water, hair dryer, blow, wet blend,
69
+ * wet drag, blot) act on wet paint, which a vector document never holds, so they are refused.
70
+ */
62
71
  export declare const VECTOR_PROFILE: BrushHostProfile;
63
72
  /** The built-in profiles by id. */
64
73
  export declare const BRUSH_HOST_PROFILES: Readonly<Record<string, BrushHostProfile>>;
@@ -10,11 +10,15 @@ export declare const MAX_BRUSH_PALETTE = 16;
10
10
  export declare const BRUSH_WORD: RegExp;
11
11
  /** A sticker ref `provider:id`, or a bare editor stamp (read as `editor:<stamp>`). */
12
12
  export declare const BRUSH_STAMP_PATTERN: string;
13
+ /** Tip grain sources: the built-in patterns (not the flat `smooth`) or an image source `source:<id>`. */
14
+ export declare const TIP_GRAIN_PATTERNS: ("canvas" | "linen" | "cold-pressed" | "hot-pressed" | "rough" | "vellum" | "laid" | "ingres" | "sanded" | "gesso")[];
15
+ export declare const BRUSH_GRAIN_SOURCE_PATTERN: string;
13
16
  /** JSON Schema of a `Brush`. */
14
17
  export declare const BRUSH_JSON_SCHEMA: JsonSchema;
15
18
  /** The facet schemas, for tools that take one facet. */
16
19
  export declare const BRUSH_FACET_SCHEMAS: {
17
20
  readonly tip: JsonSchema;
21
+ readonly tipGrain: JsonSchema;
18
22
  readonly marks: {
19
23
  readonly continuous: JsonSchema;
20
24
  readonly scatter: JsonSchema;
@@ -10,11 +10,15 @@ export declare const MAX_BRUSH_PALETTE = 16;
10
10
  export declare const BRUSH_WORD: RegExp;
11
11
  /** A sticker ref `provider:id`, or a bare editor stamp (read as `editor:<stamp>`). */
12
12
  export declare const BRUSH_STAMP_PATTERN: string;
13
+ /** Tip grain sources: the built-in patterns (not the flat `smooth`) or an image source `source:<id>`. */
14
+ export declare const TIP_GRAIN_PATTERNS: ("canvas" | "linen" | "cold-pressed" | "hot-pressed" | "rough" | "vellum" | "laid" | "ingres" | "sanded" | "gesso")[];
15
+ export declare const BRUSH_GRAIN_SOURCE_PATTERN: string;
13
16
  /** JSON Schema of a `Brush`. */
14
17
  export declare const BRUSH_JSON_SCHEMA: JsonSchema;
15
18
  /** The facet schemas, for tools that take one facet. */
16
19
  export declare const BRUSH_FACET_SCHEMAS: {
17
20
  readonly tip: JsonSchema;
21
+ readonly tipGrain: JsonSchema;
18
22
  readonly marks: {
19
23
  readonly continuous: JsonSchema;
20
24
  readonly scatter: JsonSchema;