@pixerate/editor 0.109.0 → 0.110.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 (61) hide show
  1. package/dist/blending-DXl06NSx.d.ts +486 -0
  2. package/dist/blending-aOE6Y3UE.d.cts +486 -0
  3. package/dist/chunk-7VOHOPLW.cjs +1136 -0
  4. package/dist/chunk-7VOHOPLW.cjs.map +1 -0
  5. package/dist/{chunk-RE77QYUF.js → chunk-DMRSJBJF.js} +3 -3
  6. package/dist/chunk-FTBZPI77.cjs +1 -0
  7. package/dist/chunk-FTBZPI77.cjs.map +1 -0
  8. package/dist/{chunk-WMBIJCHA.js → chunk-HNS7UHCM.js} +5 -1
  9. package/dist/{chunk-WMBIJCHA.js.map → chunk-HNS7UHCM.js.map} +1 -1
  10. package/dist/{chunk-CFMWALBM.js → chunk-LQEEDHOP.js} +2 -2
  11. package/dist/{chunk-D37BX2ZA.cjs → chunk-MGL4U5WA.cjs} +199 -199
  12. package/dist/{chunk-D37BX2ZA.cjs.map → chunk-MGL4U5WA.cjs.map} +1 -1
  13. package/dist/{chunk-AZTZRLDI.cjs → chunk-POGBUYWE.cjs} +92 -92
  14. package/dist/{chunk-AZTZRLDI.cjs.map → chunk-POGBUYWE.cjs.map} +1 -1
  15. package/dist/chunk-VKK5VS2T.js +1 -0
  16. package/dist/{chunk-SZ6KMZNM.cjs → chunk-XIOMMGPD.cjs} +6 -2
  17. package/dist/chunk-XIOMMGPD.cjs.map +1 -0
  18. package/dist/{chunk-3PHA3UKI.js → chunk-Z4BC6M74.js} +523 -121
  19. package/dist/chunk-Z4BC6M74.js.map +1 -0
  20. package/dist/{export-BLPLaE6Z.d.ts → export-CO2ryu3I.d.ts} +3 -3
  21. package/dist/{export-DSUc4fQD.d.cts → export-DRiLzP_j.d.cts} +3 -3
  22. package/dist/image-editor/index.cjs +722 -696
  23. package/dist/image-editor/index.cjs.map +1 -1
  24. package/dist/image-editor/index.d.cts +9 -9
  25. package/dist/image-editor/index.d.ts +9 -9
  26. package/dist/image-editor/index.js +31 -5
  27. package/dist/image-editor/index.js.map +1 -1
  28. package/dist/paint/index.cjs +28 -4
  29. package/dist/paint/index.cjs.map +1 -1
  30. package/dist/paint/index.d.cts +3 -89
  31. package/dist/paint/index.d.ts +3 -89
  32. package/dist/paint/index.js +27 -3
  33. package/dist/paint-editor/index.cjs +1052 -286
  34. package/dist/paint-editor/index.cjs.map +1 -1
  35. package/dist/paint-editor/index.d.cts +301 -22
  36. package/dist/paint-editor/index.d.ts +301 -22
  37. package/dist/paint-editor/index.js +848 -82
  38. package/dist/paint-editor/index.js.map +1 -1
  39. package/dist/raster/index.cjs +5 -3
  40. package/dist/raster/index.cjs.map +1 -1
  41. package/dist/raster/index.d.cts +2 -2
  42. package/dist/raster/index.d.ts +2 -2
  43. package/dist/raster/index.js +4 -2
  44. package/dist/{render-C-fO_mes.d.cts → render-BEriFuqK.d.cts} +1 -1
  45. package/dist/{render-OLIMk_v0.d.ts → render-tjzKaik-.d.ts} +1 -1
  46. package/dist/{surface-CkKOHGPm.d.cts → surface-C8RRg_Bh.d.cts} +109 -1
  47. package/dist/{surface-CkKOHGPm.d.ts → surface-C8RRg_Bh.d.ts} +109 -1
  48. package/package.json +1 -1
  49. package/skills/pixerate-paint-editor/SKILL.md +49 -13
  50. package/dist/chunk-3PHA3UKI.js.map +0 -1
  51. package/dist/chunk-6BU5WU4R.cjs +0 -1
  52. package/dist/chunk-6BU5WU4R.cjs.map +0 -1
  53. package/dist/chunk-HUFQD4CB.cjs +0 -734
  54. package/dist/chunk-HUFQD4CB.cjs.map +0 -1
  55. package/dist/chunk-SZ6KMZNM.cjs.map +0 -1
  56. package/dist/chunk-XYBJKUAU.js +0 -1
  57. package/dist/mixer-5ziHU8OS.d.cts +0 -282
  58. package/dist/mixer-DNqiYXYQ.d.ts +0 -282
  59. /package/dist/{chunk-RE77QYUF.js.map → chunk-DMRSJBJF.js.map} +0 -0
  60. /package/dist/{chunk-CFMWALBM.js.map → chunk-LQEEDHOP.js.map} +0 -0
  61. /package/dist/{chunk-XYBJKUAU.js.map → chunk-VKK5VS2T.js.map} +0 -0
