@pixerate/editor 0.267.0 → 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 (80) 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-FGJHJIMN.js → chunk-74ODPZLX.js} +10 -10
  27. package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
  28. package/dist/{chunk-LHPEG5AU.cjs → chunk-B3G7BKUV.cjs} +90 -90
  29. package/dist/{chunk-EF7LZAXQ.js → chunk-BAZMCFVL.js} +1 -1
  30. package/dist/{chunk-HIO6E7LC.cjs → chunk-BPJSI4NT.cjs} +2 -2
  31. package/dist/{chunk-XVOP3CGQ.cjs → chunk-EU2ZZA6L.cjs} +393 -62
  32. package/dist/{chunk-YIC6MAJ2.js → chunk-HFY5LG5J.js} +1 -1
  33. package/dist/{chunk-7UKFXXWL.js → chunk-HVO6XCI4.js} +1 -1
  34. package/dist/{chunk-OKHKA43L.cjs → chunk-JBB3GDBB.cjs} +330 -216
  35. package/dist/{chunk-AGNUD5BI.js → chunk-KZI3YWKW.js} +351 -20
  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/index.cjs +851 -851
  46. package/dist/image-editor/index.js +23 -23
  47. package/dist/paint/index.cjs +5 -5
  48. package/dist/paint/index.js +11 -11
  49. package/dist/paint-editor/brush-library-tools.d.cts +12 -2
  50. package/dist/paint-editor/brush-library-tools.d.ts +12 -2
  51. package/dist/paint-editor/brush-model.d.cts +29 -1
  52. package/dist/paint-editor/brush-model.d.ts +29 -1
  53. package/dist/paint-editor/brush-source-art.d.cts +32 -0
  54. package/dist/paint-editor/brush-source-art.d.ts +32 -0
  55. package/dist/paint-editor/controller.d.cts +6 -1
  56. package/dist/paint-editor/controller.d.ts +6 -1
  57. package/dist/paint-editor/dab-stroke.d.cts +2 -0
  58. package/dist/paint-editor/dab-stroke.d.ts +2 -0
  59. package/dist/paint-editor/engine.d.cts +11 -2
  60. package/dist/paint-editor/engine.d.ts +11 -2
  61. package/dist/paint-editor/index.cjs +576 -481
  62. package/dist/paint-editor/index.d.cts +3 -1
  63. package/dist/paint-editor/index.d.ts +3 -1
  64. package/dist/paint-editor/index.js +127 -32
  65. package/dist/paint-editor/stamp-tip.d.cts +8 -5
  66. package/dist/paint-editor/stamp-tip.d.ts +8 -5
  67. package/dist/paint-editor/thumbnails.d.cts +6 -0
  68. package/dist/paint-editor/thumbnails.d.ts +6 -0
  69. package/dist/paint-editor/tip-grain.d.cts +34 -0
  70. package/dist/paint-editor/tip-grain.d.ts +34 -0
  71. package/dist/raster/index.cjs +2 -2
  72. package/dist/raster/index.js +9 -9
  73. package/dist/vector/index.cjs +1002 -1002
  74. package/dist/vector/index.js +7 -7
  75. package/package.json +1 -1
  76. package/skills/pixerate-brushes/SKILL.md +35 -8
  77. package/skills/pixerate-brushes-design/SKILL.md +8 -1
  78. package/skills/pixerate-paint-editor/SKILL.md +6 -0
  79. package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
  80. package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
@@ -26,7 +26,9 @@ export * from './mixing-pad.cjs';
26
26
  export * from './controller.cjs';
27
27
  export * from './presence.cjs';
28
28
  export * from './tools.cjs';
29
- export { createPaintBrushLibraryToolProvider, paintBrushPreviewRenderer, type PaintBrushLibraryToolProviderOptions } from './brush-library-tools.cjs';
29
+ export { createPaintBrushLibraryToolProvider, paintBrushPreviewRenderer, type PaintBrushLibraryToolProviderOptions, type PaintBrushPreviewOptions } from './brush-library-tools.cjs';
30
+ export { BrushSourceArtCache, engineBrushSourceIds } from './brush-source-art.cjs';
31
+ export { TipGrain, type TipGrainDab } from './tip-grain.cjs';
30
32
  export type { PaintAgentTool, PaintOrigin, PaintToolCoordinates } from './hosted.cjs';
31
33
  export * from './ui.cjs';
32
34
  export { BLEND_MODES, PAINTERLY_BLEND_MODES, isBlendMode, isPainterlyBlendMode, standardBlendMode, type BlendMode, type BlendModeInfo } from '../raster/blend.cjs';
@@ -26,7 +26,9 @@ export * from './mixing-pad.js';
26
26
  export * from './controller.js';
27
27
  export * from './presence.js';
28
28
  export * from './tools.js';
