@pixerate/editor 0.275.1 → 0.277.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 (81) 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 +84 -71
  4. package/dist/brushes/index.js +20 -7
  5. package/dist/brushes/patch.d.cts +4 -4
  6. package/dist/brushes/patch.d.ts +4 -4
  7. package/dist/brushes/profiles.d.cts +4 -0
  8. package/dist/brushes/profiles.d.ts +4 -0
  9. package/dist/brushes/schema.d.cts +2 -0
  10. package/dist/brushes/schema.d.ts +2 -0
  11. package/dist/brushes/types.d.cts +32 -1
  12. package/dist/brushes/types.d.ts +32 -1
  13. package/dist/{brushstrokeRender-2RQL46WB.cjs → brushstrokeRender-7HOMKZNO.cjs} +21 -21
  14. package/dist/{brushstrokeRender-WGY4DZHF.js → brushstrokeRender-EIK3625T.js} +5 -5
  15. package/dist/canvas/index.cjs +13 -13
  16. package/dist/canvas/index.js +1 -1
  17. package/dist/{chunk-T6KNJVBP.cjs → chunk-4NZTK7LM.cjs} +35 -35
  18. package/dist/{chunk-LMG57L5A.js → chunk-CTHWXW5M.js} +121 -8
  19. package/dist/{chunk-43XJC3PP.js → chunk-DWPFGWW5.js} +1 -1
  20. package/dist/{chunk-MSWLJI4G.js → chunk-FNIWHSLI.js} +2 -2
  21. package/dist/{chunk-JDXWLCXF.cjs → chunk-H3EME3S5.cjs} +2 -2
  22. package/dist/{chunk-Y2ISSVPH.js → chunk-HHX7ST6Z.js} +167 -9
  23. package/dist/{chunk-6QNDGMK7.cjs → chunk-HRUK5TFW.cjs} +187 -45
  24. package/dist/{chunk-NIBFKXMD.cjs → chunk-IFWLZZCW.cjs} +619 -2
  25. package/dist/{chunk-W6JDUXP3.js → chunk-J23JOHPR.js} +1 -1
  26. package/dist/{chunk-HVO6XCI4.js → chunk-JPB65KQ6.js} +1 -1
  27. package/dist/{chunk-Y354ETAE.cjs → chunk-JVFGK3DK.cjs} +69 -58
  28. package/dist/{chunk-ACTHSQOI.js → chunk-LDZOXESA.js} +618 -1
  29. package/dist/{chunk-IEEY5RJV.js → chunk-LZE2QZCT.js} +1 -1
  30. package/dist/{chunk-DHC4E2DU.cjs → chunk-MITQGHN3.cjs} +121 -8
  31. package/dist/{chunk-75SYKUOT.js → chunk-N4MCWQZ7.js} +1 -1
  32. package/dist/{chunk-G5TCMKX5.js → chunk-NJLPW4MM.js} +17 -6
  33. package/dist/{chunk-STLTOISW.cjs → chunk-NPPMKMCU.cjs} +17 -17
  34. package/dist/{chunk-OLZXXAER.cjs → chunk-PEQWARCP.cjs} +408 -250
  35. package/dist/{chunk-ZM7IKQ4X.js → chunk-PPDUCZZV.js} +152 -10
  36. package/dist/{chunk-RAPNRK4R.cjs → chunk-QBZEC2JJ.cjs} +88 -88
  37. package/dist/{chunk-RJGW3RQS.js → chunk-ROURHJYT.js} +1 -1
  38. package/dist/{chunk-62MFUHSI.cjs → chunk-UQXULAMQ.cjs} +16 -16
  39. package/dist/{chunk-YYZC2EQO.cjs → chunk-VNF7A2JA.cjs} +23 -23
  40. package/dist/{chunk-C6VB7CPY.cjs → chunk-WFSYZO5Z.cjs} +1 -1
  41. package/dist/image-editor/index.cjs +880 -852
  42. package/dist/image-editor/index.js +36 -8
  43. package/dist/paint/index.cjs +33 -5
  44. package/dist/paint/index.d.cts +1 -0
  45. package/dist/paint/index.d.ts +1 -0
  46. package/dist/paint/index.js +32 -4
  47. package/dist/paint/live-patterns.d.cts +149 -0
  48. package/dist/paint/live-patterns.d.ts +149 -0
  49. package/dist/paint/wet/world.d.cts +13 -0
  50. package/dist/paint/wet/world.d.ts +13 -0
  51. package/dist/paint-editor/brush-model.d.cts +21 -1
  52. package/dist/paint-editor/brush-model.d.ts +21 -1
  53. package/dist/paint-editor/controller.d.cts +102 -1
  54. package/dist/paint-editor/controller.d.ts +102 -1
  55. package/dist/paint-editor/document.d.cts +8 -1
  56. package/dist/paint-editor/document.d.ts +8 -1
  57. package/dist/paint-editor/ferro-tools.d.cts +10 -0
  58. package/dist/paint-editor/ferro-tools.d.ts +10 -0
  59. package/dist/paint-editor/ferro.d.cts +134 -0
  60. package/dist/paint-editor/ferro.d.ts +134 -0
  61. package/dist/paint-editor/index.cjs +2291 -904
  62. package/dist/paint-editor/index.d.cts +2 -0
  63. package/dist/paint-editor/index.d.ts +2 -0
  64. package/dist/paint-editor/index.js +1533 -146
  65. package/dist/paint-editor/stroke.d.cts +14 -0
  66. package/dist/paint-editor/stroke.d.ts +14 -0
  67. package/dist/paint-editor/thick.d.cts +15 -2
  68. package/dist/paint-editor/thick.d.ts +15 -2
  69. package/dist/paint-editor/ui.d.cts +7 -1
  70. package/dist/paint-editor/ui.d.ts +7 -1
  71. package/dist/paint-editor/wet-tools.d.cts +42 -0
  72. package/dist/paint-editor/wet-tools.d.ts +42 -0
  73. package/dist/paint-editor/wet.d.cts +22 -1
  74. package/dist/paint-editor/wet.d.ts +22 -1
  75. package/dist/vector/index.cjs +1002 -1002
  76. package/dist/vector/index.js +7 -7
  77. package/package.json +1 -1
  78. package/skills/pixerate-brushes/SKILL.md +1 -0
  79. package/skills/pixerate-paint-editor/SKILL.md +11 -29
  80. package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
  81. package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Live patterns: Gray-Scott reaction-diffusion fields `(U, V)` that grow inside wet paint and