@@ -1,9 +1,9 @@
1
- import { R as RasterLayer, a as RasterGroupLayer, b as RasterPixelLayer, C as CompositorLayer, c as RasterCompositor, d as RasterSources, L as LayerLocks } from '../render-C-fO_mes.cjs';
2
- import { B as BrushSettings, M as MixerSettings, C as CoverageMap, D as Dab, S as StrokePoint, a as MixerLoad } from '../mixer-5ziHU8OS.cjs';
3
- export { b as MAX_BRUSH_SIZE } from '../mixer-5ziHU8OS.cjs';
4
- import { R as RasterRect, a as TiledSurface, B as BlendMode, b as BlendModeInfo } from '../surface-CkKOHGPm.cjs';
5
- export { c as BLEND_MODES, d as PAINTERLY_BLEND_MODES, e as Tile, i as isBlendMode, f as isPainterlyBlendMode, s as standardBlendMode } from '../surface-CkKOHGPm.cjs';
6
- import { I as ImageEditorAssetStore, S as SerializedLayer, P as PsdExportResult } from '../export-DSUc4fQD.cjs';
1
+ import { R as RasterLayer, a as RasterGroupLayer, b as RasterPixelLayer, C as CompositorLayer, c as RasterCompositor, d as RasterSources, L as LayerLocks } from '../render-BEriFuqK.cjs';
2
+ import { B as BrushSettings, M as MixerSettings, a as MixingModel, b as BlenderSettings, S as SmudgeSettings, C as CoverageMap, D as Dab, c as StrokePoint, d as MixerLoad, P as PaintTileTarget, R as RgbTriple } from '../blending-aOE6Y3UE.cjs';
3
+ export { e as MAX_BRUSH_SIZE } from '../blending-aOE6Y3UE.cjs';
4
+ import { R as RasterRect, a as TiledSurface, B as BlendMode, c as BlendModeInfo } from '../surface-C8RRg_Bh.cjs';
5
+ export { d as BLEND_MODES, e as PAINTERLY_BLEND_MODES, f as Tile, i as isBlendMode, g as isPainterlyBlendMode, s as standardBlendMode } from '../surface-C8RRg_Bh.cjs';
6
+ import { I as ImageEditorAssetStore, S as SerializedLayer, P as PsdExportResult } from '../export-DRiLzP_j.cjs';
7
7
  import { g as PresenceStore, e as PresenceClock, d as PresenceActorInfo, c as PresenceActor, f as PresenceState } from '../presence-Cb6WhcrQ.cjs';
8
8
  import { a as AgentToolProvider } from '../registry-BqA1AZt0.cjs';
9
9
  import '../types-CwMPoRMU.cjs';
@@ -150,11 +150,49 @@ declare function renderPaper(paper: PaperSettings, rect: {
150
150
  * the blending brushes. The built-in presets are our own settings; none come from another product.
151
151
  */
152
152
 
153
- /** How a brush makes marks: dabs into a stroke buffer, removing alpha, or the Mixer Brush engine. */
154
- type PaintBrushEngine = 'dab' | 'eraser' | 'mixer';
153
+ /**
154
+ * How a brush makes marks: `dab` (dabs into a stroke buffer, or dab by dab through the pick-up /
155
+ * mix exchange when its behaviour asks for it), `eraser` (removes alpha), `mixer` (the Mixer
156
+ * Brush), `blender` (mixes the colours under and near the tip) or `smudge` (drags paint along).
157
+ */
158
+ type PaintBrushEngine = 'dab' | 'eraser' | 'mixer' | 'blender' | 'smudge';
155
159
  declare const PAINT_BRUSH_ENGINES: readonly PaintBrushEngine[];
156
- type PaintBrushCategory = 'pencil' | 'pastel' | 'charcoal' | 'airbrush' | 'marker' | 'ink' | 'eraser' | 'blender' | 'smudge' | 'mixer' | 'custom';
160
+ type PaintBrushCategory = 'pencil' | 'pastel' | 'charcoal' | 'airbrush' | 'marker' | 'ink' | 'paint' | 'eraser' | 'blender' | 'smudge' | 'mixer' | 'custom';
157
161
  declare const PAINT_BRUSH_CATEGORIES: readonly PaintBrushCategory[];
162
+ /** Multi-coloured brush: `tip` lays the colours across the tip, `dab` changes colour from dab to dab. */
163
+ type MultiColorMode = 'off' | 'tip' | 'dab';
164
+ declare const MULTI_COLOR_MODES: readonly MultiColorMode[];
165
+ interface MultiColorSettings {
166
+ /** `off` (default), `tip` (the colours side by side across the tip, like a brush loaded with several paints; the stripes follow the stroke) or `dab` (the colour changes along the stroke). */
167
+ mode: MultiColorMode;
168
+ /** 2..8 colours #rrggbb. Fewer than 2: the paint colour and the secondary colour. */
169
+ colors: string[];
170
+ /** `dab` mode: `sequence` (default) runs through the colours in order, blending between them; `random` picks one per run of dabs (seeded, so replays match). */
171
+ order: 'sequence' | 'random';
172
+ /** `dab` mode: dabs per colour, 1..500 (default 12). */
173
+ length: number;
174
+ /** How softly neighbouring colours blend, 0 (hard stripes or steps) .. 1 (smooth gradient, default). */
175
+ softness: number;
176
+ }
177
+ declare const DEFAULT_MULTI_COLOR: Readonly<MultiColorSettings>;
178
+ /**
179
+ * How a brush handles paint on the canvas. The pick-up / mix ratios are the shared exchange's
180
+ * (`@pixerate/editor/paint` `PaintExchange`); wet media will use the same ones. On a Mixer Brush
181
+ * they are its `mixer.wet`, `mixer.mix`, `!mixer.cleanEachStroke` and `mixer.mixing`, kept in sync.
182
+ */
183
+ interface BrushBehavior {
184
+ /** Pick-up ratio, 0..1: how much of the paint under each dab the brush lifts and carries (0: none, default). */
185
+ pickup: number;
186
+ /** Mix ratio, 0..1: how much of the carried paint goes into what the brush lays down, against its own colour (default 0.5). */
187
+ mix: number;
188
+ /** Dirty brush: keep the carried paint after the stroke, so the next stroke starts with the last colour the brush touched (default false). */
189
+ dirty: boolean;
190
+ /** How picked-up paint, multi-colour gradients and the deposit mix: `pigment` (default, Kubelka-Munk) or `rgb`. */
191
+ mixing: MixingModel;
192
+ /** Multi-coloured brush. */
193
+ multicolor: MultiColorSettings;
194
+ }
195
+ declare const DEFAULT_BEHAVIOR: Readonly<BrushBehavior>;
158
196
  /** What turns the tip: a fixed angle, the direction of the stroke, or the pen's barrel rotation. */
159
197
  type BrushRotation = 'fixed' | 'stroke' | 'barrel';
160
198
  declare const BRUSH_ROTATIONS: readonly BrushRotation[];
@@ -202,17 +240,39 @@ interface PaintBrush {
202
240
  mixer: MixerSettings;
203
241
  /** Mixer engine only: load the current colour each stroke (false: a smudge or blender that only moves paint). */
204
242
  loadsColor: boolean;
243
+ /** Pick-up and mix ratios, dirty brush and multi-colour (dab and mixer engines). */
244
+ behavior: BrushBehavior;
245
+ /** Blender engine only: strength, blend distance and mixing. */
246
+ blender: BlenderSettings;
247
+ /** Smudge engine only: strength, length and mixing. */
248
+ smudge: SmudgeSettings;
205
249
  /** True for the built-in presets (they cannot be removed or overwritten). */
206
250
  builtin?: boolean;
207
251
  }
208
252
  declare function normalizeDynamics(raw?: Partial<BrushDynamics>, base?: BrushDynamics): BrushDynamics;
209
253
  declare const BRUSH_ID: RegExp;
210
- /** Brush settings to change or read: any subset of a brush, its tip, dynamics and mixer. */
211
- type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer'>> & {
254
+ declare function normalizeMultiColor(raw?: Partial<MultiColorSettings>, base?: MultiColorSettings): MultiColorSettings;
255
+ /** Behaviour input: any subset, with a partial multi-colour block. */
256
+ type BrushBehaviorInput = Partial<Omit<BrushBehavior, 'multicolor'>> & {
257
+ multicolor?: Partial<MultiColorSettings>;
258
+ };
259
+ declare function normalizeBehavior(raw?: BrushBehaviorInput, base?: BrushBehavior): BrushBehavior;
260
+ /** Brush settings to change or read: any subset of a brush and of its blocks. */
261
+ type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer' | 'behavior' | 'blender' | 'smudge'>> & {
212
262
  tip?: Partial<BrushSettings>;
213
263
  dynamics?: Partial<BrushDynamics>;
214
264
  mixer?: Partial<MixerSettings>;
265
+ behavior?: BrushBehaviorInput;
266
+ blender?: Partial<BlenderSettings>;
267
+ smudge?: Partial<SmudgeSettings>;
215
268
  };
269
+ /** Whether a brush can pick paint up from the canvas (the pick-up / mix ratios apply): dab and mixer brushes. */
270
+ declare function isWetCapable(brush: Pick<PaintBrush, 'engine'>): boolean;
271
+ /**
272
+ * Whether a dab brush paints dab by dab through the exchange (it picks up paint or is
273
+ * multi-coloured) instead of building one coverage buffer of one colour.
274
+ */
275
+ declare function usesPaintExchange(brush: PaintBrush): boolean;
216
276
  /** A brush with every field in range; missing fields come from `base` (default: a plain round dab brush). */
217
277
  declare function normalizePaintBrush(raw?: PaintBrushInput, base?: PaintBrush): PaintBrush;
218
278
  /** The built-in brush library. */
@@ -306,6 +366,10 @@ declare const IDENTITY_COPY: StrokeCopy;
306
366
  /** A dab with the pen state it was made at (the shared `Dab` plus pressure for the grain). */
307
367
  interface PenDab extends Dab {
308
368
  pressure: number;
369
+ /** Direction of travel at the dab, radians counter-clockwise with y up (as `angle`). */
370
+ direction: number;
371
+ /** The dab's index within its copy of the stroke (0, 1, 2 …). */
372
+ index: number;
309
373
  }
310
374
  /**
311
375
  * Walks pen samples into dabs, incrementally (any chunking gives the same dabs). Dab angles are in
@@ -390,6 +454,8 @@ interface RecordedStroke {
390
454
  seed: number;
391
455
  /** Extra copies painted with the stroke (mirror / kaleidoscope), as affine maps of the points. */
392
456
  copies?: Affine[];
457
+ /** What the brush carried at the start (Mixer Brush and wet-capable brushes), so a replay mixes the same. */
458
+ load?: MixerLoad;
393
459
  }