29
- export { createPaintBrushLibraryToolProvider, paintBrushPreviewRenderer, type PaintBrushLibraryToolProviderOptions } from './brush-library-tools.js';
29
+ export { createPaintBrushLibraryToolProvider, paintBrushPreviewRenderer, type PaintBrushLibraryToolProviderOptions, type PaintBrushPreviewOptions } from './brush-library-tools.js';
30
+ export { BrushSourceArtCache, engineBrushSourceIds } from './brush-source-art.js';
31
+ export { TipGrain, type TipGrainDab } from './tip-grain.js';
30
32
  export type { PaintAgentTool, PaintOrigin, PaintToolCoordinates } from './hosted.js';
31
33
  export * from './ui.js';
32
34
  export { BLEND_MODES, PAINTERLY_BLEND_MODES, isBlendMode, isPainterlyBlendMode, standardBlendMode, type BlendMode, type BlendModeInfo } from '../raster/blend.js';
@@ -11,7 +11,7 @@ import {
11
11
  mergeBrushPatch,
12
12
  multiColorSliders,
13
13
  patchBrushValue
14
- } from "../chunk-JGWVZPT6.js";
14
+ } from "../chunk-3XDSNIGF.js";
15
15
  import {
16
16
  createStickerTools,
17
17
  describeStickerRef
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  BRUSH_PREVIEW_DARK_PAPER,
21
21
  brushPreviewsOnDark
22
- } from "../chunk-EYSHTYLG.js";
22
+ } from "../chunk-LETF75CA.js";
23
23
  import {
24
24
  toVideoFfmpeg
25
25
  } from "../chunk-MWUZCEFT.js";
@@ -34,7 +34,7 @@ import {
34
34
  resolveExternalAssets,
35
35
  serializeLayers,
36
36
  serializeLayersExternal
37
- } from "../chunk-FGJHJIMN.js";
37
+ } from "../chunk-74ODPZLX.js";
38
38
  import "../chunk-ADMRNWR7.js";
39
39
  import "../chunk-WJ4WQJCC.js";
40
40
  import {
@@ -60,7 +60,7 @@ import "../chunk-ZIF4UJJ6.js";
60
60
  import {
61
61
  createPigmentTools,
62
62
  pointerPressure
63
- } from "../chunk-7UKFXXWL.js";
63
+ } from "../chunk-HVO6XCI4.js";
64
64
  import "../chunk-VWEWY4FY.js";
65
65
  import "../chunk-HVQVHUTV.js";