3
+ * freeze where the paint dries.
4
+ *
5
+ * Each wet layer with live patterns active holds a sparse set of half-resolution (`128 × 128`)
6
+ * pattern tiles paired with the `256 × 256` tiles of `WetWorld`, outside the wet-media LBM kernel.
7
+ * Every wet simulation step runs Gray-Scott substeps gated by the wet layer's water (`P_RHO` +
8
+ * `P_SW > DRY_EPS`): where water remains, `U` and `V` react and diffuse into Turing spots,
9
+ * stripes, coral dendrites, mazes or worms; where the paint has dried, the reaction and diffusion
10
+ * rates drop to zero so the pattern freezes in place.
11
+ */
12
+ import { TiledSurface, type RasterRect } from '../raster/surface.js';
13
+ /** Side length of one half-resolution pattern tile (`WET_TILE / 2`). */
14
+ export declare const PATTERN_TILE = 128;
15
+ /** Cells per pattern tile (`128 × 128`). */
16
+ export declare const PATTERN_CELLS: number;
17
+ /** Gray-Scott substeps run for each wet simulation step. */
18
+ export declare const PATTERN_SUBSTEPS = 4;
19
+ /** Threshold of `V` above which a cell counts as pattern-bearing in stats and relief. */
20
+ export declare const PATTERN_V_EPS = 0.02;
21
+ /** Named Gray-Scott Pearson pattern presets. */
22
+ export declare const LIVE_PATTERN_KINDS: readonly ["spots", "stripes", "coral", "maze", "worms"];
23
+ export type LivePatternKind = (typeof LIVE_PATTERN_KINDS)[number];
24
+ export interface LivePatternPresetInfo {
25
+ readonly kind: LivePatternKind;
26
+ readonly label: string;
27
+ readonly description: string;
28
+ readonly feed: number;
29
+ readonly kill: number;
30
+ }
31
+ /** Built-in Pearson (1993) feed / kill parameter presets. */
32
+ export declare const LIVE_PATTERN_PRESETS: Readonly<Record<LivePatternKind, LivePatternPresetInfo>>;
33
+ /** Resolved live-pattern parameters for a brush or wet layer. */
34
+ export interface LivePatternsSettings {
35
+ /** Named Gray-Scott preset (`spots`, `stripes`, `coral`, `maze`, `worms`). */
36
+ pattern: LivePatternKind;
37
+ /** Gray-Scott feed rate F (0.01..0.1). */
38
+ feed: number;
39
+ /** Gray-Scott kill rate k (0.03..0.08). */
40
+ kill: number;
41
+ /** Pattern feature scale (0.5..4, default 1). Scales the diffusion rates. */
42
+ scale: number;
43
+ /** Visual contrast of the V concentration field (0..2, default 1). */
44
+ contrast: number;
45
+ /** Optional hex colour (`#rrggbb`) that V blends the wet paint towards; `null` modulates pigment contrast. */
46
+ secondColor: string | null;
47
+ /** Impasto relief written to the layer's thick height map as patterns grow (0..1, default 0). */
48
+ relief: number;
49
+ }
50
+ export type LivePatternsInput = Partial<LivePatternsSettings>;
51
+ export declare const DEFAULT_LIVE_PATTERNS: Readonly<LivePatternsSettings>;
52
+ export declare function isLivePatternKind(v: unknown): v is LivePatternKind;
53
+ /** Normalizes a hex colour string (`#rgb` or `#rrggbb`) to lowercase `#rrggbb`, or `null`. */
54
+ export declare function normalizePatternHexColor(v: unknown, fallback?: string | null): string | null;
55
+ /** Parses a `#rrggbb` colour to `[r, g, b]` bytes, or `null`. */
56
+ export declare function parsePatternHexColor(hex: string | null): [number, number, number] | null;
57
+ /** Clamps and fills a `LivePatternsSettings` object from partial input. */
58
+ export declare function normalizeLivePatterns(raw?: LivePatternsInput | null | undefined, base?: LivePatternsSettings): LivePatternsSettings;
59
+ /** Deterministic 32-bit integer hash in `[0, 1)` shared between TypeScript and Swift. */
60
+ export declare function patternNoise(gx: number, gy: number, seed: number): number;
61
+ /** Summary statistics for one wet layer's Gray-Scott field. */
62
+ export interface LivePatternStats {
63
+ /** Whether live patterns are enabled on this layer. */
64
+ enabled: boolean;
65
+ /** Current Gray-Scott reaction-diffusion settings for this layer. */
66
+ settings: LivePatternsSettings;
67
+ /** Half-resolution (`128 × 128`) tiles currently holding pattern state. */
68
+ activeTiles: number;
69
+ /** Pattern cells (`2 × 2` px each) where `V >= PATTERN_V_EPS`. */
70
+ patternCells: number;
71
+ /** Pattern cells where `V >= PATTERN_V_EPS` that are still wet (actively growing). */
72
+ wetPatternCells: number;
73
+ /** Mean `V` concentration across active pattern cells (0..1). */
74
+ meanV: number;
75
+ /** Peak `V` concentration across the layer (0..1). */
76
+ maxV: number;
77
+ /** Document pixel bounding box of active pattern cells, or `null` when none. */
78
+ bounds: RasterRect | null;
79
+ }
80
+ export interface LivePatternTileState {
81
+ readonly u: Float64Array;
82
+ readonly v: Float64Array;
83
+ readonly bakedV: Float64Array;
84
+ }
85
+ export interface LivePatternFieldSnapshot {
86
+ readonly settings: LivePatternsSettings;
87
+ readonly enabled: boolean;
88
+ readonly tiles: ReadonlyMap<number, LivePatternTileState>;
89
+ readonly bytes: number;
90
+ }
91
+ /** Callback returning whether a half-res pattern cell `(tx, ty, lx, ly)` still holds water. */
92
+ export type WetCellPredicate = (tx: number, ty: number, lx: number, ly: number) => boolean;
93
+ /**
94
+ * Per-wet-layer Gray-Scott reaction-diffusion field `(U, V)` stored in `128 × 128` tiles.
95
+ */
96
+ export declare class LivePatternField {
97
+ width: number;
98
+ height: number;
99
+ tilesX: number;
100
+ tilesY: number;
101
+ settings: LivePatternsSettings;
102
+ enabled: boolean;
103
+ readonly tiles: Map<number, {
104
+ u: Float64Array;
105
+ v: Float64Array;
106
+ bakedV: Float64Array;
107
+ }>;
108
+ constructor(width: number, height: number, settings?: LivePatternsInput);
109
+ resize(width: number, height: number): void;
110
+ configure(settings: LivePatternsInput): LivePatternsSettings;
111
+ clear(): void;
112
+ private ensureTile;
113
+ /**
114
+ * Seeds Gray-Scott reagent `V` inside a circle at document coordinates `(cx, cy)` with document
115
+ * `radius` (pixels). Returns the number of half-resolution cells seeded.
116
+ */
117
+ seedDisc(cx: number, cy: number, radius: number, seed?: number, strength?: number): number;
118
+ /**
119
+ * Advances the Gray-Scott reaction-diffusion field for `wetSteps` wet simulation steps.
120
+ * Only cells where `isWetCell(tx, ty, lx, ly)` is true update; dry cells have zero rate and freeze.
121
+ * Returns whether any cell updated.
122
+ */
123
+ step(wetSteps: number, isWetCell: WetCellPredicate, canWakeTile?: (tx: number, ty: number) => boolean): boolean;
124
+ /**
125
+ * Bilinearly samples unbaked `V` (`max(0, v - bakedV)`) at document-tile pixel `(x, y)` in `0..255`.
126
+ */
127
+ private sampleUnbakedV;
128
+ /**
129
+ * Blends the pattern's `V` concentration into a shaded `256 × 256` RGBA wet tile `out`.
130
+ * Returns whether any pixel in `out` was modified.
131
+ */
132
+ shadeTile(tx: number, ty: number, out: Uint8ClampedArray, w: number, h: number): boolean;
133
+ /** Marks a tile's current `V` values as baked into the layer's base surface so they are not baked twice. */
134
+ markBaked(tx: number, ty: number): void;
135
+ /**
136
+ * When `settings.relief > 0`, writes pattern `V` ridges into a 1-channel thick height surface.
137
+ * Returns the updated height `TiledSurface`, or `currentHeight` when `relief <= 0` or nothing changed.
138
+ */
139
+ applyReliefHeight(currentHeight: TiledSurface): TiledSurface;
140
+ /** Computes summary statistics of the layer's live pattern field. */
141
+ stats(isWetCell?: WetCellPredicate): LivePatternStats;
142
+ snapshot(): LivePatternFieldSnapshot;
143
+ restore(snap: LivePatternFieldSnapshot): void;
144
+ }
145
+ /** Shifts a `LivePatternFieldSnapshot` by `(tx0, ty0)` whole wet tiles for a resized canvas `to`. */
146
+ export declare function moveLivePatternSnapshot(snap: LivePatternFieldSnapshot, to: {
147
+ width: number;
148
+ height: number;
149
+ }, tx0: number, ty0: number): LivePatternFieldSnapshot;
@@ -29,6 +29,7 @@
29
29
  * only feeds the display.