394
460
  declare function applyAffine(m: Affine, x: number, y: number): {
395
461
  x: number;
@@ -485,15 +551,91 @@ declare function createPaintDocument(options?: Partial<PaintCanvas> & {
485
551
  paper?: Partial<PaperSettings>;
486
552
  }): PaintDocumentState;
487
553
 
554
+ /**
555
+ * The paint editor's dab-by-dab stroke engine: every brush whose dabs read what earlier dabs laid
556
+ * down. The Mixer Brush, the blender, the smudge, and dab brushes that pick up paint or are
557
+ * multi-coloured all run here, with the same pen input as dry media (`PenDabWalker`: pressure,
558
+ * tilt, barrel rotation, velocity, rotation modes, jitter), paper grain and symmetry copies.
559
+ *
560
+ * - `mixer` and wet-capable `dab` brushes go through the shared pick-up / mix exchange
561
+ * (`PaintExchange` over a `TileMedium`): each dab picks up from the layer, mixes its paint from
562
+ * the brush colour (or the multi-colour gradient) and the carried paint, and lays it down. A
563
+ * Mixer Brush deposits per dab (photocraft's model); a dab brush keeps the stroke's opacity
564
+ * ceiling: a shared `CoverageMap` builds up as for dry media and each dab lays only the
565
+ * increase, `k = (c₁ − c₀)·o / (1 − c₀·o)`, so a one-colour Normal stroke matches the dry engine.
566
+ * - `blender` calls `blenderDab`, `smudge` a `SmudgeTip` per copy.
567
+ *
568
+ * Pixels go through the session's live tiles (`PaintTileTarget`). Pure data in, pixels out:
569
+ * points, seed, colours and the starting load replay exactly.
570
+ */
571
+
572
+ interface PaintDabStrokeOptions {
573
+ /** Where the stroke reads and writes pixels. */
574
+ target: PaintTileTarget;
575
+ /** The paint colour, sRGB 0..1. */
576
+ color: RgbTriple;
577
+ /** The secondary colour (a multi-colour brush without its own colours uses both). */
578
+ secondary: RgbTriple;
579
+ /** What the brush carries at the start (mixer and wet-capable dab brushes). */
580
+ load: MixerLoad;
581
+ grain: PaperGrain | null;
582
+ paperDepth: number;
583
+ seed?: number;
584
+ copies?: readonly StrokeCopy[];
585
+ }
586
+ /** Colours of a multi-colour brush, resolved against the paint and secondary colours. */
587
+ declare function multiColorPalette(mc: MultiColorSettings, color: RgbTriple, secondary: RgbTriple): RgbTriple[];
588
+ /** The tip gradient of a multi-colour brush: `GRADIENT_STEPS` colours across the tip. */
589
+ declare function multiColorGradient(palette: readonly RgbTriple[], softness: number, mixing: MixingModel): RgbTriple[];
590
+ /** The colour of dab `index` of a `dab`-mode multi-colour brush. */
591
+ declare function multiColorForDab(mc: MultiColorSettings, palette: readonly RgbTriple[], index: number, seed: number, mixing: MixingModel): RgbTriple;
592
+ declare class PaintDabStroke {
593
+ readonly brush: PaintBrush;
594
+ private readonly options;
595
+ dabCount: number;
596
+ private readonly smoother;
597
+ private readonly copies;
598
+ private readonly states;
599
+ private readonly medium;
600
+ private readonly depth;
601
+ private readonly coverage;
602
+ private readonly palette;
603
+ private readonly gradient;
604
+ private readonly fieldLut;
605
+ private readonly scratch;
606
+ private readonly mixing;
607
+ private tip;
608
+ private cov;
609
+ private field;
610
+ private readonly paint;
611
+ private lastRaw;
612
+ private lastFed;
613
+ constructor(brush: PaintBrush, options: PaintDabStrokeOptions);
614
+ /** What copy 0's brush carries after the dabs so far (mixer and wet-capable dab brushes). */
615
+ get load(): MixerLoad | null;
616
+ push(points: readonly PenPoint[]): RasterRect[];
617
+ /** Ends the stroke: the smoothed path catches up with the last raw sample; a tap paints one dab. */
618
+ finish(): RasterRect[];
619
+ private paintDabs;
620
+ /** The tip's pure coverage (`tip`) and, with grain, what the paper catches of it (`cov`). */
621
+ private rasterize;
622
+ private dab;
623
+ /** Fills `this.field` with the tip gradient (mixed with the carried paint) per pixel; returns the paint weight. */
624
+ private tipField;
625
+ /** A dab brush's deposit: only what this dab adds to the stroke's coverage, under its opacity ceiling. */
626
+ private depositWithCeiling;
627
+ }
628
+
488
629
  /**
489
630
  * A live stroke on one pixel layer. Points arrive while the pen moves; the layer shows the stroke
490
631
  * so far through a live surface (no history); `end()` commits it as one undo step.
491
632
  *
492
633
  * - Dab and eraser brushes: the coverage is cumulative, so each update recomposites the touched
493
634
  * region from the pre-stroke tile (`applyStrokeToTile` with `into` and `region`).
494
- * - Mixer engine brushes (Mixer Brush, blender, smudge): dabs read what earlier dabs laid down, so
495
- * they paint into live tile copies; every tile written since the last update gets a new `Tile`
496
- * object, because the compositor caches by tile identity.
635
+ * - Dab-by-dab brushes (`PaintDabStroke`: the Mixer Brush, blender, smudge, and dab brushes that
636
+ * pick up paint or are multi-coloured): dabs read what earlier dabs laid down, so they paint
637
+ * into live tile copies; every tile written since the last update gets a new `Tile` object,
638
+ * because the compositor caches by tile identity.
497
639
  */
498
640
 
499
641
  interface StrokeSessionHost {
@@ -525,8 +667,10 @@ interface StrokeSessionInit {
525
667
  copies: readonly StrokeCopy[];
526
668
  /** Applied to each raw sample before it is painted and recorded (perspective snapping). */
527
669
  inputFilter?: (p: PenPoint) => PenPoint;
528
- /** Mixer engine: the brush load at the start of the stroke. */
670
+ /** Mixer and wet-capable dab brushes: what the brush carries at the start of the stroke. */
529
671
  mixerLoad: MixerLoad;
672
+ /** The secondary colour 0..255 RGBA (a multi-colour brush without its own colours uses it). */
673
+ secondary?: readonly [number, number, number, number];
530
674
  }
531
675
  interface PaintStrokeResult {
532
676
  layerId: string;
@@ -535,7 +679,7 @@ interface PaintStrokeResult {
535
679
  dabs: number;
536
680
  /** Id of the recorded stroke (see `list_strokes` / `replay_strokes`), or `null` when nothing changed. */
537
681
  strokeId: string | null;
538
- /** Mixer engine brushes: what the brush carries after the stroke. */
682
+ /** Mixer and wet-capable dab brushes: what the brush carries after the stroke (cleaned unless it is dirty). */
539
683
  mixerLoad?: MixerLoad;
540
684
  }
541
685
  declare class PaintStrokeSession {
@@ -549,8 +693,7 @@ declare class PaintStrokeSession {
549
693
  private readonly touched;
550
694
  private readonly fresh;
551
695
  private readonly dry;
552
- private readonly mixers;
553
- private readonly mixerSmoother;
696
+ private readonly wet;
554
697
  private readonly apply;
555
698
  private readonly copies;
556
699
  private bounds;
@@ -558,7 +701,7 @@ declare class PaintStrokeSession {
558
701
  private closed;
559
702
  result: PaintStrokeResult | null;
560
703
  constructor(host: StrokeSessionHost, init: StrokeSessionInit);
561
- private mixerTarget;
704
+ private tileTarget;
562
705
  get open(): boolean;
563
706
  /** Adds raw pen samples (document pixels). */
564
707
  push(points: readonly PenPoint[]): void;
@@ -570,6 +713,12 @@ declare class PaintStrokeSession {
570
713
  }
571
714
  /** The Mixer Brush load a stroke starts with. */
572
715
  declare function mixerLoadForStroke(brush: PaintBrush, current: MixerLoad, color: readonly [number, number, number]): MixerLoad;
716
+ /**
717
+ * What any brush carries at the start of a stroke: the Mixer Brush as `mixerLoadForStroke`; a
718
+ * wet-capable dab brush the paint colour, full, plus the carried paint when it is dirty; other
719
+ * brushes nothing.
720
+ */
721
+ declare function brushLoadForStroke(brush: PaintBrush, current: MixerLoad, color: readonly [number, number, number]): MixerLoad;
573
722
 
574
723
  /**
575
724
  * Compositing for the paint editor, on the shared `RasterCompositor`: the paper is a content layer
@@ -668,6 +817,8 @@ interface SerializeOptions {
668
817
  /** Painting guides (symmetry, perspective, tracing and reference images as PNG data URLs). */
669
818
  guides?: unknown;
670
819
  swatches?: unknown;
820
+ /** The mixing pad: `{ current?: png data URL, saved: [{ id, name, png }] }`. */
821
+ mixingPad?: unknown;
671
822
  }
672
823
  /** The document as JSON with inline PNG data URLs. */
673
824
  declare function serializePaintDocument(state: PaintDocumentState, options?: SerializeOptions): SerializedPaintDocument;
@@ -679,6 +830,8 @@ interface LoadedPaintDocument {
679
830
  /** The saved guides and swatches, as stored (the controller validates them). */
680
831
  guides?: unknown;
681
832
  swatches?: unknown;
833
+ /** The saved mixing pad, as stored (the controller validates it). */
834
+ mixingPad?: unknown;
682
835
  /** What could not be read (the rest still loads). */
683
836
  problems: string[];
684
837
  }
@@ -1036,13 +1189,33 @@ declare const DEFAULT_SWATCHES: readonly Swatch[];
1036
1189
  /**
1037
1190
  * The mixing pad: a small canvas beside the painting to put paint on, smear it with a palette
1038
1191
  * knife and pick the mixed colour from. Paint is laid with the Mixer Brush in pigment mode and the
1039
- * knife is a pigment smudge, so blue and yellow pushed together make green. It is scratch space:
1040
- * not part of the painting and not in its undo history.
1192
+ * knife smears it with pigment mixing, so blue and yellow pushed together make green. It is
1193
+ * scratch space: not part of the painting's undo history, but saved with the document, and pads
1194
+ * can be kept by name (`MixingPad.toPng` / `fromPng`).
1195
+ *
1196
+ * `mix` puts measured blobs of paint down (areas in proportion to their parts) and works them
1197
+ * together with the knife, the way a painter mixes a pile on the palette.
1041
1198
  */
1042
1199
 
1043
1200
  type MixingPadTool = 'paint' | 'knife';
1044
1201
  declare const MIXING_PAD_WIDTH = 360;
1045
1202
  declare const MIXING_PAD_HEIGHT = 220;
1203
+ /** One paint in a `mix`: a colour and its parts. */
1204
+ interface PadMixPart {
1205
+ color: string;
1206
+ parts: number;
1207
+ }
1208
+ interface PadMixOptions {
1209
+ /** Centre of the pile in pad pixels (default the pad's centre). */
1210
+ x?: number;
1211
+ y?: number;
1212
+ /** Radius of the pile in pad pixels, 12..100 (default 40). */
1213
+ radius?: number;
1214
+ /** Knife passes over the pile, 1..20 (default 3): more passes, finer streaks around the mixed core. */
1215
+ passes?: number;
1216
+ /** Wipe the pile's area first (default true); false mixes into what is already there. */
1217
+ clear?: boolean;
1218
+ }
1046
1219
  declare class MixingPad {
1047
1220
  readonly width = 360;
1048
1221
  readonly height = 220;
@@ -1062,9 +1235,36 @@ declare class MixingPad {
1062
1235
  height: number;
1063
1236
  } | null;
1064
1237
  };
1238
+ /** Paints `points` with `brush` in `color` on the pad. */
1239
+ private run;
1065
1240
  /** The average colour within `radius` pixels of (x, y), or `null` where the pad is bare. */
1066
1241
  pick(x: number, y: number, radius?: number): string | null;
1067
1242
  clear(): void;
1243
+ /**
1244
+ * Mixes paints the way a painter does on a palette: blobs whose areas are in proportion to
1245
+ * their parts, side by side around (x, y), spiral knife passes that fold them together, and then the centre worked until it is fully mixed: a soft-edged core
1246
+ * of the Kubelka-Munk mix of the parts (`mixPigments`), with partly mixed streaks around it.
1247
+ * Returns that colour (picked back from the centre) and the pile's bounds.
1248
+ *
1249
+ * The core is laid, not smeared, because smearing alone drifts: the knife samples in RGB and a
1250
+ * mixed colour's scattering is re-estimated from its luminance, so repeated small mixes favour
1251
+ * the lighter paint (see GOTCHAS).
1252
+ */
1253
+ mix(paints: readonly PadMixPart[], options?: PadMixOptions): {
1254
+ color: string | null;
1255
+ bounds: {
1256
+ x: number;
1257
+ y: number;
1258
+ width: number;
1259
+ height: number;
1260
+ };
1261
+ };
1262
+ /** The pad's pixels as a PNG data URL (with alpha), for saving. */
1263
+ toPng(): string;
1264
+ /** Replaces the pad with a PNG (data URL or base64) of the pad's size, as `toPng` writes. */
1265
+ fromPng(png: string): void;
1266
+ /** Whether nothing is on the pad. */
1267
+ get empty(): boolean;
1068
1268
  /** The pad over white, `width × height` RGBA. */
1069
1269
  pixels(): Uint8ClampedArray;
1070
1270
  }
@@ -1125,6 +1325,8 @@ interface PaintEditorState {
1125
1325
  swatches: readonly Swatch[];
1126
1326
  /** Changes whenever the mixing pad does. */
1127
1327
  mixingPadVersion: number;
1328
+ /** Mixing pads kept by name (saved with the document). */
1329
+ savedMixingPads: readonly SavedMixingPadInfo[];
1128
1330
  /** A stroke is in progress. */
1129
1331
  stroking: boolean;
1130
1332
  /** Increments on every change. */
@@ -1166,6 +1368,8 @@ interface PaintStrokeInit {
1166
1368
  x: number;
1167
1369
  y: number;
1168
1370
  };
1371
+ /** What the brush carries at the start (default: from the editor's brush load); replays pass the recorded one. */
1372
+ load?: MixerLoad;
1169
1373
  }
1170
1374
  interface LayerUpdate {
1171
1375
  name?: string;
@@ -1210,6 +1414,11 @@ interface PixelImage {
1210
1414
  height: number;
1211
1415
  data: Uint8ClampedArray;
1212
1416
  }
1417
+ /** A mixing pad kept by name. */
1418
+ interface SavedMixingPadInfo {
1419
+ id: string;
1420
+ name: string;
1421
+ }
1213
1422
  declare class PaintEditorController {
1214
1423
  private doc;
1215
1424
  private extensions;
@@ -1218,6 +1427,8 @@ declare class PaintEditorController {
1218
1427
  private readonly historyLimit;
1219
1428
  private brush;
1220
1429
  private userBrushes;
1430
+ /** The library list in the state (the same array until the user brushes change, so thumbnails are not redrawn). */
1431
+ private library;
1221
1432
  private color;
1222
1433
  private secondaryColor;
1223
1434
  private mixerLoad;
@@ -1236,6 +1447,8 @@ declare class PaintEditorController {
1236
1447
  private guides;
1237
1448
  private swatches;
1238
1449
  private readonly pad;
1450
+ private savedPads;
1451
+ private nextPadId;
1239
1452
  private tracingVersion;
1240
1453
  private tracingCache;
1241
1454
  private readonly imagePixels;
@@ -1372,6 +1585,11 @@ declare class PaintEditorController {
1372
1585
  resetView(): PaintView;
1373
1586
  setHand(hand: Hand): Hand;
1374
1587
  private strokeBrush;
1588
+ /**
1589
+ * After a stroke: a Mixer Brush that loads colour keeps its whole load; any other dirty brush
1590
+ * keeps only the paint it carries (the reservoir stays the Mixer Brush's).
1591
+ */
1592
+ private keepLoad;
1375
1593
  /** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
1376
1594
  protected strokeCopies(): Affine[];
1377
1595
  private sessionFor;
@@ -1531,6 +1749,35 @@ declare class PaintEditorController {
1531
1749
  setColor?: boolean;
1532
1750
  }): string | null;
1533
1751
  clearMixingPad(): void;
1752
+ /**
1753
+ * Mixes paints on the pad like a painter: blobs in proportion to their `parts`, knife passes,
1754
+ * and a fully mixed core of the Kubelka-Munk mix (see `MixingPad.mix`). With `setColor` the
1755
+ * mix becomes the paint colour. Returns the mixed colour and the pile's bounds.
1756
+ */
1757
+ mixOnPad(paints: readonly {
1758
+ color: string;
1759
+ parts: number;
1760
+ }[], options?: PadMixOptions & {
1761
+ setColor?: boolean;
1762
+ }): {
1763
+ color: string;
1764
+ bounds: RasterRect;
1765
+ };
1766
+ /** The mixing pad as a PNG data URL (with alpha). */
1767
+ mixingPadPng(): string;
1768
+ /** Replaces the mixing pad with a PNG (data URL or base64, 360 × 220). */
1769
+ setMixingPadPng(png: string): void;
1770
+ /** The mixing pads kept by name. */
1771
+ listMixingPads(): SavedMixingPadInfo[];
1772
+ /** Keeps the mixing pad as it is now under `name` (saved with the document). Returns its id. */
1773
+ saveMixingPad(name?: string): SavedMixingPadInfo;
1774
+ private requirePad;
1775
+ /** Puts a kept pad back on the mixing pad (replacing what is there). */
1776
+ loadMixingPad(id: string): SavedMixingPadInfo;
1777
+ /** Forgets a kept pad. */
1778
+ removeMixingPad(id: string): SavedMixingPadInfo;
1779
+ private mixingPadJSON;
1780
+ private readMixingPad;
1534
1781
  /** A layer's tile surface (live while stroking), for thumbnails. */
1535
1782
  layerSurface(id: string): TiledSurface | null;
1536
1783
  }
@@ -1784,5 +2031,37 @@ interface BlendMenuGroup {
1784
2031
  * description as a tooltip. Both bindings render this as `<optgroup>`s.
1785
2032
  */
1786
2033
  declare const BLEND_MENU_GROUPS: readonly BlendMenuGroup[];
2034
+ /** The engine-specific part of the brush panel: which sliders and toggles the active brush has. */
2035
+ interface BrushEngineControls {
2036
+ /** Section heading. */
2037
+ title: string;
2038
+ sliders: readonly SliderControl[];
2039
+ /** Where the RGB / pigment choice is stored, or `null` when the engine has none. */
2040
+ mixingPath: 'behavior.mixing' | 'blender.mixing' | 'smudge.mixing' | null;
2041
+ /** The dirty-brush toggle (`behavior.dirty`). */
2042
+ dirty: boolean;
2043
+ /** The multi-colour controls (`behavior.multicolor`). */
2044
+ multicolor: boolean;
2045
+ /** Load / Clean buttons and a swatch of what the brush carries. */
2046
+ load: boolean;
2047
+ }
2048
+ /**
2049
+ * The controls the active brush's engine adds to the brush panel (`null` for the eraser):
2050
+ * blender strength and blend distance; smudge strength and length; the Mixer Brush's pick-up,
2051
+ * mix and load; a dab brush's pick-up and mix ratios. Every engine but the eraser has an RGB /
2052
+ * pigment choice; dab and mixer brushes also have the dirty and multi-colour controls. Pick-up
2053
+ * and mix always go through `behavior`, which keeps a Mixer Brush's `mixer` block in sync.
2054
+ */
2055
+ declare function brushEngineControls(brush: Pick<PaintBrush, 'engine'>): BrushEngineControls | null;
2056
+ /** The multi-colour sliders for a mode: softness, and dabs per colour in `dab` mode. */
2057
+ declare function multiColorSliders(mode: string): readonly SliderControl[];
2058
+ /** A brush change setting `path` (e.g. `smudge.mixing`, `behavior.dirty`) to any value, keeping the rest of its block. */
2059
+ declare function patchBrushValue(brush: PaintBrush, path: string, value: unknown): PaintBrushInput;
2060
+ /** What the brush carries (picked-up paint, else loaded paint) as #rrggbb, or `null` when it is clean and empty. */
2061
+ declare function brushLoadColor(load: MixerLoad): string | null;
2062
+ /** Sample sizes (square, in pad pixels) for picking from the mixing pad. */
2063
+ declare const PAD_SAMPLE_SIZES: readonly number[];
2064
+ /** The averaging radius `mixingPadPick` takes for a sample size: 1 → 0, 3 → 1, 5 → 2 … */
2065
+ declare function padSampleRadius(size: number): number;
1787
2066
 
1788
- export { type Affine, BLEND_MENU_GROUPS, BRUSH_ID, BRUSH_ROTATIONS, BRUSH_SLIDERS, BUILTIN_BRUSHES, type BlendMenuGroup, BlendMode, BlendModeInfo, type BrushDynamics, type BrushLibraryFile, type BrushRotation, type BrushThumbnail, type ColorWheel, DEFAULT_BRUSH_ID, DEFAULT_DPI, DEFAULT_DYNAMICS, DEFAULT_PAPER, DEFAULT_SWATCHES, DEFAULT_VIEW, DryStroke, type DryStrokeOptions, type FillLayerOptions, GRAIN_PATTERNS, GRAIN_TILE, type GrainPattern, type GrainTuning, HARMONY_KINDS, type Hand, type HarmonyColor, type HarmonyKind, IDENTITY_AFFINE, IDENTITY_COPY, type ImageEncoder, type ImageFormat, type ImageNode, type LayerUpdate, type Linear2, type LoadedPaintDocument, MAX_CANVAS_SIDE, MAX_COORDINATE, MAX_IMAGE_SIDE, MAX_ZOOM, MIN_CANVAS_SIDE, MIN_ZOOM, MIXING_PAD_HEIGHT, MIXING_PAD_WIDTH, MixingPad, type MixingPadTool, PAINT_BRUSH_CATEGORIES, PAINT_BRUSH_ENGINES, PAINT_FORMAT, PAINT_SCHEMA_VERSION, PAINT_TOOLS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_NODE_ID, PAPER_SLIDERS, type Paint2DContext, type PaintAgentCommand, type PaintAssetStore, type PaintBrush, type PaintBrushCategory, type PaintBrushEngine, type PaintBrushInput, type PaintCanvas, PaintCanvasPresenter, PaintDirector, type PaintDirectorOptions, PaintDocumentError, type PaintDocumentExtensions, type PaintDocumentState, PaintEditorController, PaintEditorError, type PaintEditorOptions, type PaintEditorState, type PaintEditorToolProviderOptions, PaintGesture, type PaintGroupLayer, type PaintGuides, type PaintLayer, type PaintLayerInfo, type PaintPerformanceResult, type PaintPixelLayer, type PaintPointerEvent, PaintPresence, type PaintPresenceActor, type PaintPresenceState, type PaintStrokeInit, type PaintStrokeResult, PaintStrokeSession, type PaintTool, type PaintView, type PaperFamily, PaperGrain, type PaperNode, type PaperSettings, type PaperTiling, type PenDab, PenDabWalker, type PenPoint, PenSmoother, type PerspectiveGuide, PerspectiveSnapper, type PixelImage, type Point2, type PointerSample, type RecordedStroke, type ReferenceImage, type RenderOptions, type ReplayOptions, type Rgb255, SNAP_DECIDE_DISTANCE, SYMMETRY_MODES, type SerializeOptions, type SerializedPaintDocument, type SliderControl, type StrokeCopy, type StrokeSessionHost, type StrokeSessionInit, type StrokeVariation, type Swatch, type SymmetryGuide, type SymmetryMode, TRACING_NODE_ID, TileableNoise, type TracingImage, type ViewLayer, type ViewportBox, affineCopy, applyAffine, brushToJSON, clonePaper, colorHarmony, composeAffine, createPaintCompositor, createPaintDocument, createPaintEditorToolProvider, createPaintLayer, decodeImageBlob, defaultGuides, defaultImageEncoder, deserializePaintDocument, deserializePaintDocumentAsync, documentExtensions, documentToScreen, downloadBytes, downsampleRgba, fitZoom, grainCatch, grainHeights, guideOverlay, handlePaintShortcut, hash2, hexToRgb8, hslToRgb, hsvToRgb, hueToWheel, invertAffine, isPalmTouch, mapPenPoint, mix32, mixerLoadForStroke, mixerTip, newPaintLayerId, normalizeBrushStroke, normalizeCanvas, normalizeDynamics, normalizePaintBrush, normalizePaper, normalizePerspective, normalizeRotation, normalizeSymmetry, normalizeView, paperGrain, paperKey, paperNode, paperPreset, parseBrushLibrary, parseHexColor, patchPath, penOrientation, perspectiveDirections, perspectiveGuideLines, pointerToPenPoint, rand01, randSigned, readPath, renderBrushThumbnail, renderColorWheel, renderPaper, rgbToHexColor, rgbToHsl, rgbToHsv, samplePressure, screenToDocument, seedFromString, serializePaintDocument, serializePaintDocumentExternal, strokeBounds, strokeDuration, strokeSchedule, sweepPath, symmetryAxisAngles, symmetryAxisSegments, symmetryCopies, thumbnailStroke, tiltToOrientation, toStrokePoint, variationAffine, varyStroke, viewCssTransform, viewMatrix, viewPenToDocument, wheelAngle, wheelPick, wheelPoint, wheelToHue };
2067
+ export { type Affine, BLEND_MENU_GROUPS, BRUSH_ID, BRUSH_ROTATIONS, BRUSH_SLIDERS, BUILTIN_BRUSHES, type BlendMenuGroup, BlendMode, BlendModeInfo, type BrushBehavior, type BrushBehaviorInput, type BrushDynamics, type BrushEngineControls, type BrushLibraryFile, type BrushRotation, type BrushThumbnail, type ColorWheel, DEFAULT_BEHAVIOR, DEFAULT_BRUSH_ID, DEFAULT_DPI, DEFAULT_DYNAMICS, DEFAULT_MULTI_COLOR, DEFAULT_PAPER, DEFAULT_SWATCHES, DEFAULT_VIEW, DryStroke, type DryStrokeOptions, type FillLayerOptions, GRAIN_PATTERNS, GRAIN_TILE, type GrainPattern, type GrainTuning, HARMONY_KINDS, type Hand, type HarmonyColor, type HarmonyKind, IDENTITY_AFFINE, IDENTITY_COPY, type ImageEncoder, type ImageFormat, type ImageNode, type LayerUpdate, type Linear2, type LoadedPaintDocument, MAX_CANVAS_SIDE, MAX_COORDINATE, MAX_IMAGE_SIDE, MAX_ZOOM, MIN_CANVAS_SIDE, MIN_ZOOM, MIXING_PAD_HEIGHT, MIXING_PAD_WIDTH, MULTI_COLOR_MODES, MixingPad, type MixingPadTool, type MultiColorMode, type MultiColorSettings, PAD_SAMPLE_SIZES, PAINT_BRUSH_CATEGORIES, PAINT_BRUSH_ENGINES, PAINT_FORMAT, PAINT_SCHEMA_VERSION, PAINT_TOOLS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_NODE_ID, PAPER_SLIDERS, type PadMixOptions, type PadMixPart, type Paint2DContext, type PaintAgentCommand, type PaintAssetStore, type PaintBrush, type PaintBrushCategory, type PaintBrushEngine, type PaintBrushInput, type PaintCanvas, PaintCanvasPresenter, PaintDabStroke, type PaintDabStrokeOptions, PaintDirector, type PaintDirectorOptions, PaintDocumentError, type PaintDocumentExtensions, type PaintDocumentState, PaintEditorController, PaintEditorError, type PaintEditorOptions, type PaintEditorState, type PaintEditorToolProviderOptions, PaintGesture, type PaintGroupLayer, type PaintGuides, type PaintLayer, type PaintLayerInfo, type PaintPerformanceResult, type PaintPixelLayer, type PaintPointerEvent, PaintPresence, type PaintPresenceActor, type PaintPresenceState, type PaintStrokeInit, type PaintStrokeResult, PaintStrokeSession, type PaintTool, type PaintView, type PaperFamily, PaperGrain, type PaperNode, type PaperSettings, type PaperTiling, type PenDab, PenDabWalker, type PenPoint, PenSmoother, type PerspectiveGuide, PerspectiveSnapper, type PixelImage, type Point2, type PointerSample, type RecordedStroke, type ReferenceImage, type RenderOptions, type ReplayOptions, type Rgb255, SNAP_DECIDE_DISTANCE, SYMMETRY_MODES, type SavedMixingPadInfo, type SerializeOptions, type SerializedPaintDocument, type SliderControl, type StrokeCopy, type StrokeSessionHost, type StrokeSessionInit, type StrokeVariation, type Swatch, type SymmetryGuide, type SymmetryMode, TRACING_NODE_ID, TileableNoise, type TracingImage, type ViewLayer, type ViewportBox, affineCopy, applyAffine, brushEngineControls, brushLoadColor, brushLoadForStroke, brushToJSON, clonePaper, colorHarmony, composeAffine, createPaintCompositor, createPaintDocument, createPaintEditorToolProvider, createPaintLayer, decodeImageBlob, defaultGuides, defaultImageEncoder, deserializePaintDocument, deserializePaintDocumentAsync, documentExtensions, documentToScreen, downloadBytes, downsampleRgba, fitZoom, grainCatch, grainHeights, guideOverlay, handlePaintShortcut, hash2, hexToRgb8, hslToRgb, hsvToRgb, hueToWheel, invertAffine, isPalmTouch, isWetCapable, mapPenPoint, mix32, mixerLoadForStroke, mixerTip, multiColorForDab, multiColorGradient, multiColorPalette, multiColorSliders, newPaintLayerId, normalizeBehavior, normalizeBrushStroke, normalizeCanvas, normalizeDynamics, normalizeMultiColor, normalizePaintBrush, normalizePaper, normalizePerspective, normalizeRotation, normalizeSymmetry, normalizeView, padSampleRadius, paperGrain, paperKey, paperNode, paperPreset, parseBrushLibrary, parseHexColor, patchBrushValue, patchPath, penOrientation, perspectiveDirections, perspectiveGuideLines, pointerToPenPoint, rand01, randSigned, readPath, renderBrushThumbnail, renderColorWheel, renderPaper, rgbToHexColor, rgbToHsl, rgbToHsv, samplePressure, screenToDocument, seedFromString, serializePaintDocument, serializePaintDocumentExternal, strokeBounds, strokeDuration, strokeSchedule, sweepPath, symmetryAxisAngles, symmetryAxisSegments, symmetryCopies, thumbnailStroke, tiltToOrientation, toStrokePoint, usesPaintExchange, variationAffine, varyStroke, viewCssTransform, viewMatrix, viewPenToDocument, wheelAngle, wheelPick, wheelPoint, wheelToHue };