@pixerate/editor 0.267.0 → 0.268.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/animation-editor/index.cjs +62 -62
  2. package/dist/animation-editor/index.js +6 -6
  3. package/dist/brushes/index.cjs +198 -72
  4. package/dist/brushes/index.d.cts +4 -3
  5. package/dist/brushes/index.d.ts +4 -3
  6. package/dist/brushes/index.js +136 -10
  7. package/dist/brushes/library.d.cts +27 -1
  8. package/dist/brushes/library.d.ts +27 -1
  9. package/dist/brushes/profiles.d.cts +4 -0
  10. package/dist/brushes/profiles.d.ts +4 -0
  11. package/dist/brushes/schema.d.cts +4 -0
  12. package/dist/brushes/schema.d.ts +4 -0
  13. package/dist/brushes/sources.d.cts +114 -0
  14. package/dist/brushes/sources.d.ts +114 -0
  15. package/dist/brushes/types.d.cts +37 -5
  16. package/dist/brushes/types.d.ts +37 -5
  17. package/dist/brushes/ui.d.cts +40 -4
  18. package/dist/brushes/ui.d.ts +40 -4
  19. package/dist/{brushstrokeRender-NLFPEBGI.js → brushstrokeRender-A727TTNQ.js} +6 -6
  20. package/dist/{brushstrokeRender-L7JYRR2R.cjs → brushstrokeRender-WR2N5HHY.cjs} +22 -22
  21. package/dist/canvas/index.cjs +13 -13
  22. package/dist/canvas/index.js +1 -1
  23. package/dist/{chunk-J2TNIZ55.cjs → chunk-2WOGXEDX.cjs} +1 -1
  24. package/dist/{chunk-JGWVZPT6.js → chunk-3XDSNIGF.js} +72 -5
  25. package/dist/{chunk-EXB2EGBE.cjs → chunk-62MFUHSI.cjs} +16 -16
  26. package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
  27. package/dist/{chunk-EF7LZAXQ.js → chunk-BAZMCFVL.js} +1 -1
  28. package/dist/{chunk-HIO6E7LC.cjs → chunk-BPJSI4NT.cjs} +2 -2
  29. package/dist/{chunk-XVOP3CGQ.cjs → chunk-EU2ZZA6L.cjs} +393 -62
  30. package/dist/{chunk-YIC6MAJ2.js → chunk-HFY5LG5J.js} +1 -1
  31. package/dist/{chunk-7UKFXXWL.js → chunk-HVO6XCI4.js} +1 -1
  32. package/dist/{chunk-LHPEG5AU.cjs → chunk-J4GXP53R.cjs} +140 -91
  33. package/dist/{chunk-OKHKA43L.cjs → chunk-JBB3GDBB.cjs} +330 -216
  34. package/dist/{chunk-AGNUD5BI.js → chunk-KZI3YWKW.js} +351 -20
  35. package/dist/{chunk-FGJHJIMN.js → chunk-LDOD7VCT.js} +59 -10
  36. package/dist/{chunk-EYSHTYLG.js → chunk-LETF75CA.js} +1 -1
  37. package/dist/{chunk-FQPUF2W2.cjs → chunk-NIBFKXMD.cjs} +217 -35
  38. package/dist/{chunk-G7NHE2VD.cjs → chunk-NPWWSCWT.cjs} +35 -35
  39. package/dist/{chunk-OSEXT7JL.js → chunk-QNY7BDFP.js} +1 -1
  40. package/dist/{chunk-PXDIAYB6.cjs → chunk-STLTOISW.cjs} +34 -214
  41. package/dist/{chunk-4IDUYB45.js → chunk-UVTO3D4E.js} +130 -16
  42. package/dist/{chunk-TXFWZ5ZJ.js → chunk-W6JDUXP3.js} +4 -184
  43. package/dist/{chunk-LA5Y4ETA.cjs → chunk-YOVXQHA3.cjs} +23 -23
  44. package/dist/{chunk-DT4YQUGT.cjs → chunk-YXP6X3SY.cjs} +101 -34
  45. package/dist/image-editor/adjustment-layers.d.cts +8 -0
  46. package/dist/image-editor/adjustment-layers.d.ts +8 -0
  47. package/dist/image-editor/index.cjs +857 -853
  48. package/dist/image-editor/index.js +30 -26
  49. package/dist/paint/index.cjs +5 -5
  50. package/dist/paint/index.js +11 -11
  51. package/dist/paint-editor/brush-library-tools.d.cts +12 -2
  52. package/dist/paint-editor/brush-library-tools.d.ts +12 -2
  53. package/dist/paint-editor/brush-model.d.cts +29 -1
  54. package/dist/paint-editor/brush-model.d.ts +29 -1
  55. package/dist/paint-editor/brush-source-art.d.cts +32 -0
  56. package/dist/paint-editor/brush-source-art.d.ts +32 -0
  57. package/dist/paint-editor/controller.d.cts +6 -1
  58. package/dist/paint-editor/controller.d.ts +6 -1
  59. package/dist/paint-editor/dab-stroke.d.cts +2 -0
  60. package/dist/paint-editor/dab-stroke.d.ts +2 -0
  61. package/dist/paint-editor/engine.d.cts +11 -2
  62. package/dist/paint-editor/engine.d.ts +11 -2
  63. package/dist/paint-editor/index.cjs +576 -481
  64. package/dist/paint-editor/index.d.cts +3 -1
  65. package/dist/paint-editor/index.d.ts +3 -1
  66. package/dist/paint-editor/index.js +127 -32
  67. package/dist/paint-editor/stamp-tip.d.cts +8 -5
  68. package/dist/paint-editor/stamp-tip.d.ts +8 -5
  69. package/dist/paint-editor/thumbnails.d.cts +6 -0
  70. package/dist/paint-editor/thumbnails.d.ts +6 -0
  71. package/dist/paint-editor/tip-grain.d.cts +34 -0
  72. package/dist/paint-editor/tip-grain.d.ts +34 -0
  73. package/dist/raster/index.cjs +2 -2
  74. package/dist/raster/index.js +9 -9
  75. package/dist/vector/index.cjs +1002 -1002
  76. package/dist/vector/index.js +7 -7
  77. package/package.json +110 -1
  78. package/skills/pixerate-brushes/SKILL.md +35 -8
  79. package/skills/pixerate-brushes-design/SKILL.md +8 -1
  80. package/skills/pixerate-image-editor/SKILL.md +1 -1
  81. package/skills/pixerate-paint-editor/SKILL.md +6 -0
  82. package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
  83. package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Brush sources: the images a brush is made from, as Procreate's Shape and Grain sources. A