30
30
  */
31
31
  import { TiledSurface, type RasterRect } from '../../raster/surface.cjs';
32
+ import { LivePatternField, type LivePatternFieldSnapshot, type LivePatternStats, type LivePatternsInput, type LivePatternsSettings } from '../live-patterns.cjs';
32
33
  import { type WetPaper, type WetParams, type WetTilt } from './model.cjs';
33
34
  import { type WetWorkerLayout } from './wasm.cjs';
34
35
  import type { WetGpuEngine } from './gpu.cjs';
@@ -81,6 +82,8 @@ export interface WetLaneOptions {
81
82
  flowStrength?: number;
82
83
  /** The layer's light paint under the wet paint (one channel; default none). */
83
84
  light?: TiledSurface | null;
85
+ /** Shared per-layer reaction-diffusion field outside the wet kernel. */
86
+ livePatterns?: LivePatternField;
84
87
  /** Active ghost paint state on the layer, if any. */
85
88
  ghost?: WetGhostState | null;
86
89
  }
@@ -111,12 +114,15 @@ export interface WetSnapshot {
111
114
  light?: TiledSurface;
112
115
  lit?: boolean;
113
116
  ghost?: WetGhostState | null;
117
+ livePatterns?: LivePatternFieldSnapshot;
114
118
  }>;