66
66
  import {
@@ -85,6 +85,7 @@ import {
85
85
  PaintWetLayers,
86
86
  PenDabWalker,
87
87
  PenSmoother,
88
+ TipGrain,
88
89
  WET_GPU_MODES,
89
90
  WET_MEDIUM_PARAMS,
90
91
  WetStrokeDriver,
@@ -112,13 +113,14 @@ import {
112
113
  toStrokePoint,
113
114
  wetPaperInput,
114
115
  wetPigmentOf
115
- } from "../chunk-4IDUYB45.js";
116
+ } from "../chunk-UVTO3D4E.js";
116
117
  import {
117
118
  BRUSH_ID,
118
119
  BRUSH_PRESETS,
119
120
  BRUSH_ROTATIONS,
120
121
  BUILTIN_BRUSHES,
121
122
  BrushLibraryError,
123
+ DAB_FLIPS,
122
124
  DEFAULT_BEHAVIOR,
123
125
  DEFAULT_BRISTLE,
124
126
  DEFAULT_BRUSH_ID,
@@ -129,6 +131,7 @@ import {
129
131
  DEFAULT_PARTICLE,
130
132
  DEFAULT_SPRAY,
131
133
  DEFAULT_STICKER,
134
+ DEFAULT_TIP_GRAIN,
132
135
  DEFAULT_WET_BRUSH,
133
136
  DOT_ROTATIONS,
134
137
  DOT_TEXTURE_ORDERS,
@@ -142,12 +145,17 @@ import {
142
145
  PARTICLE_KINDS,
143
146
  PARTICLE_RENDERS,
144
147
  RIBBON_PAIRS,
148
+ TIP_GRAIN_MODES,
145
149
  WET_MEDIA,
146
150
  WET_TOOLS,
151
+ brushSourceIdOf,
152
+ brushSourceIds,
147
153
  brushTakesPaint,
148
154
  brushToJSON,
149
155
  closest,
150
156
  createBrushLibrary,
157
+ decodeBrushSource,
158
+ isBrushSourceRef,
151
159
  isWetBrush,
152
160
  isWetCapable,
153
161
  migrateBrushV1,
@@ -165,9 +173,10 @@ import {
165
173
  normalizeWetBrush,
166
174
  paintToMultiColor,
167
175
  parseBrushLibrary,
176
+ readBrushSource,
168
177
  resolveBrush,
169
178
  usesPaintExchange
170
- } from "../chunk-AGNUD5BI.js";
179
+ } from "../chunk-KZI3YWKW.js";
171
180
  import {
172
181
  STAMP_KINDS,
173
182
  StickersSlot,
@@ -215,18 +224,14 @@ import {
215
224
  pigmentOf,
216
225
  wetSupport
217
226
  } from "../chunk-GSTSZPSR.js";
218
- import {
219
- GRAIN_PATTERNS,
220
- GRAIN_TILE,
221
- PaperGrain,
222
- grainCatch,
223
- grainHeights
224
- } from "../chunk-TXFWZ5ZJ.js";
227
+ import "../chunk-W6JDUXP3.js";
225
228
  import {
226
229
  CoverageMap,
227
230
  DEFAULT_PAINT_GRADIENT,
228
231
  DEFAULT_PAINT_GRADIENT_MAPPING,
229
232
  EMPTY_MIXER_LOAD,
233
+ GRAIN_PATTERNS,
234
+ GRAIN_TILE,
230
235
  MAX_BRUSH_SIZE,
231
236
  PAINT_COLOR_JSON_SCHEMA,
232
237
  PAINT_GRADIENT_FULL_SPEED,
@@ -243,6 +248,7 @@ import {
243
248
  PaintExchange,
244
249
  PaintGradientSampler,
245
250
  PaintSourceError,
251
+ PaperGrain,
246
252
  SmudgeTip,
247
253
  TIP_GRADIENT_STEPS,
248
254
  TileMedium,
@@ -251,6 +257,8 @@ import {
251
257
  cleanMixer,
252
258
  dabRect,
253
259
  exchangeRates,
260
+ grainCatch,
261
+ grainHeights,
254
262
  hexToPaintRgb,
255
263
  isGradientPaint,
256
264
  loadMixer,
@@ -267,7 +275,7 @@ import {
267
275
  scalePaintGradient,
268
276
  solidPaint,
269
277
  updatePaintGradientStop
270
- } from "../chunk-GCVFEPJG.js";
278
+ } from "../chunk-ACTHSQOI.js";
271
279
  import {
272
280
  TileableNoise,
273
281
  hash2,
@@ -277,6 +285,14 @@ import {
277
285
  seedFromString
278
286
  } from "../chunk-4D6KP5BV.js";
279
287
  import "../chunk-ODBT55Q7.js";
288
+ import {
289
+ BLEND_MODES,
290
+ PAINTERLY_BLEND_MODES,
291
+ isBlendMode,
292
+ isPainterlyBlendMode,
293
+ mixPigments,
294
+ standardBlendMode
295
+ } from "../chunk-IRSTHLQQ.js";
280
296
  import {
281
297
  base64ToBytes,
282
298
  bytesToBase64,
@@ -286,14 +302,6 @@ import {
286
302
  parseDataUrl,
287
303
  pngDataUrl
288
304
  } from "../chunk-OTTT5N3K.js";
289
- import {
290
- BLEND_MODES,
291
- PAINTERLY_BLEND_MODES,
292
- isBlendMode,
293
- isPainterlyBlendMode,
294
- mixPigments,
295
- standardBlendMode
296
- } from "../chunk-IRSTHLQQ.js";
297
305
  import {
298
306
  TILE,
299
307
  TiledSurface,
@@ -3089,7 +3097,7 @@ function thumbnailStroke(width, height) {
3089
3097
  }
3090
3098
  return pts;
3091
3099
  }
3092
- function paintTestStroke(surface, brush, paper, color, width, height) {
3100
+ function paintTestStroke(surface, brush, paper, color, width, height, stampArt) {
3093
3101
  const [r, g, b] = hexToRgb8(color);
3094
3102
  let result = surface;
3095
3103
  let thick = null;
@@ -3121,7 +3129,8 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
3121
3129
  mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),
3122
3130
  thick: brush.engine === "impasto" ? { channels: emptyThickChannels() } : void 0,
3123
3131
  sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0,
3124
- light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null
3132
+ light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null,
3133
+ stampArt
3125
3134
  }
3126
3135
  );
3127
3136
  session.push(thumbnailStroke(width, height));
@@ -3146,7 +3155,7 @@ function renderBrushThumbnail(brush, options) {
3146
3155
  }
3147
3156
  let light = null;
3148
3157
  if (b.engine === "wet") surface = paintWetTestStroke(b, options.paper, options.color, width, height);
3149
- else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height));
3158
+ else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height, options.stampArt));
3150
3159
  const layer = { type: "pixel", id: "thumbnail", name: "thumbnail", visible: true, opacity: 1, blend: "normal", surface, offset: { x: 0, y: 0 } };
3151
3160
  const paper = light && brushPreviewsOnDark(b) ? { ...options.paper, color: BRUSH_PREVIEW_DARK_PAPER } : options.paper;
3152
3161
  const spill = light ? computeSpill(surface, light, { radius: Math.max(4, height / 6), strength: 1 }) : null;
@@ -3736,6 +3745,59 @@ function samplePath(d, options = {}) {
3736
3745
  return out;
3737
3746
  }
3738
3747
 
3748
+ // src/paint-editor/brush-source-art.ts
3749
+ var BrushSourceArtCache = class {
3750
+ sources = /* @__PURE__ */ new Map();
3751
+ art = /* @__PURE__ */ new Map();
3752
+ /** Adds or replaces sources (a replaced image is decoded again). */
3753
+ set(sources) {
3754
+ for (const s of sources) {
3755
+ const was = this.sources.get(s.id);
3756
+ if (was && was.image === s.image) continue;
3757
+ this.sources.set(s.id, s);
3758
+ this.art.delete(s.id);
3759
+ }
3760
+ }
3761
+ /** The source of an id, if known. */
3762
+ source(id) {
3763
+ return this.sources.get(id);
3764
+ }
3765
+ /** Every known source. */
3766
+ all() {
3767
+ return [...this.sources.values()];
3768
+ }
3769
+ /** The art of a `source:<id>` ref (alpha = the mask), or `undefined` when the source is unknown or its image can't be read. */
3770
+ get = (ref2) => {
3771
+ const id = brushSourceIdOf(ref2);
3772
+ if (id === null) return void 0;
3773
+ if (this.art.has(id)) return this.art.get(id) ?? void 0;
3774
+ const source = this.sources.get(id);
3775
+ if (!source) return void 0;
3776
+ let art = null;
3777
+ try {
3778
+ const { width, height, mask } = decodeBrushSource(source);
3779
+ const data = new Uint8ClampedArray(width * height * 4);
3780
+ for (let i = 0; i < mask.length; i++) {
3781
+ data[i * 4] = data[i * 4 + 1] = data[i * 4 + 2] = 255;
3782
+ data[i * 4 + 3] = mask[i];
3783
+ }
3784
+ art = { width, height, data, tintable: true };
3785
+ } catch {
3786
+ art = null;
3787
+ }
3788
+ this.art.set(id, art);
3789
+ return art ?? void 0;
3790
+ };
3791
+ /** A lookup that reads sources from here and every other ref from `stickers`. */
3792
+ with(stickers) {
3793
+ return (ref2, variant) => isBrushSourceRef(ref2) ? this.get(ref2) : stickers(ref2, variant);
3794
+ }
3795
+ };
3796
+ function engineBrushSourceIds(brush) {
3797
+ const refs = [brush.stamp, brush.tipGrain?.source, ...brush.particle?.dotTextures ?? []];
3798
+ return [...new Set(refs.filter((r) => !!r && isBrushSourceRef(r)).map((r) => brushSourceIdOf(r)))];
3799
+ }
3800
+
3739
3801
  // src/paint-editor/live-strokes.ts
3740
3802
  var LIVE_STROKE_ENGINES = ["dab", "particle", "bristle", "mixer"];
3741
3803
  function liveStrokeRefusal(brush) {
@@ -4445,6 +4507,10 @@ var PaintEditorController = class _PaintEditorController {
4445
4507
  /** The sticker library slot (see `setStickersProvider`). */
4446
4508
  stickers = new StickersSlot(editorStickers());
4447
4509
  stickerArt = new StickerArtCache();
4510
+ /** Image sources strokes draw with: the brush library's and the open painting's (saved with it). */
4511
+ brushSources = new BrushSourceArtCache();
4512
+ /** Art for stamp tips and tip grain: image sources from `brushSources`, stickers from the sticker library. */
4513
+ stampArt = (ref2, variant) => isBrushSourceRef(ref2) ? this.brushSources.get(ref2) : this.stickerArt.get(ref2, variant);
4448
4514
  rasterizeSticker;
4449
4515
  effectsRenderer = new PaintEffectsRenderer();
4450
4516
  paperEffectNodeCache = null;
@@ -5808,7 +5874,10 @@ var PaintEditorController = class _PaintEditorController {
5808
5874
  }
5809
5875
  /** Reads the library again (after a change anywhere it is shared). */
5810
5876
  refreshBrushes() {
5811
- const run = this.brushLibrary.list().then((entries) => {
5877
+ const library = this.brushLibrary;
5878
+ const sources = typeof library.listSources === "function" ? library.listSources().catch(() => []) : Promise.resolve([]);
5879
+ const run = Promise.all([library.list(), sources]).then(([entries, list]) => {
5880
+ this.brushSources.set(list);
5812
5881
  this.brushEntries = entries.map((e) => libraryBrush(e.brush, e.builtin));
5813
5882
  this.library = null;
5814
5883
  const pending = this.pendingBrushId;
@@ -5955,7 +6024,7 @@ var PaintEditorController = class _PaintEditorController {
5955
6024
  /** A thumbnail of a brush: a test stroke on the current paper. */
5956
6025
  brushThumbnail(id, options = {}) {
5957
6026
  const b = id === this.brush.id ? this.brush : this.requireBrush(id);
5958
- return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
6027
+ return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color, stampArt: this.stampArt });
5959
6028
  }
5960
6029
  // --- Colour --------------------------------------------------------------------
5961
6030
  colorScheme;
@@ -6258,7 +6327,7 @@ var PaintEditorController = class _PaintEditorController {
6258
6327
  // Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
6259
6328
  light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
6260
6329
  sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
6261
- stampArt: this.stickerArt.get,
6330
+ stampArt: this.stampArt,
6262
6331
  paint: effect ? null : this.strokeGradient(init, brush, color),
6263
6332
  effect
6264
6333
  });
@@ -6636,6 +6705,7 @@ var PaintEditorController = class _PaintEditorController {
6636
6705
  if (w > 16384 || h > 16384) throw new PaintEditorError(`At scale ${scale} the canvas would be ${w}\xD7${h}; the limit is 16384 px per side. Use a smaller scale.`);
6637
6706
  const target = new _PaintEditorController({ width: w, height: h, dpi: dpi * scale, brushLibrary: this.brushLibrary, wet: this.wetOptions });
6638
6707
  target.brushEntries = this.brushEntries;
6708
+ target.brushSources.set(this.brushSources.all());
6639
6709
  const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };
6640
6710
  target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });
6641
6711
  target.lighting = { ...this.lighting };
@@ -7074,6 +7144,11 @@ var PaintEditorController = class _PaintEditorController {
7074
7144
  for (const [id, list] of Object.entries(this.doc.liveStrokes ?? {})) if (findLayer(this.doc.layers, id)) liveStrokes[id] = list.map(strokeToJSON);
7075
7145
  if (Object.keys(liveStrokes).length) ext.liveStrokes = liveStrokes;
7076
7146
  else delete ext.liveStrokes;
7147
+ const strokes = [...this.doc.strokes, ...Object.values(this.doc.liveStrokes ?? {}).flat()];
7148
+ const sourceIds = [...new Set(strokes.flatMap((st) => engineBrushSourceIds(st.brush)))];
7149
+ const sources = sourceIds.map((id) => this.brushSources.source(id)).filter((x) => x !== void 0);
7150
+ if (sources.length) ext.brushSources = sources.map((x) => ({ ...x }));
7151
+ else delete ext.brushSources;
7077
7152
  const light = {};
7078
7153
  for (const [id, t] of Object.entries(this.doc.light ?? {})) {
7079
7154
  if (!findLayer(this.doc.layers, id)) continue;
@@ -7096,6 +7171,7 @@ var PaintEditorController = class _PaintEditorController {
7096
7171
  }
7097
7172
  /** Reads `layerChannels` and `lighting` from loaded extensions into the document. */
7098
7173
  readThick(state, ext, problems) {
7174
+ if (Array.isArray(ext.brushSources)) this.brushSources.set(ext.brushSources.map((x, i) => readBrushSource(x, `brushSources[${i}]`, problems)).filter((x) => x !== null));
7099
7175
  this.lighting = normalizeLighting(ext.lighting ?? {}, DEFAULT_LIGHTING);
7100
7176
  this.litCaches.clear();
7101
7177
  if (ext.stickers && typeof ext.stickers === "object") {
@@ -7214,6 +7290,7 @@ var PaintEditorController = class _PaintEditorController {
7214
7290
  delete this.extensions.lighting;
7215
7291
  delete this.extensions.stickers;
7216
7292
  delete this.extensions.liveStrokes;
7293
+ delete this.extensions.brushSources;
7217
7294
  delete this.extensions.effectLayers;
7218
7295
  delete this.extensions.paperEffect;
7219
7296
  this.viewCompositor.clear();
@@ -10691,11 +10768,23 @@ function strokeSummary(s, withPoints, live) {
10691
10768
  }
10692
10769
 
10693
10770
  // src/paint-editor/brush-library-tools.ts
10694
- function paintBrushPreviewRenderer(paper = () => DEFAULT_PAPER) {
10695
- const render = (brush, { width, height, color }) => {
10696
- const t = renderBrushThumbnail(resolveBrush(brush), { paper: paper(), width, height, color: color.toLowerCase() });
10771
+ function paintBrushPreviewRenderer(paper = () => DEFAULT_PAPER, options = {}) {
10772
+ const sources = new BrushSourceArtCache();
10773
+ const draw = (brush, { width, height, color }) => {
10774
+ const stickers = options.stampArt?.();
10775
+ const stampArt = sources.with(stickers ?? (() => void 0));
10776
+ const t = renderBrushThumbnail(resolveBrush(brush), { paper: paper(), width, height, color: color.toLowerCase(), stampArt });
10697
10777
  return { data: bytesToBase64(encodePng(t.width, t.height, t.data)), mimeType: "image/png" };
10698
10778
  };
10779
+ const render = (brush, size) => {
10780
+ const ids = brushSourceIds(brush);
10781
+ if (!ids.length || !options.library) return draw(brush, size);
10782
+ const library = options.library;
10783
+ return Promise.all(ids.map((id) => library.getSource(id))).then((found) => {
10784
+ sources.set(found.filter((s) => s !== null));
10785
+ return draw(brush, size);
10786
+ });
10787
+ };
10699
10788
  return Object.defineProperty(render, "paper", { get: () => paper().color, enumerable: true });
10700
10789
  }
10701
10790
  function createPaintBrushLibraryToolProvider(controller, options = {}) {
@@ -10703,7 +10792,7 @@ function createPaintBrushLibraryToolProvider(controller, options = {}) {
10703
10792
  profile: PAINT_PROFILE,
10704
10793
  stickers: () => controller.stickers.provider,
10705
10794
  ...options,
10706
- renderPreview: paintBrushPreviewRenderer(() => controller.getState().document.paper)
10795
+ renderPreview: paintBrushPreviewRenderer(() => controller.getState().document.paper, { library: controller.brushLibrary, stampArt: () => controller.stampArt })
10707
10796
  });
10708
10797
  }
10709
10798
 
@@ -12074,6 +12163,8 @@ export {
12074
12163
  BRUSH_SLIDERS,
12075
12164
  BUILTIN_BRUSHES,
12076
12165
  BristleStroke,
12166
+ BrushSourceArtCache,
12167
+ DAB_FLIPS,
12077
12168
  DEFAULT_BACKLIGHT_COLOR,
12078
12169
  DEFAULT_BEHAVIOR,
12079
12170
  DEFAULT_BRISTLE,
@@ -12092,6 +12183,7 @@ export {
12092
12183
  DEFAULT_SPRAY,
12093
12184
  DEFAULT_STICKER,
12094
12185
  DEFAULT_SWATCHES,
12186
+ DEFAULT_TIP_GRAIN,
12095
12187
  DEFAULT_VIEW,
12096
12188
  DEFAULT_WET_BRUSH,
12097
12189
  DEFAULT_WET_PARAMS,
@@ -12197,9 +12289,11 @@ export {
12197
12289
  StickerArtCache,
12198
12290
  StickerBrowser,
12199
12291
  StickerSpray,
12292
+ TIP_GRAIN_MODES,
12200
12293
  TRACING_NODE_ID,
12201
12294
  ThickStrokeDriver,
12202
12295
  TileableNoise,
12296
+ TipGrain,
12203
12297
  WET_FLOW_KINDS,
12204
12298
  WET_GPU_MODES,
12205
12299
  WET_GPU_OPTIONS,
@@ -12264,6 +12358,7 @@ export {
12264
12358
  effectPaintSize,
12265
12359
  emptiedLayers,
12266
12360
  emptyThickChannels,
12361
+ engineBrushSourceIds,
12267
12362
  fitZoom,
12268
12363
  grainCatch,
12269
12364
  grainHeights,
@@ -10,11 +10,12 @@ import type { StickerArtLookup } from './sticker-art.cjs';
10
10
  /** Coverage 0..1 of a stamp at (u, v) of its unit square (−1..1, y down); `seed` varies shapes with random parts. */
11
11
  export type StampSampler = (u: number, v: number, seed: number) => number;
12
12
  /**
13
- * The sampler of a stamp ref: an editor stamp's coverage, or the alpha of a sticker's loaded art
14
- * (bilinear, centred in the unit square, aspect kept). `null` when the art is not loaded (or the
15
- * ref is not a sticker): the caller paints the round tip instead.
13
+ * The sampler of a stamp ref: an editor stamp's coverage, or the alpha of a sticker's or an image
14
+ * source's loaded art (bilinear, centred in the unit square, aspect kept). `null` when the art is
15
+ * not loaded (or the ref is not a sticker): the caller paints the round tip instead. `hardness`
16
+ * below 1 softens an image source's edges.
16
17
  */
17
- export declare function stampSampler(ref: string, art?: StickerArtLookup): StampSampler | null;
18
+ export declare function stampSampler(ref: string, art?: StickerArtLookup, hardness?: number): StampSampler | null;
18
19
  /** A dab of a stamp tip: centre, radius (px), roundness squashing it across, angle (radians, counter-clockwise) and flow. */
19
20
  export interface StampDab {
20
21
  x: number;
@@ -23,6 +24,8 @@ export interface StampDab {
23
24
  roundness: number;
24
25
  angle: number;
25
26
  alpha: number;
27
+ /** Mirrored across the tip's x axis (bit 1) and y axis (bit 2). */
28
+ flip?: number;
26
29
  }
27
30
  /** The pixel rectangle a stamp dab can touch. */
28
31
  export declare function stampDabRect(d: StampDab): CropBox;
@@ -32,7 +35,7 @@ export declare function stampDabRect(d: StampDab): CropBox;
32
35
  * Small dabs are supersampled.
33
36
  */
34
37
  export declare function rasterizeStampDab(d: StampDab, sample: StampSampler, seed: number, rect: CropBox, out: Float32Array): void;
35
- /** The sticker refs a brush lays that need loaded art (provider stickers; editor stamps need none). */
38
+ /** The sticker refs a brush lays that need loaded art (provider stickers; editor stamps and image sources need none). */
36
39
  export declare function brushStickerRefs(brush: {
37
40
  engine: string;
38
41
  stamp: string | null;
@@ -10,11 +10,12 @@ import type { StickerArtLookup } from './sticker-art.js';
10
10
  /** Coverage 0..1 of a stamp at (u, v) of its unit square (−1..1, y down); `seed` varies shapes with random parts. */
11
11
  export type StampSampler = (u: number, v: number, seed: number) => number;
12
12
  /**
13
- * The sampler of a stamp ref: an editor stamp's coverage, or the alpha of a sticker's loaded art
14
- * (bilinear, centred in the unit square, aspect kept). `null` when the art is not loaded (or the
15
- * ref is not a sticker): the caller paints the round tip instead.
13
+ * The sampler of a stamp ref: an editor stamp's coverage, or the alpha of a sticker's or an image
14
+ * source's loaded art (bilinear, centred in the unit square, aspect kept). `null` when the art is
15
+ * not loaded (or the ref is not a sticker): the caller paints the round tip instead. `hardness`
16
+ * below 1 softens an image source's edges.
16
17
  */
17
- export declare function stampSampler(ref: string, art?: StickerArtLookup): StampSampler | null;
18
+ export declare function stampSampler(ref: string, art?: StickerArtLookup, hardness?: number): StampSampler | null;
18
19
  /** A dab of a stamp tip: centre, radius (px), roundness squashing it across, angle (radians, counter-clockwise) and flow. */
19
20
  export interface StampDab {
20
21
  x: number;
@@ -23,6 +24,8 @@ export interface StampDab {
23
24
  roundness: number;
24
25
  angle: number;
25
26
  alpha: number;
27
+ /** Mirrored across the tip's x axis (bit 1) and y axis (bit 2). */
28
+ flip?: number;
26
29
  }
27
30
  /** The pixel rectangle a stamp dab can touch. */
28
31
  export declare function stampDabRect(d: StampDab): CropBox;
@@ -32,7 +35,7 @@ export declare function stampDabRect(d: StampDab): CropBox;
32
35
  * Small dabs are supersampled.
33
36
  */
34
37
  export declare function rasterizeStampDab(d: StampDab, sample: StampSampler, seed: number, rect: CropBox, out: Float32Array): void;
35
- /** The sticker refs a brush lays that need loaded art (provider stickers; editor stamps need none). */
38
+ /** The sticker refs a brush lays that need loaded art (provider stickers; editor stamps and image sources need none). */
36
39
  export declare function brushStickerRefs(brush: {
37
40
  engine: string;
38
41
  stamp: string | null;
@@ -6,6 +6,7 @@
6
6
  import { type PaintBrush } from './brushes.cjs';
7
7
  import type { PenPoint } from './engine.cjs';
8
8
  import { type PaperSettings } from './paper.cjs';
9
+ import type { StickerArtLookup } from './sticker-art.cjs';
9
10
  export interface BrushThumbnail {
10
11
  width: number;
11
12
  height: number;
@@ -14,9 +15,14 @@ export interface BrushThumbnail {
14
15
  }
15
16
  /** The test stroke's samples for a `width × height` thumbnail. */
16
17
  export declare function thumbnailStroke(width: number, height: number): PenPoint[];
18
+ /**
19
+ * A sample stroke with `brush` on `paper`. `stampArt` draws stamp tips of stickers and image
20
+ * sources, and image tip grain (without it they paint round, and without the grain).
21
+ */
17
22
  export declare function renderBrushThumbnail(brush: PaintBrush, options: {
18
23
  paper: PaperSettings;
19
24
  width?: number;
20
25
  height?: number;
21
26
  color: string;
27
+ stampArt?: StickerArtLookup;
22
28
  }): BrushThumbnail;
@@ -6,6 +6,7 @@
6
6
  import { type PaintBrush } from './brushes.js';
7
7
  import type { PenPoint } from './engine.js';
8
8
  import { type PaperSettings } from './paper.js';
9
+ import type { StickerArtLookup } from './sticker-art.js';
9
10
  export interface BrushThumbnail {
10
11
  width: number;
11
12
  height: number;
@@ -14,9 +15,14 @@ export interface BrushThumbnail {
14
15
  }
15
16
  /** The test stroke's samples for a `width × height` thumbnail. */
16
17
  export declare function thumbnailStroke(width: number, height: number): PenPoint[];
18
+ /**
19
+ * A sample stroke with `brush` on `paper`. `stampArt` draws stamp tips of stickers and image
20
+ * sources, and image tip grain (without it they paint round, and without the grain).
21
+ */
17
22
  export declare function renderBrushThumbnail(brush: PaintBrush, options: {
18
23
  paper: PaperSettings;
19
24
  width?: number;
20
25
  height?: number;
21
26
  color: string;
27
+ stampArt?: StickerArtLookup;
22
28
  }): BrushThumbnail;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tip grain: texture inside each dab (Procreate's grain source), separate from the paper's grain.
3
+ * The texture is a built-in grain pattern or an image source (`source:<id>`, light parts are
4
+ * peaks), tiled. `texturised` grain stays put on the canvas; `moving` grain is read in the tip's
5
+ * own frame, offset by the distance travelled, so it rolls along the stroke and turns with the tip.
6
+ *
7
+ * A dab's coverage is multiplied by `1 − depth × (1 − height)`. Everything is a function of the
8
+ * dab (position, angle, travel), so replays and repaints at another scale lay the same grain.
9
+ */
10
+ import type { RasterRect } from '../raster/surface.cjs';
11
+ import type { TipGrainSettings } from './brush-model.cjs';
12
+ import type { StickerArtLookup } from './sticker-art.cjs';
13
+ /** What the grain needs of a dab. */
14
+ export interface TipGrainDab {
15
+ x: number;
16
+ y: number;
17
+ /** Radians, counter-clockwise with y up. */
18
+ angle: number;
19
+ /** Path length travelled to the dab (px). */
20
+ travel: number;
21
+ }
22
+ /** A brush's tip grain, ready to apply to dabs. */
23
+ export declare class TipGrain {
24
+ private readonly field;
25
+ private readonly settings;
26
+ private constructor();
27
+ /**
28
+ * The grain for `settings`, or `null` when there is none to apply: no depth, or an image source
29
+ * whose art is not loaded (the dabs then paint without it).
30
+ */
31
+ static create(settings: TipGrainSettings | undefined, art?: StickerArtLookup): TipGrain | null;
32
+ /** Multiplies the dab's coverage `vals` (over `rect`) by the grain. */
33
+ apply(vals: Float32Array, rect: RasterRect, d: TipGrainDab): void;
34
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tip grain: texture inside each dab (Procreate's grain source), separate from the paper's grain.
3
+ * The texture is a built-in grain pattern or an image source (`source:<id>`, light parts are
4
+ * peaks), tiled. `texturised` grain stays put on the canvas; `moving` grain is read in the tip's
5
+ * own frame, offset by the distance travelled, so it rolls along the stroke and turns with the tip.
6
+ *
7
+ * A dab's coverage is multiplied by `1 − depth × (1 − height)`. Everything is a function of the
8
+ * dab (position, angle, travel), so replays and repaints at another scale lay the same grain.
9
+ */
10
+ import type { RasterRect } from '../raster/surface.js';
11
+ import type { TipGrainSettings } from './brush-model.js';
12
+ import type { StickerArtLookup } from './sticker-art.js';
13
+ /** What the grain needs of a dab. */
14
+ export interface TipGrainDab {
15
+ x: number;
16
+ y: number;
17
+ /** Radians, counter-clockwise with y up. */
18
+ angle: number;
19
+ /** Path length travelled to the dab (px). */
20
+ travel: number;
21
+ }
22
+ /** A brush's tip grain, ready to apply to dabs. */
23
+ export declare class TipGrain {
24
+ private readonly field;
25
+ private readonly settings;
26
+ private constructor();
27
+ /**
28
+ * The grain for `settings`, or `null` when there is none to apply: no depth, or an image source
29
+ * whose art is not loaded (the dabs then paint without it).
30
+ */
31
+ static create(settings: TipGrainSettings | undefined, art?: StickerArtLookup): TipGrain | null;
32
+ /** Multiplies the dab's coverage `vals` (over `rect`) by the grain. */
33
+ apply(vals: Float32Array, rect: RasterRect, d: TipGrainDab): void;
34
+ }
@@ -79,7 +79,6 @@ var _chunkK6PXS6DOcjs = require('../chunk-K6PXS6DO.cjs');
79
79
 
80
80
 
81
81
 
82
- var _chunkW5M3J7PVcjs = require('../chunk-W5M3J7PV.cjs');
83
82
 
84
83
 
85
84
 
@@ -114,6 +113,7 @@ var _chunkW5M3J7PVcjs = require('../chunk-W5M3J7PV.cjs');
114
113
 
115
114
 
116
115
 
116
+ var _chunk2VX55NFMcjs = require('../chunk-2VX55NFM.cjs');
117
117
 
118
118
 
119
119
 
@@ -122,7 +122,7 @@ var _chunkW5M3J7PVcjs = require('../chunk-W5M3J7PV.cjs');
122
122
 
123
123
 
124
124
 
125
- var _chunk2VX55NFMcjs = require('../chunk-2VX55NFM.cjs');
125
+ var _chunkW5M3J7PVcjs = require('../chunk-W5M3J7PV.cjs');
126
126
 
127
127
 
128
128
 
@@ -71,15 +71,6 @@ import {
71
71
  DIRECT_GAUSSIAN_MAX_SIGMA,
72
72
  gaussianBlur
73
73
  } from "../chunk-ZIF4UJJ6.js";
74
- import {
75
- base64ToBytes,
76
- bytesToBase64,
77
- decodePng,
78
- encodePng,
79
- isPng,
80
- parseDataUrl,
81
- pngDataUrl
82
- } from "../chunk-OTTT5N3K.js";
83
74
  import {
84
75
  BIT_DEPTHS,
85
76
  BLEND_MODES,
@@ -123,6 +114,15 @@ import {
123
114
  to8From32,
124
115
  writeDeepRegion
125
116
  } from "../chunk-IRSTHLQQ.js";
117
+ import {
118
+ base64ToBytes,
119
+ bytesToBase64,
120
+ decodePng,
121
+ encodePng,
122
+ isPng,
123
+ parseDataUrl,
124
+ pngDataUrl
125
+ } from "../chunk-OTTT5N3K.js";
126
126
  import {
127
127
  SurfaceDraft,
128
128
  TILE,