3
+ * source is a grey mask (255 paints, or is a grain peak; 0 is empty) kept as a greyscale PNG
4
+ * `data:` URL, so a library file or a saved painting carries its bytes alongside the brushes
5
+ * that name it.
6
+ *
7
+ * - A stamp tip lays a source with the ref `source:<id>` in `tip.stamps`, the same way it lays a
8
+ * sticker: shaped dabs, sprays and particle dots all take it.
9
+ * - `tip.grain.source` takes `source:<id>` as a grain texture (or one of the built-in grain
10
+ * patterns).
11
+ *
12
+ * `makeBrushSource` reads an uploaded image (PNG, any size up to `MAX_BRUSH_SOURCE_INPUT_PIXELS`),
13
+ * picks the channel that holds the marks and scales it to fit `MAX_BRUSH_SOURCE_SIZE`.
14
+ */
15
+ import type { Brush } from './types.js';
16
+ /** The ref prefix of a brush source: `source:<id>`. The sticker provider id `source` is reserved for it. */
17
+ export declare const BRUSH_SOURCE_PROVIDER = "source";
18
+ /** Longest side a source is kept at (px); larger images are scaled down when added. */
19
+ export declare const MAX_BRUSH_SOURCE_SIZE = 512;
20
+ /** Largest image `makeBrushSource` decodes (pixels). */
21
+ export declare const MAX_BRUSH_SOURCE_INPUT_PIXELS: number;
22
+ /** Most sources one library keeps. */
23
+ export declare const MAX_BRUSH_SOURCES = 500;
24
+ /**
25
+ * Which part of an image holds the marks: `alpha` (art on a transparent background), `darkness`
26
+ * (dark marks on white paper, a scan), `lightness` (white marks on black, as Procreate's shape
27
+ * sources) or `auto`: alpha when the image has any transparency, else darkness.
28
+ */
29
+ export type BrushSourceChannel = 'auto' | 'alpha' | 'darkness' | 'lightness';
30
+ export declare const BRUSH_SOURCE_CHANNELS: readonly BrushSourceChannel[];
31
+ /** A stored source. */
32
+ export interface BrushSource {
33
+ /** Lowercase letters, digits and dashes, up to 64 characters. */
34
+ id: string;
35
+ /** Display name, up to 80 characters. */
36
+ name: string;
37
+ /** Size of the mask in px (each 1..`MAX_BRUSH_SOURCE_SIZE`). */
38
+ width: number;
39
+ height: number;
40
+ /** The mask: a greyscale PNG `data:` URL, 255 paints. */
41
+ image: string;
42
+ /** The licence or terms it came with (a brush pack's notes), kept with the source. */
43
+ licence?: string;
44
+ /** Where it came from: a file or brush pack name. */
45
+ origin?: string;
46
+ }
47
+ /** What `makeBrushSource` reads. */
48
+ export interface BrushSourceInput {
49
+ /** Default: from the name. */
50
+ id?: string;
51
+ name: string;
52
+ /** A PNG as a `data:image/png;base64,…` URL, or its bytes. */
53
+ image: string | Uint8Array;
54
+ /** Default `auto`. */
55
+ channel?: BrushSourceChannel;
56
+ /** Swap painted and empty (default false). */
57
+ invert?: boolean;
58
+ licence?: string;
59
+ origin?: string;
60
+ }
61
+ /** A source without its image, for lists. */
62
+ export type BrushSourceSummary = Omit<BrushSource, 'image'> & {
63
+ ref: string;
64
+ };
65
+ /** A refusal; the message says what to do instead. */
66
+ export declare class BrushSourceError extends Error {
67
+ constructor(message: string);
68
+ }
69
+ /** `source:<id>`. */
70
+ export declare function brushSourceRef(id: string): string;
71
+ /** Whether a stamp or grain ref names a brush source. */
72
+ export declare function isBrushSourceRef(ref: unknown): ref is string;
73
+ /** The source id of a `source:<id>` ref, or `null` for any other ref. */
74
+ export declare function brushSourceIdOf(ref: string): string | null;
75
+ /** The ids of the sources a brush lays or textures with (its stamp tip and its grain). */
76
+ export declare function brushSourceIds(brush: Pick<Brush, 'tip'>): string[];
77
+ /** The list form of a source. */
78
+ export declare function summarizeBrushSource(s: BrushSource): BrushSourceSummary;
79
+ /** An id from a name: lowercase words joined by dashes. */
80
+ export declare function brushSourceIdFromName(name: string): string;
81
+ /**
82
+ * A source from a grey mask (255 paints), scaled to fit `MAX_BRUSH_SOURCE_SIZE`. Importers use it
83
+ * for tips they have decoded themselves. Throws when the mask is empty or the wrong size.
84
+ */
85
+ export declare function brushSourceFromMask(meta: {
86
+ id: string;
87
+ name: string;
88
+ licence?: string;
89
+ origin?: string;
90
+ }, width: number, height: number, mask: Uint8Array | Uint8ClampedArray): BrushSource;
91
+ /**
92
+ * A source from an uploaded image (see the module comment): its marks read from `channel`,
93
+ * inverted when asked, scaled to fit. Throws (a `BrushSourceError`) when the image is not a PNG,
94
+ * too large, or empty in that channel; `problems` hears what was changed (scaling, the channel
95
+ * `auto` picked).
96
+ */
97
+ export declare function makeBrushSource(input: BrushSourceInput, problems?: string[]): BrushSource;
98
+ /**
99
+ * A stored source read back (from a library file or a saved painting), or `null` with the reason
100
+ * in `problems`. The image is checked for its form, not decoded (`decodeBrushSource` does that
101
+ * when it is first painted with).
102
+ */
103
+ export declare function readBrushSource(raw: unknown, at: string, problems: string[]): BrushSource | null;
104
+ /** A decoded source: one byte per pixel, 255 paints. */
105
+ export interface DecodedBrushSource {
106
+ width: number;
107
+ height: number;
108
+ mask: Uint8ClampedArray;
109
+ }
110
+ /**
111
+ * The source's mask, decoded once per image (cached by its data, so a painting's copy and the
112
+ * library's share it). Throws when the image is not a valid PNG.
113
+ */
114
+ export declare function decodeBrushSource(source: Pick<BrushSource, 'image'>): DecodedBrushSource;
@@ -29,8 +29,8 @@ export type BrushTool = 'eraser' | 'blender' | 'smudge' | 'water' | 'dryer' | 'b
29
29
  export declare const BRUSH_TOOLS: readonly BrushTool[];