115
119
  /** Per active key: the current set and the deposit planes; and its last flags. */
116
120
  readonly slots: ReadonlyMap<number, {
117
121
  planes: Float32Array;
118
122
  last: number;
119
123
  }>;
124
+ /** Per-layer live-pattern snapshots (including layers whose water has already dried). */
125
+ readonly patterns?: ReadonlyMap<string, LivePatternFieldSnapshot>;
120
126
  /** Bytes held. */
121
127
  readonly bytes: number;
122
128
  }
@@ -190,6 +196,13 @@ export declare class WetWorld {
190
196
  removeLane(id: string): void;
191
197
  getParams(id: string): WetParams;
192
198
  setParams(id: string, params: Partial<WetParams>): WetParams;
199
+ getLivePatterns(id: string): LivePatternsSettings;
200
+ hasLivePatterns(id: string): boolean;
201
+ livePatternField(id: string): LivePatternField;
202
+ setLivePatterns(id: string, settings: LivePatternsInput): LivePatternsSettings;
203
+ seedLivePatterns(id: string, cx: number, cy: number, radius: number, seed?: number, strength?: number, settings?: LivePatternsInput): number;
204
+ livePatternStats(id: string): LivePatternStats;
205
+ private isPatternCellWet;
193
206
  getTilt(): WetTilt;
194
207
  setTilt(tilt: Partial<WetTilt>): WetTilt;
195
208
  /** Changes the paper: heights of live tiles are refilled, rates repacked. */
@@ -29,6 +29,7 @@
29
29
  * only feeds the display.
30
30
  */
31
31
  import { TiledSurface, type RasterRect } from '../../raster/surface.js';
32
+ import { LivePatternField, type LivePatternFieldSnapshot, type LivePatternStats, type LivePatternsInput, type LivePatternsSettings } from '../live-patterns.js';
32
33
  import { type WetPaper, type WetParams, type WetTilt } from './model.js';
33
34
  import { type WetWorkerLayout } from './wasm.js';
34
35
  import type { WetGpuEngine } from './gpu.js';
@@ -81,6 +82,8 @@ export interface WetLaneOptions {
81
82
  flowStrength?: number;
82
83
  /** The layer's light paint under the wet paint (one channel; default none). */
83
84
  light?: TiledSurface | null;
85
+ /** Shared per-layer reaction-diffusion field outside the wet kernel. */
86
+ livePatterns?: LivePatternField;
84
87
  /** Active ghost paint state on the layer, if any. */
85
88
  ghost?: WetGhostState | null;
86
89
  }
@@ -111,12 +114,15 @@ export interface WetSnapshot {
111
114
  light?: TiledSurface;
112
115
  lit?: boolean;
113
116
  ghost?: WetGhostState | null;
117
+ livePatterns?: LivePatternFieldSnapshot;
114
118
  }>;
115
119
  /** Per active key: the current set and the deposit planes; and its last flags. */
116
120
  readonly slots: ReadonlyMap<number, {
117
121
  planes: Float32Array;
118
122
  last: number;
119
123
  }>;
124
+ /** Per-layer live-pattern snapshots (including layers whose water has already dried). */
125
+ readonly patterns?: ReadonlyMap<string, LivePatternFieldSnapshot>;
120
126
  /** Bytes held. */
121
127
  readonly bytes: number;
122
128
  }
