@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
@@ -0,0 +1,486 @@
1
+ import { B as BlendMode, R as RasterRect, T as TileKey, P as Pigment } from './surface-C8RRg_Bh.js';
2
+
3
+ /**
4
+ * The brush engine (a subset of photocraft's `crates/paint`): a computed round tip with size,
5
+ * hardness, angle, roundness and spacing; pressure to size and opacity; smoothing; and Photoshop's
6
+ * opacity-versus-flow build-up. Strokes are data, so one replays the same way for undo, agents
7
+ * and tests.
8
+ *
9
+ * - Points are walked into dabs at `spacing × diameter` (`walkDabs`).
10
+ * - Each dab's coverage (`dabCoverage`, photocraft's measured Photoshop profile) builds up in a
11
+ * stroke coverage map with flow towards the dab's opacity ceiling: `c ← c + v·(ceil − c)`.
12
+ * - The stroke composites once at the stroke opacity over the layer as it was before the stroke,
13
+ * so overlapping dabs never exceed the opacity.
14
+ */
15
+
16
+ /** One input sample. Missing pressure is a mouse (1). */
17
+ interface StrokePoint {
18
+ x: number;
19
+ y: number;
20
+ /** 0..1 (default 1). */
21
+ pressure?: number;
22
+ /** Milliseconds (optional; kept for replays). */
23
+ t?: number;
24
+ }
25
+ interface BrushSettings {
26
+ /** Diameter in pixels (1..2500, default 20). */
27
+ size: number;
28
+ /** 0 (soft) .. 1 (hard, default). */
29
+ hardness: number;
30
+ /** Dab spacing as a fraction of the diameter (0.01..10, default 0.1). */
31
+ spacing: number;
32
+ /** Tip angle in degrees (default 0). */
33
+ angle: number;
34
+ /** Tip roundness 0.01..1 (default 1). */
35
+ roundness: number;
36
+ /** Stroke opacity ceiling 0..1 (default 1). */
37
+ opacity: number;
38
+ /** Build-up per dab 0..1 (default 1). */
39
+ flow: number;
40
+ /** Input smoothing 0..0.95 (default 0). */
41
+ smoothing: number;
42
+ /** Pressure controls size (default true). */
43
+ pressureSize: boolean;
44
+ /** Pressure controls opacity (default false). */
45
+ pressureOpacity: boolean;
46
+ /** How the stroke paints onto the layer (default normal). */
47
+ blend: BlendMode;
48
+ }
49
+ declare const DEFAULT_BRUSH: Readonly<BrushSettings>;
50
+ declare const MAX_BRUSH_SIZE = 2500;
51
+ /** A brush with every field in range (missing fields from `DEFAULT_BRUSH`). */
52
+ declare function normalizeBrush(raw?: Partial<BrushSettings>): BrushSettings;
53
+ /** The core radius and shoulder width (pixels) of a round tip of `radius` and `hardness`. */
54
+ declare function tipShape(radius: number, hardness: number): [number, number];
55
+ /** Opacity of a round tip at distance `d` from its centre. */
56
+ declare function tipFalloff(d: number, radius: number, hardness: number): number;
57
+ /** How far from its centre a round tip still paints. */
58
+ declare function tipReach(radius: number, hardness: number): number;
59
+ /** Coverage of a round dab at distance `d` from its centre. */
60
+ declare function dabCoverage(d: number, radius: number, hardness: number): number;
61
+ interface Dab {
62
+ x: number;
63
+ y: number;
64
+ radius: number;
65
+ /** Flow (per-dab build-up). */
66
+ alpha: number;
67
+ /** Opacity ceiling this dab builds up to. */
68
+ opacity: number;
69
+ /** Radians. */
70
+ angle: number;
71
+ roundness: number;
72
+ }
73
+ /** Exponential smoothing of input points; the first and last point are kept. */
74
+ declare function smoothPoints(points: readonly StrokePoint[], amount: number): StrokePoint[];
75
+ /**
76
+ * Walks points into dabs (incrementally: feed points in any chunks and get the same dabs). The
77
+ * first point is a dab; then one every `spacing × diameter` along the path.
78
+ */
79
+ declare class DabWalker {
80
+ readonly brush: BrushSettings;
81
+ private last;
82
+ private nextAt;
83
+ private pendingFirst;
84
+ constructor(brush: BrushSettings);
85
+ private diameter;
86
+ private stepLen;
87
+ private dab;
88
+ push(p: StrokePoint, out: Dab[]): void;
89
+ finish(out: Dab[]): void;
90
+ }
91
+ /** All dabs of a point list (smoothing applied). */
92
+ declare function walkDabs(points: readonly StrokePoint[], brush: BrushSettings): Dab[];
93
+ /** The pixel rectangle a dab can touch. */
94
+ declare function dabRect(d: Dab, hardness: number): RasterRect;
95
+ /**
96
+ * Rasterizes one dab over `rect` into `out` (rect.width × rect.height values, coverage × flow).
97
+ * Small tips (radius below 3 px) take each pixel's covered area from a 4×4 grid.
98
+ */
99
+ declare function rasterizeDab(d: Dab, hardness: number, rect: RasterRect, out: Float32Array): void;
100
+ /** A stroke's accumulated coverage, in 256 px tiles over document space. */
101
+ declare class CoverageMap {
102
+ readonly tiles: Map<number, Float32Array<ArrayBufferLike>>;
103
+ bounds: RasterRect | null;
104
+ private dirty;
105
+ get(x: number, y: number): number;
106
+ /** Builds up `vals` (over `rect`) with flow towards `ceil`: `c ← c + v·(ceil − c)`. */
107
+ accumulate(rect: RasterRect, vals: Float32Array, ceil: number): void;
108
+ /** Tiles changed since the last call. */
109
+ takeDirty(): TileKey[];
110
+ }
111
+ /** Paints a stroke's dabs into a coverage map, incrementally. */
112
+ declare class StrokeCoverage {
113
+ readonly brush: BrushSettings;
114
+ readonly coverage: CoverageMap;
115
+ private walker;
116
+ private smoothing;
117
+ private smoothPos;
118
+ private buf;
119
+ dabCount: number;
120
+ constructor(brush: BrushSettings);
121
+ /** Feeds input points; returns the document rectangles the new dabs touched. */
122
+ push(points: readonly StrokePoint[]): RasterRect[];
123
+ /** Ends the stroke (a single click still paints one dab). */
124
+ finish(last?: StrokePoint): RasterRect[];
125
+ private smooth;
126
+ private paint;
127
+ }
128
+ /** How a stroke changes pixels. */
129
+ interface StrokeApply {
130
+ /** `paint` puts colour down; `erase` removes alpha (or, with transparency locked, paints `background`). */
131
+ mode: 'paint' | 'erase';
132
+ /** RGBA 0..255 (paint colour, or the mask grey in `r`). */
133
+ color: readonly [number, number, number, number];
134
+ /** RGBA 0..255 the eraser paints on a transparency-locked layer (and on masks). */
135
+ background: readonly [number, number, number, number];
136
+ opacity: number;
137
+ blend: BlendMode;
138
+ lockTransparency: boolean;
139
+ /** Selection coverage (document space, canvas-sized), or `null`. */
140
+ selection: {
141
+ width: number;
142
+ height: number;
143
+ data: ArrayLike<number>;
144
+ } | null;
145
+ /** Layer offset (target pixels are in layer space). */
146
+ offset: {
147
+ x: number;
148
+ y: number;
149
+ };
150
+ }
151
+ /**
152
+ * Composites the stroke's coverage over `tileData` (the target's tile (tx, ty) as it was before
153
+ * the stroke, in layer space; RGBA or one byte for masks) and returns the new tile data. With
154
+ * `into` (the tile's previous result) and `region` (document space), only `region` is recomputed
155
+ * and the rest of `into` is kept.
156
+ */
157
+ declare function applyStrokeToTile(coverage: CoverageMap, tileData: Uint8ClampedArray, channels: 1 | 4, tx: number, ty: number, s: StrokeApply, options?: {
158
+ into?: Uint8ClampedArray;
159
+ region?: RasterRect;
160
+ }): Uint8ClampedArray;
161
+ /** Tile coordinates of coverage tile keys (for callers that track dirty tiles). */
162
+ declare function coverageTileCoord(key: TileKey): {
163
+ tx: number;
164
+ ty: number;
165
+ };
166
+
167
+ /**
168
+ * The pick-up / mix exchange between a loaded brush and the paint under it: the one mechanism
169
+ * every wet-capable brush shares. The Mixer Brush (`MixerStroke`) and the paint editor's
170
+ * wet-capable brushes run on it now; the wet layer (phase 3) plugs in as another `PaintMedium`.
171
+ *
172
+ * Two halves:
173
+ *
174
+ * - `PaintExchange` is what the brush carries: a **reservoir** of loaded paint (with an amount
175
+ * that drains per dab) and a **well** of paint picked up from the canvas. Per dab it
176
+ * 1. picks up: the well moves towards the colour under the dab by the pick-up rate times how
177
+ * much of the footprint holds paint (`pickUp`);
178
+ * 2. mixes the dab's paint: reservoir weight `amount × (1 − mix)`, well weight `mix`
179
+ * (`paint`);
180
+ * 3. drains the reservoir (`drain`).
181
+ * Every mix is RGB or Kubelka-Munk pigment (`mixing`), so a blue brush through yellow goes
182
+ * green.
183
+ * - `PaintMedium` is what the brush touches: `sample` (the colour and contact under a footprint)
184
+ * and `deposit` (lay paint at per-pixel amounts). `TileMedium` is the raster layer's pixels.
185
+ * A wet layer implements the same two calls over its wet pigment and water (sampling only wet
186
+ * paint, so pick-up lifts what is still wet), and nothing in `PaintExchange` changes.
187
+ *
188
+ * The ratios a user sets are `pickup` (pick-up ratio, 0..1) and `mix` (mix ratio, 0..1);
189
+ * `exchangeRates` turns them into per-dab rates with the Mixer Brush's constants, so a Mixer
190
+ * Brush's `wet` and `mix` are exactly these two ratios. Everything is data in, pixels out.
191
+ */
192
+
193
+ /** How colours mix: straight sRGB averages, or like paint (Kubelka-Munk). */
194
+ type MixingModel = 'rgb' | 'pigment';
195
+ declare const MIXING_MODELS: readonly MixingModel[];
196
+ /** An sRGB colour, 0..1 per channel. */
197
+ type RgbTriple = [number, number, number];
198
+ /** What a brush carries between dabs and strokes. Colours are straight sRGB, 0..1. */
199
+ interface MixerLoad {
200
+ /** Loaded paint, or `null` when the brush is unloaded. */
201
+ readonly reservoir: readonly [number, number, number] | null;
202
+ /** Reservoir left, 0..1. */
203
+ readonly amount: number;
204
+ /** Colour picked up from the canvas (the well), or `null` when the brush is clean. */
205
+ readonly pickup: readonly [number, number, number] | null;
206
+ }
207
+ declare const EMPTY_MIXER_LOAD: MixerLoad;
208
+ /** Fraction of the way the well moves towards the canvas colour per dab at a pick-up ratio of 1. */
209
+ declare const MIXER_PICKUP_RATE = 0.25;
210
+ /** Fraction of the reservoir that drains per dab at a load of 0. */
211
+ declare const MIXER_DRAIN = 0.08;
212
+ /** Per-dab rates of an exchange. */
213
+ interface ExchangeRates {
214
+ /** How far the well moves towards the canvas colour per dab at full contact (0: lifts nothing). */
215
+ pickup: number;
216
+ /** The well's weight in each dab's paint; the reservoir gets `amount × (1 − mix)`. */
217
+ mix: number;
218
+ /** Fraction of the reservoir lost per dab. */
219
+ drain: number;
220
+ mixing: MixingModel;
221
+ }
222
+ /**
223
+ * Per-dab rates from the ratios a user sets: pick-up ratio `pickup` (0 dry .. 1), mix ratio
224
+ * `mix` (0 .. 1, scaled by the pick-up ratio: a brush that lifts nothing has nothing to mix) and
225
+ * `load` (1 never drains). The Mixer Brush passes its `wet`, `mix` and `load`.
226
+ */
227
+ declare function exchangeRates(pickup: number, mix: number, load: number, mixing: MixingModel): ExchangeRates;
228
+ /** What a footprint holds: its average paint colour and how much of it holds paint (0..1). */
229
+ interface PaintSample {
230
+ rgb: RgbTriple;
231
+ /** Paint-weighted share of the footprint (alpha × coverage over coverage). */
232
+ contact: number;
233
+ }
234
+ /** The brush side of the exchange: reservoir, amount and well, mixed per dab. */
235
+ declare class PaintExchange {
236
+ readonly rates: ExchangeRates;
237
+ private reservoir;
238
+ private amount;
239
+ private well;
240
+ private readonly pig;
241
+ private readonly out;
242
+ constructor(rates: ExchangeRates, load: MixerLoad);
243
+ /** What the brush carries now. */
244
+ get load(): MixerLoad;
245
+ /** The colour picked up so far, or `null`. */
246
+ get pickup(): Readonly<RgbTriple> | null;
247
+ /** The prepared pigment of the last `paint` colour (pigment mixing only), for `deposit`. */
248
+ get paintPigment(): Pigment | null;
249
+ /** Recolours the reservoir without refilling it (a multi-coloured brush, per dab). */
250
+ setReservoirColor(r: number, g: number, b: number): void;
251
+ /** `a` mixed `t` of the way to `b` into `this.out`, by the mixing model. */
252
+ private mix;
253
+ /** `a` mixed `t` of the way to `b` (sRGB 0..1) by the mixing model, into `out`. */
254
+ mixColors(a: readonly number[], b: readonly number[], t: number, out: number[]): number[];
255
+ /** 1. Pick-up: the well moves towards the sampled colour (or takes it, when clean). */
256
+ pickUp(sample: PaintSample | null): void;
257
+ /**
258
+ * 2. The dab's paint: the reservoir mixed with the well, written to `out`. Returns the total
259
+ * weight (≤ 0: nothing to lay down); deposit `min(1, weight)` of it. With pigment mixing,
260
+ * `paintPigment` is the result's pigment afterwards.
261
+ */
262
+ paint(out: RgbTriple): number;
263
+ /** 3. The reservoir drains. */
264
+ drain(): void;
265
+ }
266
+ /** The pixels a brush reads and writes (RGBA tiles in layer space). */
267
+ interface PaintTileTarget {
268
+ /** The tile (TILE × TILE × 4) at (tx, ty) as it is now, or `null` when it is fully transparent. */
269
+ readTile(tx: number, ty: number): Uint8ClampedArray | null;
270
+ /** A writable tile at (tx, ty), returned by `readTile` from then on. */
271
+ writeTile(tx: number, ty: number): Uint8ClampedArray;
272
+ /** Layer offset: layer pixel = document pixel − offset. */
273
+ offset: {
274
+ x: number;
275
+ y: number;
276
+ };
277
+ /** Keep alpha and paint only where the layer has pixels. */
278
+ lockTransparency: boolean;
279
+ /** Selection coverage (document space, canvas-sized), or `null`. */
280
+ selection: {
281
+ width: number;
282
+ height: number;
283
+ data: ArrayLike<number>;
284
+ } | null;
285
+ }
286
+ /** The canvas side of the exchange: where a brush picks paint up and lays it down. */
287
+ interface PaintMedium {
288
+ /** The paint under a footprint (`cov` over `rect`, document space), or `null` where there is none. */
289
+ sample(rect: RasterRect, cov: ArrayLike<number>): PaintSample | null;
290
+ /**
291
+ * Lays paint over the footprint at `dep × cov` per pixel. The colour is `paint` (with its
292
+ * prepared pigment for pigment mixing), or per pixel from `field` (3 floats per pixel of
293
+ * `rect`, sRGB 0..1) when given.
294
+ */
295
+ deposit(rect: RasterRect, cov: ArrayLike<number>, dep: number, paint: Readonly<RgbTriple>, paintPigment: Pigment | null, field?: Float32Array | null): void;
296
+ }
297
+ interface TileMediumOptions {
298
+ /**
299
+ * How deposits combine with the pixels. Missing: the exchange's mixing (source-over, or a
300
+ * pigment mix with pigment mixing), as the Mixer Brush paints. Otherwise any blend mode, as a
301
+ * brush tip's `blend`.
302
+ */
303
+ blend?: BlendMode;
304
+ }
305
+ /** A raster layer's pixels as a `PaintMedium` (through live tiles). */
306
+ declare class TileMedium implements PaintMedium {
307
+ readonly target: PaintTileTarget;
308
+ private readonly options;
309
+ private tx;
310
+ private ty;
311
+ private tile;
312
+ private writable;
313
+ private readonly canvasPig;
314
+ private readonly paintPig;
315
+ private paintKey;
316
+ private readonly cb;
317
+ private readonly src;
318
+ private readonly o;
319
+ constructor(target: PaintTileTarget, options?: TileMediumOptions);
320
+ /** Forgets the cached tile (call when another writer may have replaced tiles). */
321
+ reset(): void;
322
+ /**
323
+ * The tile holding document pixel (dx, dy) (`pixels`) and the pixel's byte index in it, or -1
324
+ * when reading an empty tile.
325
+ */
326
+ locate(dx: number, dy: number, write: boolean): number;
327
+ /** The tile `locate` last found. */
328
+ get pixels(): Uint8ClampedArray;
329
+ /** Selection coverage at a document pixel, 0..1 (1 without a selection). */
330
+ selectionAt(dx: number, dy: number): number;
331
+ sample(rect: RasterRect, cov: ArrayLike<number>): PaintSample | null;
332
+ deposit(rect: RasterRect, cov: ArrayLike<number>, dep: number, paint: Readonly<RgbTriple>, paintPigment: Pigment | null, field?: Float32Array | null): void;
333
+ }
334
+
335
+ /**
336
+ * The Mixer Brush: a reservoir of loaded paint plus a pickup well that takes up colour from the
337
+ * canvas under each dab (photocraft's behavioural model):
338
+ *
339
+ * - Each dab samples the canvas under its footprint (coverage- and alpha-weighted). The pickup
340
+ * colour `P` moves towards that sample by `PICKUP_RATE × wet` (scaled by the sample's coverage).
341
+ * - The dab's paint mixes the reservoir `R` (weight `amount × (1 − mix·wet)`) with the pickup
342
+ * (weight `mix·wet`) and lands at `flow × opacity` times the total weight, over the pixels as
343
+ * they are now, so earlier dabs of the same stroke are picked up again (smearing).
344
+ * - The reservoir drains per dab unless `load` is 1, so low-load strokes run dry. A dry brush
345
+ * (`wet` 0) paints only reservoir colour; an empty, wet brush smears canvas colour.
346
+ *
347
+ * `mixing: 'pigment'` does all three mixes (pickup, paint and deposit) with the Kubelka-Munk model
348
+ * of `./pigment`, so a blue brush dragged through yellow makes green instead of grey. Everything
349
+ * is data in, pixels out: the same canvas, points, settings and load replay identically.
350
+ */
351
+
352
+ /** Mixer Brush options. */
353
+ interface MixerSettings {
354
+ /** How much colour the brush picks up from the canvas (the pick-up ratio), 0 (dry) .. 1 (default 0.5). */
355
+ wet: number;
356
+ /** How much paint the reservoir holds, 0 .. 1 (default 0.5): below 1 it drains per dab and the stroke runs dry. */
357
+ load: number;
358
+ /** How far each dab's paint moves from the reservoir towards the picked-up colour (the mix ratio), 0 .. 1 (default 0.5). */
359
+ mix: number;
360
+ /** Refill the reservoir with the paint colour at the start of each stroke (default true). */
361
+ loadEachStroke: boolean;
362
+ /** Empty the pickup well after each stroke (default true); off keeps the brush dirty for the next stroke. */
363
+ cleanEachStroke: boolean;
364
+ /** `rgb` (default) or `pigment` (blue and yellow make green). */
365
+ mixing: MixingModel;
366
+ }
367
+ declare const DEFAULT_MIXER: Readonly<MixerSettings>;
368
+ /** Mixer settings with every field in range (missing fields from `DEFAULT_MIXER`). */
369
+ declare function normalizeMixer(raw?: Partial<MixerSettings>): MixerSettings;
370
+ /** The per-dab exchange rates of Mixer Brush settings (`wet` is the pick-up ratio, `mix` the mix ratio). */
371
+ declare function mixerRates(settings: MixerSettings): ExchangeRates;
372
+ /** The brush loaded full with `color` (sRGB 0..1); the pickup well is kept. */
373
+ declare function loadMixer(load: MixerLoad, color: readonly [number, number, number]): MixerLoad;
374
+ /** The brush with its pickup well emptied; the reservoir is kept. */
375
+ declare function cleanMixer(load: MixerLoad): MixerLoad;
376
+ /** The pixels a mixer stroke reads and writes (RGBA tiles in layer space). */
377
+ type MixerTarget = PaintTileTarget;
378
+ /**
379
+ * One Mixer Brush stroke. Feed points with `push` (any chunks give the same dabs), end with
380
+ * `finish`; `load` is the brush load after the dabs so far. Each call returns the document
381
+ * rectangles it touched. Runs on the shared pick-up / mix exchange (`./exchange`).
382
+ */
383
+ declare class MixerStroke {
384
+ readonly brush: BrushSettings;
385
+ dabCount: number;
386
+ bounds: RasterRect | null;
387
+ private walker;
388
+ private smoothPos;
389
+ private buf;
390
+ private readonly exchange;
391
+ private readonly medium;
392
+ private readonly paintColor;
393
+ constructor(brush: BrushSettings, settings: Partial<MixerSettings>, load: MixerLoad, target: MixerTarget);
394
+ /** The brush load after the dabs so far. */
395
+ get load(): MixerLoad;
396
+ push(points: readonly StrokePoint[]): RasterRect[];
397
+ finish(last?: StrokePoint): RasterRect[];
398
+ private smooth;
399
+ private paint;
400
+ private dab;
401
+ }
402
+
403
+ /**
404
+ * Blending tools on raster tiles, dab by dab (any dab walker can drive them):
405
+ *
406
+ * - **Blender** (`blenderDab`): every pixel under the tip moves towards the mix of the paint
407
+ * within `distance` of it, so neighbouring colours run together. The mix is taken through the
408
+ * pigment model: the neighbourhood's K/S spectra are averaged with Kubelka-Munk weights
409
+ * (alpha × scattering), so blue next to yellow blends into green, not grey. `rgb` averages
410
+ * straight colours instead.
411
+ * - **Smudge** (`SmudgeTip`): a finger that carries a patch of paint. Each dab lays the patch it
412
+ * carries over the pixels under the tip (`strength`) and picks up some of the paint that was
413
+ * there (`1 − length`), so the paint's pattern is dragged along the stroke and fades with distance.
414
+ * Optional pigment mixing for both steps.
415
+ *
416
+ * Both blend alpha too: blending next to bare canvas softens an edge, smudging drags paint out
417
+ * into it. A transparency-locked layer keeps its alpha. Pure data in, pixels out.
418
+ */
419
+
420
+ /** Blender options. */
421
+ interface BlenderSettings {
422
+ /** How far each pixel moves towards the neighbourhood mix per dab at full coverage, 0..1 (default 0.5). */
423
+ strength: number;
424
+ /** Blend distance: the neighbourhood's radius as a fraction of the tip diameter, 0..1 (default 0.35). */
425
+ distance: number;
426
+ /** `pigment` (default: Kubelka-Munk, blue next to yellow blends green) or `rgb`. */
427
+ mixing: MixingModel;
428
+ }
429
+ declare const DEFAULT_BLENDER: Readonly<BlenderSettings>;
430
+ /** Smudge options. */
431
+ interface SmudgeSettings {
432
+ /** How strongly the carried paint covers the pixels under the tip per dab, 0..1 (default 0.7). */
433
+ strength: number;
434
+ /** How far paint is dragged: the share of the carried patch kept per dab, 0..1 (default 0.85). */
435
+ length: number;
436
+ /** `rgb` (default) or `pigment` (smeared colours mix like paint). */
437
+ mixing: MixingModel;
438
+ }
439
+ declare const DEFAULT_SMUDGE: Readonly<SmudgeSettings>;
440
+ declare function normalizeBlender(raw?: Partial<BlenderSettings>, base?: BlenderSettings): BlenderSettings;
441
+ declare function normalizeSmudge(raw?: Partial<SmudgeSettings>, base?: SmudgeSettings): SmudgeSettings;
442
+ /** The neighbourhood radius in pixels of a blender dab of `radius`. */
443
+ declare function blendRadius(settings: BlenderSettings, radius: number): number;
444
+ /** Reusable buffers for `blenderDab` (one per stroke). */
445
+ interface BlenderScratch {
446
+ sums: Float64Array;
447
+ mix: Pigment;
448
+ out: number[];
449
+ }
450
+ declare function createBlenderScratch(): BlenderScratch;
451
+ /**
452
+ * One blender dab: `cov` is the tip's coverage over `rect` (document space, 0..1), `opacity` the
453
+ * dab's (pressure) opacity, `radius` its radius in pixels. Reads the neighbourhood as it was
454
+ * before the dab, then writes every covered pixel.
455
+ */
456
+ declare function blenderDab(medium: TileMedium, rect: RasterRect, cov: ArrayLike<number>, opacity: number, radius: number, settings: BlenderSettings, scratch: BlenderScratch): void;
457
+ /** A dab as the smudge needs it: centre, radius (px) and pressure opacity. */
458
+ interface SmudgeDab {
459
+ x: number;
460
+ y: number;
461
+ radius: number;
462
+ opacity: number;
463
+ }
464
+ /**
465
+ * A smudging finger: carries a patch of paint (straight RGBA, `size × size` cells over the tip,
466
+ * in canvas orientation) from dab to dab. The first dab only picks the patch up.
467
+ */
468
+ declare class SmudgeTip {
469
+ readonly settings: SmudgeSettings;
470
+ private readonly size;
471
+ private readonly patch;
472
+ private loaded;
473
+ private readonly out;
474
+ constructor(settings: SmudgeSettings,
475
+ /** The largest tip diameter of the stroke, in pixels (the patch resolution). */
476
+ diameter: number);
477
+ private cell;
478
+ /** Fills the patch from the pixels under the dab. */
479
+ private load;
480
+ /** `a` mixed `t` of the way to `b` (both 0..1) into `out`. */
481
+ private mix;
482
+ /** One dab: lays the carried patch over the tip's footprint (`cov` over `rect`), then picks up. */
483
+ dab(medium: TileMedium, d: SmudgeDab, rect: RasterRect, cov: ArrayLike<number>): void;
484
+ }
485
+
486
+ export { tipShape as $, blendRadius as A, type BrushSettings as B, CoverageMap as C, type Dab as D, EMPTY_MIXER_LOAD as E, blenderDab as F, cleanMixer as G, coverageTileCoord as H, createBlenderScratch as I, dabCoverage as J, dabRect as K, exchangeRates as L, type MixerSettings as M, loadMixer as N, mixerRates as O, type PaintTileTarget as P, normalizeBlender as Q, type RgbTriple as R, type SmudgeSettings as S, TileMedium as T, normalizeBrush as U, normalizeMixer as V, normalizeSmudge as W, rasterizeDab as X, smoothPoints as Y, tipFalloff as Z, tipReach as _, type MixingModel as a, walkDabs as a0, type BlenderSettings as b, type StrokePoint as c, type MixerLoad as d, MAX_BRUSH_SIZE as e, StrokeCoverage as f, type MixerTarget as g, MixerStroke as h, type StrokeApply as i, type BlenderScratch as j, DEFAULT_BLENDER as k, DEFAULT_BRUSH as l, DEFAULT_MIXER as m, DEFAULT_SMUDGE as n, DabWalker as o, type ExchangeRates as p, MIXER_DRAIN as q, MIXER_PICKUP_RATE as r, MIXING_MODELS as s, PaintExchange as t, type PaintMedium as u, type PaintSample as v, type SmudgeDab as w, SmudgeTip as x, type TileMediumOptions as y, applyStrokeToTile as z };