30
30
  /**
31
31
  * The tip's shape. `round`, `flat` and `chisel` are ellipses (their `roundness` and `angle` set
32
- * the ellipse; the name is for people); `bristle` is a tuft of bristles; `stamp` lays textured
33
- * stamps (with `marks` `objects` or `particles`).
32
+ * the ellipse; the name is for people); `bristle` is a tuft of bristles; `stamp` lays shaped
33
+ * dabs: an editor stamp, a sticker, or an image source (`source:<id>`, see `brushes/sources.ts`).
34
34
  */
35
35
  export type BrushTipShape = 'round' | 'flat' | 'chisel' | 'bristle' | 'stamp';
36
36
  export declare const BRUSH_TIP_SHAPES: readonly BrushTipShape[];
@@ -41,10 +41,36 @@ export declare const BRUSH_MARKS_KINDS: readonly BrushMarksKind[];
41
41
  export type BrushSurface = 'dry' | 'wet' | 'thick';
42
42
  export declare const BRUSH_SURFACES: readonly BrushSurface[];
43
43
  /**
44
- * A sticker a `stamp` tip lays: a sticker ref `provider:id` (`editor:star`, or a host library's
45
- * sticker). Objects marks take any sticker; particle marks only the editor's own stamps.
44
+ * What a `stamp` tip lays: a sticker ref `provider:id` (`editor:star`, or a host library's
45
+ * sticker) or an image source `source:<id>` from the brush library. Objects marks take only
46
+ * stickers (each mark stays a sticker).
46
47
  */
47
48
  export type BrushStamp = StickerRef;
49
+ /**
50
+ * How tip grain sits: `moving` rolls with the tip along the stroke (it turns with the tip, and
51
+ * every stroke shows it the same way); `texturised` stays put on the canvas, like a texture under
52
+ * the paint, so overlapping strokes show the same grain.
53
+ */
54
+ export type BrushGrainMode = 'moving' | 'texturised';
55
+ export declare const BRUSH_GRAIN_MODES: readonly BrushGrainMode[];
56
+ /** Mirror each dab at random across these axes of the tip. */
57
+ export type BrushFlip = 'none' | 'x' | 'y' | 'xy';
58
+ export declare const BRUSH_FLIPS: readonly BrushFlip[];
59
+ /** Texture inside the tip (Procreate's grain source), separate from the paper grain. */
60
+ export interface BrushTipGrain {
61
+ /** A built-in grain pattern (`rough`, `canvas`, `sanded`…) or an image source `source:<id>` (light parts are peaks). */
62
+ source: string;
63
+ /** Default `texturised`. */
64
+ mode?: BrushGrainMode;
65
+ /** Grain size: 1 is the pattern's or image's own size, 2 twice as coarse; 0.05..20 (default 1). */
66
+ scale?: number;
67
+ /** How much the grain takes out of the mark: 0 none .. 1 only the peaks paint (default 0.6). */
68
+ depth?: number;
69
+ /** Height contrast around the middle, 0..3 (default 1). */
70
+ contrast?: number;
71
+ /** Swap peaks and valleys (default false). */
72
+ invert?: boolean;
73
+ }
48
74
  /** The tuft of a `bristle` tip (each 0..1 unless noted). */