@@ -190,6 +196,13 @@ export declare class WetWorld {
190
196
  removeLane(id: string): void;
191
197
  getParams(id: string): WetParams;
192
198
  setParams(id: string, params: Partial<WetParams>): WetParams;
199
+ getLivePatterns(id: string): LivePatternsSettings;
200
+ hasLivePatterns(id: string): boolean;
201
+ livePatternField(id: string): LivePatternField;
202
+ setLivePatterns(id: string, settings: LivePatternsInput): LivePatternsSettings;
203
+ seedLivePatterns(id: string, cx: number, cy: number, radius: number, seed?: number, strength?: number, settings?: LivePatternsInput): number;
204
+ livePatternStats(id: string): LivePatternStats;
205
+ private isPatternCellWet;
193
206
  getTilt(): WetTilt;
194
207
  setTilt(tilt: Partial<WetTilt>): WetTilt;
195
208
  /** Changes the paper: heights of live tiles are refilled, rates repacked. */
@@ -8,7 +8,9 @@ import { type BlenderSettings, type SmudgeSettings } from '../paint/blending.cjs
8
8
  import type { MixingModel } from '../paint/exchange.cjs';
9
9
  import { type PaintGradientSource, type PaintSource } from '../paint/gradient-paint.cjs';
10
10
  import { type MixerSettings } from '../paint/mixer.cjs';
11
+ import { DEFAULT_LIVE_PATTERNS, LIVE_PATTERN_KINDS, LIVE_PATTERN_PRESETS, normalizeLivePatterns, type LivePatternKind, type LivePatternsInput, type LivePatternsSettings } from '../paint/live-patterns.cjs';
11
12
  import { type BristleSettings, type ParticleSettings, type StickerSettings } from './mark-settings.cjs';
13
+ export { DEFAULT_LIVE_PATTERNS, LIVE_PATTERN_KINDS, LIVE_PATTERN_PRESETS, normalizeLivePatterns, type LivePatternKind, type LivePatternsInput, type LivePatternsSettings };
12
14
  /**
13
15
  * How a brush makes marks: `dab` (dabs into a stroke buffer, or dab by dab through the pick-up /
14
16
  * mix exchange when its behaviour asks for it), `eraser` (removes alpha), `mixer` (the Mixer
@@ -147,6 +149,17 @@ export interface MutationSettings {
147
149
  }
148
150
  export declare const DEFAULT_MUTATION: Readonly<MutationSettings>;
149
151
  export declare function normalizeMutation(raw?: Partial<MutationSettings>, base?: MutationSettings): MutationSettings;
152
+ /** Ferrofluid paint: how much liquid ferrofluid a brush lays, and its Rosensweig spike settings (see `doc.ferro`). */
153
+ export interface FerroSettings {
154
+ /** How much liquid ferrofluid each dab lays, scaled by coverage and flow (0..1, default 0: none). */
155
+ amount: number;
156
+ /** Capillary wavelength (spike-to-spike spacing) in pixels (4..64, default 16). */
157
+ wavelength: number;
158
+ /** Critical magnetic field |B| threshold where spikes begin to rise (0.01..0.95, default 0.12). */
159
+ threshold: number;
160
+ }
161
+ export declare const DEFAULT_FERRO: Readonly<FerroSettings>;
162
+ export declare function normalizeFerro(raw?: number | Partial<FerroSettings> | null | undefined, base?: FerroSettings): FerroSettings;
150
163
  export declare function normalizeImpasto(raw?: Partial<ImpastoSettings>, base?: ImpastoSettings): ImpastoSettings;
151
164
  /** Multi-coloured brush: `tip` lays the colours across the tip, `dab` changes colour from dab to dab. */
152
165
  export type MultiColorMode = 'off' | 'tip' | 'dab';
@@ -281,6 +294,8 @@ export interface PaintBrush {
281
294
  ghost?: GhostSettings;
282
295
  /** Mutation paint settings (missing when not a mutation brush). */
283
296
  mutation?: MutationSettings;
297
+ /** Ferrofluid paint: liquid ferrofluid it lays and its Rosensweig spike settings (amount 0: none). */
298
+ ferro: FerroSettings;
284
299
  /**
285
300
  * Dab engine: a stamp tip. Each dab (and each spray droplet) is this sticker's shape, an editor
286
301
  * stamp (`editor:star`) or any sticker ref from the sticker library, laid in the paint colour;
@@ -291,6 +306,8 @@ export interface PaintBrush {
291
306
  spray: SpraySettings | null;
292
307
  /** Dab engines: texture inside the tip (missing: none). Recorded with the stroke, source ref included, so replays match. */
293
308
  tipGrain?: TipGrainSettings;
309
+ /** Wet engine: Gray-Scott reaction-diffusion pattern seeded into the wet paint (missing: none). */
310
+ livePatterns?: LivePatternsSettings;
294
311
  /**
295
312
  * The brush's own gradient paint (`@pixerate/editor/paint` `PaintGradientSource`), used instead
296
313
  * of the editor's paint; `null` (default): the editor's paint. Stops may be `paint` / `secondary`
@@ -317,7 +334,7 @@ export interface SpraySettings {
317
334
  size: number;
318
335
  }
319
336
  export declare const DEFAULT_SPRAY: Readonly<SpraySettings>;
320
- export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer' | 'behavior' | 'blender' | 'smudge' | 'wet' | 'impasto' | 'particle' | 'bristle' | 'sticker' | 'paint' | 'light' | 'fluorescence' | 'thermochromic' | 'iridescence' | 'ghost' | 'mutation' | 'spray' | 'tipGrain'>> & {
337
+ export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer' | 'behavior' | 'blender' | 'smudge' | 'wet' | 'impasto' | 'particle' | 'bristle' | 'sticker' | 'paint' | 'light' | 'fluorescence' | 'thermochromic' | 'iridescence' | 'ghost' | 'mutation' | 'ferro' | 'spray' | 'tipGrain' | 'livePatterns'>> & {
321
338
  spray?: Partial<SpraySettings> | null;
322
339
  /** Tip grain (`null` removes it). */
323
340
  tipGrain?: Partial<TipGrainSettings> | null;
@@ -325,11 +342,14 @@ export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mix
325
342
  ghost?: Partial<GhostSettings> | null;
326
343
  /** Mutation paint (`null` removes it). */
327
344
  mutation?: Partial<MutationSettings> | null;
345
+ /** Live reaction-diffusion patterns (`null` removes it). */
346
+ livePatterns?: LivePatternsInput | null;
328
347
  light?: Partial<LightSettings>;
329
348
  fluorescence?: Partial<FluorescenceSettings> | number;
330
349
  thermochromic?: Partial<ThermochromicSettings>;
331
350
  /** Thin-film iridescence (`null` removes it). */
332
351
  iridescence?: Partial<IridescenceSettings> | null;
352
+ ferro?: number | Partial<FerroSettings>;
333
353
  /** A gradient (or `null` for the editor's paint); a plain colour source clears it. */
334
354
  paint?: PaintSource | null;
335
355
  tip?: Partial<BrushSettings>;
@@ -8,7 +8,9 @@ import { type BlenderSettings, type SmudgeSettings } from '../paint/blending.js'
8
8
  import type { MixingModel } from '../paint/exchange.js';
9
9
  import { type PaintGradientSource, type PaintSource } from '../paint/gradient-paint.js';
10
10
  import { type MixerSettings } from '../paint/mixer.js';
11
+ import { DEFAULT_LIVE_PATTERNS, LIVE_PATTERN_KINDS, LIVE_PATTERN_PRESETS, normalizeLivePatterns, type LivePatternKind, type LivePatternsInput, type LivePatternsSettings } from '../paint/live-patterns.js';
11
12
  import { type BristleSettings, type ParticleSettings, type StickerSettings } from './mark-settings.js';
13
+ export { DEFAULT_LIVE_PATTERNS, LIVE_PATTERN_KINDS, LIVE_PATTERN_PRESETS, normalizeLivePatterns, type LivePatternKind, type LivePatternsInput, type LivePatternsSettings };
12
14
  /**
13
15
  * How a brush makes marks: `dab` (dabs into a stroke buffer, or dab by dab through the pick-up /
14
16
  * mix exchange when its behaviour asks for it), `eraser` (removes alpha), `mixer` (the Mixer
@@ -147,6 +149,17 @@ export interface MutationSettings {
147
149
  }
148
150
  export declare const DEFAULT_MUTATION: Readonly<MutationSettings>;
149
151
  export declare function normalizeMutation(raw?: Partial<MutationSettings>, base?: MutationSettings): MutationSettings;
152
+ /** Ferrofluid paint: how much liquid ferrofluid a brush lays, and its Rosensweig spike settings (see `doc.ferro`). */
153
+ export interface FerroSettings {
154
+ /** How much liquid ferrofluid each dab lays, scaled by coverage and flow (0..1, default 0: none). */
155
+ amount: number;
156
+ /** Capillary wavelength (spike-to-spike spacing) in pixels (4..64, default 16). */
157
+ wavelength: number;
158
+ /** Critical magnetic field |B| threshold where spikes begin to rise (0.01..0.95, default 0.12). */
159
+ threshold: number;
160
+ }
161
+ export declare const DEFAULT_FERRO: Readonly<FerroSettings>;
162
+ export declare function normalizeFerro(raw?: number | Partial<FerroSettings> | null | undefined, base?: FerroSettings): FerroSettings;
150
163
  export declare function normalizeImpasto(raw?: Partial<ImpastoSettings>, base?: ImpastoSettings): ImpastoSettings;
151
164
  /** Multi-coloured brush: `tip` lays the colours across the tip, `dab` changes colour from dab to dab. */
152
165
  export type MultiColorMode = 'off' | 'tip' | 'dab';
@@ -281,6 +294,8 @@ export interface PaintBrush {
281
294
  ghost?: GhostSettings;
282
295
  /** Mutation paint settings (missing when not a mutation brush). */
283
296
  mutation?: MutationSettings;
297
+ /** Ferrofluid paint: liquid ferrofluid it lays and its Rosensweig spike settings (amount 0: none). */
298
+ ferro: FerroSettings;
284
299
  /**
285
300
  * Dab engine: a stamp tip. Each dab (and each spray droplet) is this sticker's shape, an editor
286
301
  * stamp (`editor:star`) or any sticker ref from the sticker library, laid in the paint colour;
@@ -291,6 +306,8 @@ export interface PaintBrush {
291
306
  spray: SpraySettings | null;
292
307
  /** Dab engines: texture inside the tip (missing: none). Recorded with the stroke, source ref included, so replays match. */
293
308
  tipGrain?: TipGrainSettings;
309
+ /** Wet engine: Gray-Scott reaction-diffusion pattern seeded into the wet paint (missing: none). */
310
+ livePatterns?: LivePatternsSettings;
294
311
  /**
295
312
  * The brush's own gradient paint (`@pixerate/editor/paint` `PaintGradientSource`), used instead
296
313
  * of the editor's paint; `null` (default): the editor's paint. Stops may be `paint` / `secondary`
@@ -317,7 +334,7 @@ export interface SpraySettings {
317
334
  size: number;
318
335
  }
319
336
  export declare const DEFAULT_SPRAY: Readonly<SpraySettings>;
320
- export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer' | 'behavior' | 'blender' | 'smudge' | 'wet' | 'impasto' | 'particle' | 'bristle' | 'sticker' | 'paint' | 'light' | 'fluorescence' | 'thermochromic' | 'iridescence' | 'ghost' | 'mutation' | 'spray' | 'tipGrain'>> & {
337
+ export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mixer' | 'behavior' | 'blender' | 'smudge' | 'wet' | 'impasto' | 'particle' | 'bristle' | 'sticker' | 'paint' | 'light' | 'fluorescence' | 'thermochromic' | 'iridescence' | 'ghost' | 'mutation' | 'ferro' | 'spray' | 'tipGrain' | 'livePatterns'>> & {
321
338
  spray?: Partial<SpraySettings> | null;
322
339
  /** Tip grain (`null` removes it). */
323
340
  tipGrain?: Partial<TipGrainSettings> | null;
@@ -325,11 +342,14 @@ export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mix
325
342
  ghost?: Partial<GhostSettings> | null;
326
343
  /** Mutation paint (`null` removes it). */
327
344
  mutation?: Partial<MutationSettings> | null;
345
+ /** Live reaction-diffusion patterns (`null` removes it). */
346
+ livePatterns?: LivePatternsInput | null;
328
347
  light?: Partial<LightSettings>;
329
348
  fluorescence?: Partial<FluorescenceSettings> | number;
330
349
  thermochromic?: Partial<ThermochromicSettings>;
331
350
  /** Thin-film iridescence (`null` removes it). */
332
351
  iridescence?: Partial<IridescenceSettings> | null;
352
+ ferro?: number | Partial<FerroSettings>;
333
353
  /** A gradient (or `null` for the editor's paint); a plain colour source clears it. */
334
354
  paint?: PaintSource | null;
335
355
  tip?: Partial<BrushSettings>;
@@ -50,6 +50,7 @@ import { PaintWetLayers, type PaintWetOptions, type WetEngineInfo, type WetGpuMo
50
50
  import { type WetParams, type WetTilt } from '../paint/wet/model.cjs';
51
51
  import { type WetFlowSpec } from '../paint/wet/flow.cjs';
52
52
  import { type WetBackendInfo } from '../paint/wet/world.cjs';
53
+ import { type LivePatternKind, type LivePatternStats, type LivePatternsInput } from '../paint/live-patterns.cjs';
53
54
  import { type TimeLapse, type TimeLapseOptions } from './replay.cjs';
54
55
  import { EffectsSlot } from '../effects/helpers.cjs';
55
56
  import type { EffectDescription, EffectProvider, EffectValue } from '../effects/types.cjs';
@@ -62,6 +63,7 @@ import type { PaintDirector } from './presence.cjs';
62
63
  import type { PresenceActorInfo } from '../animation/presence.cjs';
63
64
  import { type PaintProposals } from './collaboration.cjs';
64
65
  import { type BreedStrokesResult, type ProposalGenomeResult, type StrokeMutationRates } from './evolution.cjs';
66
+ import { type PaintMagnet } from './ferro.cjs';
65
67
  export declare class PaintEditorError extends Error {
66
68
  constructor(message: string);
67
69
  }
@@ -121,6 +123,13 @@ export interface PaintWetStateInfo {
121
123
  /** Engine B is shading the wet paint now. */
122
124
  gpuActive: boolean;
123
125
  }
126
+ /** One layer's Gray-Scott live-pattern state and wet coverage. */
127
+ export interface PaintLayerLivePatternsInfo extends LivePatternStats {
128
+ layerId: string;
129
+ wet: boolean;
130
+ wetCells: number;
131
+ wetBounds: RasterRect | null;
132
+ }
124
133
  export interface PaintEditorState {
125
134
  document: PaintDocumentState;
126
135
  /** The active brush (possibly edited from its preset). */
@@ -540,6 +549,9 @@ export declare class PaintEditorController {
540
549
  private readonly litCaches;
541
550
  /** The live impasto stroke's channels. */
542
551
  private liveThick;
552
+ /** The live ferrofluid stroke's channel. */
553
+ private liveFerro;
554
+ private nextMagnetId;
543
555
  /** The UV / blacklight setting (saved with the document, not undo steps). */
544
556
  private uvLight;
545
557
  private readonly uvCaches;
@@ -1278,6 +1290,51 @@ export declare class PaintEditorController {
1278
1290
  layerId: string;
1279
1291
  flattened: boolean;
1280
1292
  };
1293
+ /** The document's `ferro` and `ferroSettings` without these layers. */
1294
+ private ferroWithout;
1295
+ /** Runs deterministic advection on all pixel layers with liquid ferrofluid toward `magnets`. */
1296
+ private advectDocumentFerro;
1297
+ /** Lists the magnets on the canvas. */
1298
+ listMagnets(): PaintMagnet[];
1299
+ /** Places a dipole magnet on the canvas and advects liquid ferrofluid toward it (one undo step). */
1300
+ addMagnet(input: {
1301
+ x: number;
1302
+ y: number;
1303
+ strength?: number;
1304
+ radius?: number;
1305
+ }): PaintMagnet;
1306
+ /** Moves or retunes a magnet and advects liquid ferrofluid toward the magnets (one undo step). */
1307
+ updateMagnet(id: string, update: {
1308
+ x?: number;
1309
+ y?: number;
1310
+ strength?: number;
1311
+ radius?: number;
1312
+ }, commitToHistory?: boolean): PaintMagnet;
1313
+ /** Removes magnets by id, or all magnets when `ids` is omitted or empty (one undo step). Returns the removed ids. */
1314
+ removeMagnets(ids?: readonly string[]): string[];
1315
+ /** Replaces the canvas magnets (used by replay). */
1316
+ replaceMagnets(magnets: readonly PaintMagnet[], advect?: boolean): void;
1317
+ /** Reads a layer's liquid ferrofluid coverage and active Rosensweig spike relief under the canvas magnets. */
1318
+ getFerrofluid(layerId?: string): {
1319
+ layerId: string;
1320
+ ferro: boolean;
1321
+ bounds: RasterRect | null;
1322
+ peak: number;
1323
+ coverage: number;
1324
+ activeSpikeArea: number;
1325
+ maxSpikeHeight: number;
1326
+ magnetCount: number;
1327
+ wavelength: number;
1328
+ threshold: number;
1329
+ };
1330
+ /** Cures a layer's liquid ferrofluid: bakes the current Rosensweig spikes and glossy metallic material into `doc.thick` and clears `doc.ferro` (one undo step). */
1331
+ cureFerrofluid(layerId?: string): {
1332
+ layerId: string;
1333
+ cured: boolean;
1334
+ bounds: RasterRect | null;
1335
+ maxSpikeHeight: number;
1336
+ activeSpikeArea: number;
1337
+ };
1281
1338
  /**
1282
1339
  * A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain
1283
1340
  * scaled; every pixel layer empty; the same brushes, lighting and wet options). Dispose of it
@@ -1406,6 +1463,50 @@ export declare class PaintEditorController {
1406
1463
  ty: number;
1407
1464
  data: Uint8ClampedArray;
1408
1465
  }>;
1466
+ private layerLivePatternsInfo;
1467
+ private checkLivePatternsInput;
1468
+ /**
1469
+ * Reads a layer's Gray-Scott reaction-diffusion live patterns (`layerId`, default the active
1470
+ * layer), plus all layers with wet paint or live patterns and the built-in Pearson presets.
1471
+ */
1472
+ getLivePatterns(layerId?: string): PaintLayerLivePatternsInfo & {
1473
+ layers: PaintLayerLivePatternsInfo[];
1474
+ presets: Readonly<Record<LivePatternKind, {
1475
+ feed: number;
1476
+ kill: number;
1477
+ }>>;
1478
+ brushPresets: readonly string[];
1479
+ };
1480
+ /**
1481
+ * Configures a layer's Gray-Scott reaction-diffusion live patterns (or clears them with
1482
+ * `enabled: false`). One undo step.
1483
+ */
1484
+ setLivePatterns(layerId: string | undefined, update: LivePatternsInput & {
1485
+ enabled?: boolean;
1486
+ }): PaintLayerLivePatternsInfo;
1487
+ /**
1488
+ * Seeds Gray-Scott reaction-diffusion activator (`V`) discs into a layer's live-pattern field so
1489
+ * patterns grow where the layer holds wet water and freeze where it dries. One undo step.
1490
+ */
1491
+ seedLivePatterns(options: {
1492
+ layerId?: string;
1493
+ points: ReadonlyArray<{
1494
+ x: number;
1495
+ y: number;
1496
+ strength?: number;
1497
+ }>;
1498
+ radius?: number;
1499
+ seed?: number;
1500
+ pattern?: LivePatternKind;
1501
+ feed?: number;
1502
+ kill?: number;
1503
+ scale?: number;
1504
+ contrast?: number;
1505
+ secondColor?: string | null;
1506
+ relief?: number;
1507
+ }): PaintLayerLivePatternsInfo & {
1508
+ seededCells: number;
1509
+ };
1409
1510
  private sources;
1410
1511
  /** The layers with the live stroke's surface in place. */
1411
1512
  private currentLayers;
@@ -1425,7 +1526,7 @@ export declare class PaintEditorController {
1425
1526
  * Linear Dodge (`::uv`) spill layer directly above it.
1426
1527
  */
1427
1528
  private uvLayers;
1428
- /** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
1529
+ /** The layers with thick paint and ferrofluid spikes lit (each thick or ferrofluid layer's pixels replaced by its lit tiles). */
1429
1530
  private litLayers;
1430
1531
  /**
1431
1532
  * Light paint over the (lit) layers: glow, rim and clarity in each layer's pixels, and its spill