49
75
  export interface BrushBristles {
50
76
  /** Bristles in the tuft, 3..128 (default 32). */
@@ -81,8 +107,10 @@ export interface BrushTip {
81
107
  blend?: BlendMode;
82
108
  /** `bristle` tips: the tuft. */
83
109
  bristle?: BrushBristles;
84
- /** `stamp` tips: sticker refs (objects: any sticker, one picked at random per mark; particles: up to 4 editor stamps, in `marks.stampOrder`). */
110
+ /** `stamp` tips: sticker or source refs (objects: any sticker, one picked at random per mark; particles: up to 4, in `marks.stampOrder`; continuous dabs: the first). */
85
111
  stamps?: BrushStamp[];
112
+ /** Texture inside each dab (continuous and scatter marks). */
113
+ grain?: BrushTipGrain;
86
114
  }
87
115
  /** Dabs laid along the path (the default). */
88
116
  export interface BrushContinuousMarks {
@@ -185,6 +213,10 @@ export interface BrushDynamics {
185
213
  opacityJitter?: number;
186
214
  /** Random offset across the stroke in diameters, 0..4 (default 0). */
187
215
  scatter?: number;
216
+ /** Dabs laid at each spacing step, 1..16, each with its own jitter (default 1). */
217
+ count?: number;
218
+ /** Mirror each dab at random across these axes (default `none`): variety for image tips. */
219
+ flip?: BrushFlip;
188
220
  }
189
221
  /** A Mixer Brush's paint reservoir: the brush is loaded with colour and mixes what it picks up into it. */
190
222
  export interface BrushReservoir {
@@ -29,8 +29,8 @@ export type BrushTool = 'eraser' | 'blender' | 'smudge' | 'water' | 'dryer' | 'b
29
29
  export declare const BRUSH_TOOLS: readonly BrushTool[];
30
30
  /**
31
31
  * The tip's shape. `round`, `flat` and `chisel` are ellipses (their `roundness` and `angle` set
32
- * the ellipse; the name is for people); `bristle` is a tuft of bristles; `stamp` lays textured
33
- * stamps (with `marks` `objects` or `particles`).
32
+ * the ellipse; the name is for people); `bristle` is a tuft of bristles; `stamp` lays shaped
33
+ * dabs: an editor stamp, a sticker, or an image source (`source:<id>`, see `brushes/sources.ts`).
34
34
  */
35
35
  export type BrushTipShape = 'round' | 'flat' | 'chisel' | 'bristle' | 'stamp';
36
36
  export declare const BRUSH_TIP_SHAPES: readonly BrushTipShape[];
@@ -41,10 +41,36 @@ export declare const BRUSH_MARKS_KINDS: readonly BrushMarksKind[];
41
41
  export type BrushSurface = 'dry' | 'wet' | 'thick';
42
42
  export declare const BRUSH_SURFACES: readonly BrushSurface[];
43
43
  /**
44
- * A sticker a `stamp` tip lays: a sticker ref `provider:id` (`editor:star`, or a host library's
45
- * sticker). Objects marks take any sticker; particle marks only the editor's own stamps.
44
+ * What a `stamp` tip lays: a sticker ref `provider:id` (`editor:star`, or a host library's
45
+ * sticker) or an image source `source:<id>` from the brush library. Objects marks take only
46
+ * stickers (each mark stays a sticker).
46
47
  */
47
48
  export type BrushStamp = StickerRef;
49
+ /**
50
+ * How tip grain sits: `moving` rolls with the tip along the stroke (it turns with the tip, and
51
+ * every stroke shows it the same way); `texturised` stays put on the canvas, like a texture under
52
+ * the paint, so overlapping strokes show the same grain.
53
+ */
54
+ export type BrushGrainMode = 'moving' | 'texturised';
55
+ export declare const BRUSH_GRAIN_MODES: readonly BrushGrainMode[];
56
+ /** Mirror each dab at random across these axes of the tip. */
57
+ export type BrushFlip = 'none' | 'x' | 'y' | 'xy';
58
+ export declare const BRUSH_FLIPS: readonly BrushFlip[];
59
+ /** Texture inside the tip (Procreate's grain source), separate from the paper grain. */
60
+ export interface BrushTipGrain {
61
+ /** A built-in grain pattern (`rough`, `canvas`, `sanded`…) or an image source `source:<id>` (light parts are peaks). */
62
+ source: string;
63
+ /** Default `texturised`. */
64
+ mode?: BrushGrainMode;
65
+ /** Grain size: 1 is the pattern's or image's own size, 2 twice as coarse; 0.05..20 (default 1). */
66
+ scale?: number;
67
+ /** How much the grain takes out of the mark: 0 none .. 1 only the peaks paint (default 0.6). */
68
+ depth?: number;
69
+ /** Height contrast around the middle, 0..3 (default 1). */
70
+ contrast?: number;
71
+ /** Swap peaks and valleys (default false). */
72
+ invert?: boolean;
73
+ }
48
74
  /** The tuft of a `bristle` tip (each 0..1 unless noted). */
49
75
  export interface BrushBristles {
50
76
  /** Bristles in the tuft, 3..128 (default 32). */
@@ -81,8 +107,10 @@ export interface BrushTip {
81
107
  blend?: BlendMode;
82
108
  /** `bristle` tips: the tuft. */
83
109
  bristle?: BrushBristles;
84
- /** `stamp` tips: sticker refs (objects: any sticker, one picked at random per mark; particles: up to 4 editor stamps, in `marks.stampOrder`). */
110
+ /** `stamp` tips: sticker or source refs (objects: any sticker, one picked at random per mark; particles: up to 4, in `marks.stampOrder`; continuous dabs: the first). */
85
111
  stamps?: BrushStamp[];
112
+ /** Texture inside each dab (continuous and scatter marks). */
113
+ grain?: BrushTipGrain;
86
114
  }
87
115
  /** Dabs laid along the path (the default). */
88
116
  export interface BrushContinuousMarks {
@@ -185,6 +213,10 @@ export interface BrushDynamics {
185
213
  opacityJitter?: number;
186
214
  /** Random offset across the stroke in diameters, 0..4 (default 0). */
187
215
  scatter?: number;
216
+ /** Dabs laid at each spacing step, 1..16, each with its own jitter (default 1). */
217
+ count?: number;
218
+ /** Mirror each dab at random across these axes (default `none`): variety for image tips. */
219
+ flip?: BrushFlip;
188
220
  }
189
221
  /** A Mixer Brush's paint reservoir: the brush is loaded with colour and mixes what it picks up into it. */
190
222
  export interface BrushReservoir {
@@ -8,10 +8,11 @@
8
8
  * Sliders reuse the paint editor's tables (`BRUSH_SLIDERS`, `brushEngineControls`), mapped from
9
9
  * engine paths to facet paths through `BRUSH_FIELDS`, so the panels and the editor agree.
10
10
  */
11
- import { type BrushLibrary, type BrushLibraryEntry, type SaveBrushResult } from './library.cjs';
11
+ import { type AddBrushSourceResult, type BrushLibrary, type BrushLibraryEntry, type SaveBrushResult } from './library.cjs';
12
12
  import { type BrushHostProfile, type BrushIssue } from './profiles.cjs';
13
+ import { type BrushSource, type BrushSourceChannel } from './sources.cjs';
13
14
  import { type Brush } from './types.cjs';
14
- export type BrushEditorSectionId = 'medium' | 'shape' | 'marks' | 'feel' | 'behaviour' | 'light' | 'motion' | 'colour';
15
+ export type BrushEditorSectionId = 'medium' | 'shape' | 'grain' | 'marks' | 'feel' | 'behaviour' | 'light' | 'motion' | 'colour';
15
16
  interface ControlBase {
16
17
  /** Facet path the control sets (`tip.size`, `behaviour.exchange.pickup`…). */
17
18
  path: string;
@@ -62,7 +63,10 @@ export type BrushEditorControl = (ControlBase & {
62
63
  }>;
63
64
  max: number;
64
65
  })
65
- /** Stamp refs (`editor:star`…): `options` are the editor's stamps; hosts may offer more refs. */
66
+ /**
67
+ * Stamp refs (`editor:star`, `source:<id>`…): `options` are the editor's stamps and the image
68
+ * sources the host passed (`brushEditorSections`' `sources`); hosts may offer more refs.
69
+ */
66
70
  | (ControlBase & {
67
71
  kind: 'stamps';
68
72
  value: string[];
@@ -72,6 +76,11 @@ export type BrushEditorControl = (ControlBase & {
72
76
  label: string;
73
77
  }>;
74
78
  });
79
+ /** What the host knows beyond the brush: the image sources it can paint with. */
80
+ export interface BrushEditorContext {
81
+ /** The library's image sources (`listSources`), offered as stamps and grain. */
82
+ sources?: ReadonlyArray<Pick<BrushSource, 'id' | 'name'>>;
83
+ }
75
84
  export interface BrushEditorSection {
76
85
  id: BrushEditorSectionId;
77
86
  title: string;
@@ -84,7 +93,7 @@ export interface BrushEditorSection {
84
93
  * edit keeps them as they are; `brushEditorScope` names them. Values are the brush's own, else
85
94
  * the defaults of the engine it resolves to.
86
95
  */
87
- export declare function brushEditorSections(brush: Brush, profile?: BrushHostProfile): BrushEditorSection[];
96
+ export declare function brushEditorSections(brush: Brush, profile?: BrushHostProfile, context?: BrushEditorContext): BrushEditorSection[];
88
97
  /** What a brush has that the host can't use: one facet, in words. */
89
98
  export interface BrushEditorHiddenFacet {
90
99
  facet: string;
@@ -166,4 +175,31 @@ export declare function brushLibraryGroups(entries: readonly BrushLibraryEntry[]
166
175
  profile?: BrushHostProfile;
167
176
  source?: 'builtin' | 'user';
168
177
  }): BrushLibraryGroup[];
178
+ /**
179
+ * A PNG `data:` URL of an image file a person picked, for `BrushLibrary.addSource`: a PNG is kept
180
+ * as it is; any other image (JPEG, WebP…) is drawn through the browser's `createImageBitmap` and
181
+ * `OffscreenCanvas`. Rejects, naming PNG, where there is no browser to draw it.
182
+ */
183
+ export declare function imageFileToPngDataUrl(file: Blob): Promise<string>;
184
+ /**
185
+ * Adds an image file to the library as a source (named after the file unless `name` is given),
186
+ * as the brush editor's "Add image" does. Returns the stored source, its `source:<id>` ref and what
187
+ * reading it changed.
188
+ */
189
+ export declare function addBrushSourceFromFile(library: Pick<BrushLibrary, 'addSource'>, file: Blob & {
190
+ name?: string;
191
+ }, options?: {
192
+ name?: string;
193
+ channel?: BrushSourceChannel;
194
+ }): Promise<AddBrushSourceResult & {
195
+ ref: string;
196
+ }>;
197
+ /**
198
+ * The brush with an image source as its tip (`as: 'tip'`: a stamp tip laying it) or as its grain
199
+ * (`as: 'grain'`), normalised, as `setBrushEditorValue` returns it.
200
+ */
201
+ export declare function withBrushSource(brush: Brush, ref: string, as: 'tip' | 'grain'): {
202
+ brush: Brush;
203
+ problems: string[];
204
+ };
169
205
  export {};
@@ -8,10 +8,11 @@
8
8
  * Sliders reuse the paint editor's tables (`BRUSH_SLIDERS`, `brushEngineControls`), mapped from
9
9
  * engine paths to facet paths through `BRUSH_FIELDS`, so the panels and the editor agree.
10
10
  */
11
- import { type BrushLibrary, type BrushLibraryEntry, type SaveBrushResult } from './library.js';
11
+ import { type AddBrushSourceResult, type BrushLibrary, type BrushLibraryEntry, type SaveBrushResult } from './library.js';
12
12
  import { type BrushHostProfile, type BrushIssue } from './profiles.js';
13
+ import { type BrushSource, type BrushSourceChannel } from './sources.js';
13
14
  import { type Brush } from './types.js';
14
- export type BrushEditorSectionId = 'medium' | 'shape' | 'marks' | 'feel' | 'behaviour' | 'light' | 'motion' | 'colour';
15
+ export type BrushEditorSectionId = 'medium' | 'shape' | 'grain' | 'marks' | 'feel' | 'behaviour' | 'light' | 'motion' | 'colour';
15
16
  interface ControlBase {
16
17
  /** Facet path the control sets (`tip.size`, `behaviour.exchange.pickup`…). */
17
18
  path: string;
@@ -62,7 +63,10 @@ export type BrushEditorControl = (ControlBase & {
62
63
  }>;
63
64
  max: number;
64
65
  })
65
- /** Stamp refs (`editor:star`…): `options` are the editor's stamps; hosts may offer more refs. */
66
+ /**
67
+ * Stamp refs (`editor:star`, `source:<id>`…): `options` are the editor's stamps and the image
68
+ * sources the host passed (`brushEditorSections`' `sources`); hosts may offer more refs.
69
+ */
66
70
  | (ControlBase & {
67
71
  kind: 'stamps';
68
72
  value: string[];
@@ -72,6 +76,11 @@ export type BrushEditorControl = (ControlBase & {
72
76
  label: string;
73
77
  }>;
74
78
  });
79
+ /** What the host knows beyond the brush: the image sources it can paint with. */
80
+ export interface BrushEditorContext {
81
+ /** The library's image sources (`listSources`), offered as stamps and grain. */
82
+ sources?: ReadonlyArray<Pick<BrushSource, 'id' | 'name'>>;
83
+ }
75
84
  export interface BrushEditorSection {
76
85
  id: BrushEditorSectionId;
77
86
  title: string;
@@ -84,7 +93,7 @@ export interface BrushEditorSection {
84
93
  * edit keeps them as they are; `brushEditorScope` names them. Values are the brush's own, else
85
94
  * the defaults of the engine it resolves to.
86
95
  */
87
- export declare function brushEditorSections(brush: Brush, profile?: BrushHostProfile): BrushEditorSection[];
96
+ export declare function brushEditorSections(brush: Brush, profile?: BrushHostProfile, context?: BrushEditorContext): BrushEditorSection[];
88
97
  /** What a brush has that the host can't use: one facet, in words. */
89
98
  export interface BrushEditorHiddenFacet {
90
99
  facet: string;
@@ -166,4 +175,31 @@ export declare function brushLibraryGroups(entries: readonly BrushLibraryEntry[]
166
175
  profile?: BrushHostProfile;
167
176
  source?: 'builtin' | 'user';
168
177
  }): BrushLibraryGroup[];
178
+ /**
179
+ * A PNG `data:` URL of an image file a person picked, for `BrushLibrary.addSource`: a PNG is kept
180
+ * as it is; any other image (JPEG, WebP…) is drawn through the browser's `createImageBitmap` and
181
+ * `OffscreenCanvas`. Rejects, naming PNG, where there is no browser to draw it.
182
+ */
183
+ export declare function imageFileToPngDataUrl(file: Blob): Promise<string>;
184
+ /**
185
+ * Adds an image file to the library as a source (named after the file unless `name` is given),
186
+ * as the brush editor's "Add image" does. Returns the stored source, its `source:<id>` ref and what
187
+ * reading it changed.
188
+ */
189
+ export declare function addBrushSourceFromFile(library: Pick<BrushLibrary, 'addSource'>, file: Blob & {
190
+ name?: string;
191
+ }, options?: {
192
+ name?: string;
193
+ channel?: BrushSourceChannel;
194
+ }): Promise<AddBrushSourceResult & {
195
+ ref: string;
196
+ }>;
197
+ /**
198
+ * The brush with an image source as its tip (`as: 'tip'`: a stamp tip laying it) or as its grain
199
+ * (`as: 'grain'`), normalised, as `setBrushEditorValue` returns it.
200
+ */
201
+ export declare function withBrushSource(brush: Brush, ref: string, as: 'tip' | 'grain'): {
202
+ brush: Brush;
203
+ problems: string[];
204
+ };
169
205
  export {};
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  brushPreviewColours
3
- } from "./chunk-EYSHTYLG.js";
3
+ } from "./chunk-LETF75CA.js";
4
4
  import {
5
5
  pathDataToPolyline
6
- } from "./chunk-EF7LZAXQ.js";
6
+ } from "./chunk-BAZMCFVL.js";
7
7
  import {
8
8
  BRUSH_PRESETS,
9
9
  BUILTIN_BRUSHES,
@@ -14,7 +14,7 @@ import {
14
14
  normalizeBrushSpec,
15
15
  resolveBrush,
16
16
  resolveCategory
17
- } from "./chunk-AGNUD5BI.js";
17
+ } from "./chunk-KZI3YWKW.js";
18
18
  import "./chunk-JL6YH5WD.js";
19
19
  import {
20
20
  formatNumber,
@@ -30,14 +30,14 @@ import {
30
30
  BrushstrokeCache,
31
31
  brushstrokeDataUrl,
32
32
  isBrushstrokeBrush
33
- } from "./chunk-TXFWZ5ZJ.js";
33
+ } from "./chunk-W6JDUXP3.js";
34
34
  import {
35
35
  parsePaintSource
36
- } from "./chunk-GCVFEPJG.js";
36
+ } from "./chunk-ACTHSQOI.js";
37
37
  import "./chunk-4D6KP5BV.js";
38
38
  import "./chunk-ODBT55Q7.js";
39
- import "./chunk-OTTT5N3K.js";
40
39
  import "./chunk-IRSTHLQQ.js";
40
+ import "./chunk-OTTT5N3K.js";
41
41
  import "./chunk-HVVR5PZW.js";
42
42
  import "./chunk-QGHEDC7B.js";
43
43
  import {
@@ -1,9 +1,9 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkHIO6E7LCcjs = require('./chunk-HIO6E7LC.cjs');
3
+ var _chunkBPJSI4NTcjs = require('./chunk-BPJSI4NT.cjs');
4
4
 
5
5
 
6
- var _chunkJ2TNIZ55cjs = require('./chunk-J2TNIZ55.cjs');
6
+ var _chunk2WOGXEDXcjs = require('./chunk-2WOGXEDX.cjs');
7
7
 
8
8
 
9
9
 
@@ -14,7 +14,7 @@ var _chunkJ2TNIZ55cjs = require('./chunk-J2TNIZ55.cjs');
14
14
 
15
15
 
16
16
 
17
- var _chunkXVOP3CGQcjs = require('./chunk-XVOP3CGQ.cjs');
17
+ var _chunkEU2ZZA6Lcjs = require('./chunk-EU2ZZA6L.cjs');
18
18
  require('./chunk-RDFHWAHL.cjs');
19
19
 
20
20
 
@@ -30,14 +30,14 @@ require('./chunk-WOFXZ3XU.cjs');
30
30
 
31
31
 
32
32
 
33
- var _chunkPXDIAYB6cjs = require('./chunk-PXDIAYB6.cjs');
33
+ var _chunkSTLTOISWcjs = require('./chunk-STLTOISW.cjs');
34
34
 
35
35
 
36
- var _chunkFQPUF2W2cjs = require('./chunk-FQPUF2W2.cjs');
36
+ var _chunkNIBFKXMDcjs = require('./chunk-NIBFKXMD.cjs');
37
37
  require('./chunk-3K6HWEXZ.cjs');
38
38
  require('./chunk-YQGCPL5X.cjs');
39
- require('./chunk-W5M3J7PV.cjs');
40
39
  require('./chunk-2VX55NFM.cjs');
40
+ require('./chunk-W5M3J7PV.cjs');
41
41
  require('./chunk-474IQG74.cjs');
42
42
  require('./chunk-XFM3OCHU.cjs');
43
43
 
@@ -48,17 +48,17 @@ require('./chunk-5UEFBM2M.cjs');
48
48
  require('./chunk-IPJ3TEYR.cjs');
49
49
 
50
50
  // src/canvas/brushstrokeRender.ts
51
- var PRESETS = _chunkXVOP3CGQcjs.listPresets.call(void 0, _chunkXVOP3CGQcjs.CANVAS_PROFILE).map((spec) => _chunkXVOP3CGQcjs.BUILTIN_BRUSHES.find((b) => b.id === spec.id));
51
+ var PRESETS = _chunkEU2ZZA6Lcjs.listPresets.call(void 0, _chunkEU2ZZA6Lcjs.CANVAS_PROFILE).map((spec) => _chunkEU2ZZA6Lcjs.BUILTIN_BRUSHES.find((b) => b.id === spec.id));
52
52
  var presetIds = () => PRESETS.map((b) => b.id).join(", ");
53
- var refusal = (brush) => _chunkXVOP3CGQcjs.checkBrush.call(void 0, brush, _chunkXVOP3CGQcjs.CANVAS_PROFILE).issues.filter((i) => i.severity === "error").map((i) => `${i.reason} ${i.fix}`).join(" ");
53
+ var refusal = (brush) => _chunkEU2ZZA6Lcjs.checkBrush.call(void 0, brush, _chunkEU2ZZA6Lcjs.CANVAS_PROFILE).issues.filter((i) => i.severity === "error").map((i) => `${i.reason} ${i.fix}`).join(" ");
54
54
  function readCanvasBrushSpec(spec) {
55
55
  let brush;
56
56
  try {
57
- brush = _chunkXVOP3CGQcjs.normalizeBrushSpec.call(void 0, spec);
57
+ brush = _chunkEU2ZZA6Lcjs.normalizeBrushSpec.call(void 0, spec);
58
58
  } catch (e) {
59
59
  throw new Error(`Not a brush: ${e.message}`);
60
60
  }
61
- if (!_chunkXVOP3CGQcjs.checkBrush.call(void 0, brush, _chunkXVOP3CGQcjs.CANVAS_PROFILE).ok) throw new Error(`Brush '${brush.id}' (${brush.name}) can't brush a canvas stroke: ${refusal(brush)}`);
61
+ if (!_chunkEU2ZZA6Lcjs.checkBrush.call(void 0, brush, _chunkEU2ZZA6Lcjs.CANVAS_PROFILE).ok) throw new Error(`Brush '${brush.id}' (${brush.name}) can't brush a canvas stroke: ${refusal(brush)}`);
62
62
  return brush;
63
63
  }
64
64
  function listCanvasBrushes(user = []) {
@@ -66,12 +66,12 @@ function listCanvasBrushes(user = []) {
66
66
  const d = _optionalChain([spec, 'access', _2 => _2.colour, 'optionalAccess', _3 => _3.default]);
67
67
  return typeof d === "string" || _chunkMLLTWAXHcjs.isDualColor.call(void 0, d) ? { defaultColor: d } : {};
68
68
  };
69
- const out = PRESETS.map((b) => ({ id: b.id, name: b.name, category: b.category, size: b.tip.size, builtin: true, ...colourOf(_chunkXVOP3CGQcjs.BRUSH_PRESETS.find((p) => p.id === b.id)) }));
69
+ const out = PRESETS.map((b) => ({ id: b.id, name: b.name, category: b.category, size: b.tip.size, builtin: true, ...colourOf(_chunkEU2ZZA6Lcjs.BRUSH_PRESETS.find((p) => p.id === b.id)) }));
70
70
  for (const raw of user) {
71
71
  try {
72
72
  const brush = readCanvasBrushSpec(raw);
73
73
  if (out.some((b) => b.id === brush.id)) continue;
74
- out.push({ id: brush.id, name: brush.name, category: _chunkXVOP3CGQcjs.resolveCategory.call(void 0, brush), size: _chunkXVOP3CGQcjs.resolveBrush.call(void 0, brush).tip.size, builtin: false, spec: brush, ...colourOf(brush) });
74
+ out.push({ id: brush.id, name: brush.name, category: _chunkEU2ZZA6Lcjs.resolveCategory.call(void 0, brush), size: _chunkEU2ZZA6Lcjs.resolveBrush.call(void 0, brush).tip.size, builtin: false, spec: brush, ...colourOf(brush) });
75
75
  } catch (e2) {
76
76
  }
77
77
  }
@@ -82,15 +82,15 @@ function resolveCanvasBrush(brush, width, spec) {
82
82
  const sized = (b) => given === void 0 ? b : { ...b, tip: { ...b.tip, size: given } };
83
83
  if (typeof brush === "string") {
84
84
  if (spec !== void 0 && spec !== null) {
85
- const saved = _chunkXVOP3CGQcjs.resolveBrush.call(void 0, readCanvasBrushSpec(spec));
86
- if (!_chunkPXDIAYB6cjs.isBrushstrokeBrush.call(void 0, saved)) throw new Error(`Brush '${saved.id}' (${saved.name}) can't brush a canvas stroke: it is not a dry dab brush.`);
85
+ const saved = _chunkEU2ZZA6Lcjs.resolveBrush.call(void 0, readCanvasBrushSpec(spec));
86
+ if (!_chunkSTLTOISWcjs.isBrushstrokeBrush.call(void 0, saved)) throw new Error(`Brush '${saved.id}' (${saved.name}) can't brush a canvas stroke: it is not a dry dab brush.`);
87
87
  return sized(saved);
88
88
  }
89
89
  const preset = PRESETS.find((b) => b.id === brush);
90
90
  if (preset) return sized(preset);
91
- const other = _chunkXVOP3CGQcjs.BRUSH_PRESETS.find((b) => b.id === brush);
91
+ const other = _chunkEU2ZZA6Lcjs.BRUSH_PRESETS.find((b) => b.id === brush);
92
92
  if (other) throw new Error(`Brush '${brush}' (${other.name}) can't brush a canvas stroke: ${refusal(other)} Use one of: ${presetIds()}, or a saved brush the canvas can paint.`);
93
- throw new Error(`Unknown brush '${brush}'. ${_chunkXVOP3CGQcjs.closest.call(void 0, brush, PRESETS.map((b) => b.id))}Use one of the dry presets (${presetIds()}), a saved brush from the brush library, or tip settings ({ hardness, spacing, flow, \u2026 }).`);
93
+ throw new Error(`Unknown brush '${brush}'. ${_chunkEU2ZZA6Lcjs.closest.call(void 0, brush, PRESETS.map((b) => b.id))}Use one of the dry presets (${presetIds()}), a saved brush from the brush library, or tip settings ({ hardness, spacing, flow, \u2026 }).`);
94
94
  }
95
95
  if (!brush || typeof brush !== "object" || Array.isArray(brush)) {
96
96
  throw new Error("A brush is a preset or saved brush id (see listCanvasBrushes / canvas.listBrushes) or an object of tip settings.");
@@ -98,7 +98,7 @@ function resolveCanvasBrush(brush, width, spec) {
98
98
  return { size: _nullishCoalesce(given, () => ( 3)), ...brush };
99
99
  }
100
100
  function resolveCanvasPaint(paint) {
101
- const source = _chunkFQPUF2W2cjs.parsePaintSource.call(void 0, paint, "paint");
101
+ const source = _chunkNIBFKXMDcjs.parsePaintSource.call(void 0, paint, "paint");
102
102
  if (source.type === "gradient" && (source.map.by === "velocity" || source.map.by === "time")) {
103
103
  throw new Error(`paint.map.by "${source.map.by}" needs timed samples, which canvas strokes do not record; use distance, pressure, tilt, direction, across-tip or random.`);
104
104
  }
@@ -119,14 +119,14 @@ function strokeColor(shape) {
119
119
  const n = parseInt(parsed.hex.slice(1), 16);
120
120
  return [n >> 16 & 255, n >> 8 & 255, n & 255, Math.round(parsed.alpha * 255)];
121
121
  }
122
- var cache = new (0, _chunkPXDIAYB6cjs.BrushstrokeCache)(128);
122
+ var cache = new (0, _chunkSTLTOISWcjs.BrushstrokeCache)(128);
123
123
  var urls = /* @__PURE__ */ new WeakMap();
124
124
  function renderShapeBrushstrokeNow(shape, scale, place = {}) {
125
125
  const seed = shape.brush.seed;
126
126
  const textured = typeof seed === "number" && Number.isFinite(seed);
127
127
  const size = _nullishCoalesce(shape.brush.size, () => ( (textured ? void 0 : _nullishCoalesce(shape.strokeWidth, () => ( 3)))));
128
128
  const brush = resolveCanvasBrush(shape.brush.brush, size, shape.brush.spec);
129
- const points = _chunkJ2TNIZ55cjs.pathDataToPolyline.call(void 0, shape.d);
129
+ const points = _chunk2WOGXEDXcjs.pathDataToPolyline.call(void 0, shape.d);
130
130
  if (!points.length) return null;
131
131
  const { pressure, tiltX, tiltY } = shape.brush;
132
132
  const input = {
@@ -149,7 +149,7 @@ function renderShapeBrushstrokeNow(shape, scale, place = {}) {
149
149
  if (!stroke.bounds) return null;
150
150
  let href = urls.get(stroke);
151
151
  if (href === void 0) {
152
- href = _chunkPXDIAYB6cjs.brushstrokeDataUrl.call(void 0, stroke);
152
+ href = _chunkSTLTOISWcjs.brushstrokeDataUrl.call(void 0, stroke);
153
153
  urls.set(stroke, href);
154
154
  }
155
155
  return { href, ...stroke.bounds, scale: stroke.scale };
@@ -164,8 +164,8 @@ function brushedShapeSvgNow(shape, size, scale, place = {}) {
164
164
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${f(size.width)}" height="${f(size.height)}" viewBox="0 0 ${f(size.width)} ${f(size.height)}">${content}</svg>`;
165
165
  }
166
166
  function swatchColour(brush, background, spec) {
167
- const facets = typeof brush !== "string" ? null : spec ? readCanvasBrushSpec(spec) : _chunkXVOP3CGQcjs.BRUSH_PRESETS.find((b) => b.id === brush);
168
- return background && facets ? _chunkHIO6E7LCcjs.brushPreviewColours.call(void 0, facets, { paper: background, profile: _chunkXVOP3CGQcjs.CANVAS_PROFILE, keepPaper: true }).color : "#1f2937";
167
+ const facets = typeof brush !== "string" ? null : spec ? readCanvasBrushSpec(spec) : _chunkEU2ZZA6Lcjs.BRUSH_PRESETS.find((b) => b.id === brush);
168
+ return background && facets ? _chunkBPJSI4NTcjs.brushPreviewColours.call(void 0, facets, { paper: background, profile: _chunkEU2ZZA6Lcjs.CANVAS_PROFILE, keepPaper: true }).color : "#1f2937";
169
169
  }
170
170
  function brushSwatchNow(brush, options, spec) {
171
171
  const w = options.width && options.width > 8 ? options.width : 96;