@pixerate/editor 0.276.0 → 0.278.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 (65) hide show
  1. package/dist/animation-editor/index.cjs +59 -59
  2. package/dist/animation-editor/index.js +3 -3
  3. package/dist/brushes/index.cjs +69 -67
  4. package/dist/brushes/index.js +6 -4
  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 +2 -0
  8. package/dist/brushes/profiles.d.ts +2 -0
  9. package/dist/brushes/schema.d.cts +1 -0
  10. package/dist/brushes/schema.d.ts +1 -0
  11. package/dist/brushes/types.d.cts +10 -0
  12. package/dist/brushes/types.d.ts +10 -0
  13. package/dist/{brushstrokeRender-I6MYFCOU.cjs → brushstrokeRender-7HOMKZNO.cjs} +15 -15
  14. package/dist/{brushstrokeRender-FK3ASUUJ.js → brushstrokeRender-EIK3625T.js} +3 -3
  15. package/dist/canvas/index.cjs +13 -13
  16. package/dist/canvas/index.js +1 -1
  17. package/dist/{chunk-U26LUXCJ.cjs → chunk-4NZTK7LM.cjs} +35 -35
  18. package/dist/{chunk-CVBTJVRN.js → chunk-DWPFGWW5.js} +1 -1
  19. package/dist/{chunk-XMSH66VU.js → chunk-FNIWHSLI.js} +1 -1
  20. package/dist/{chunk-QNNZA3V5.cjs → chunk-H3EME3S5.cjs} +2 -2
  21. package/dist/{chunk-4EM5NGCL.js → chunk-HHX7ST6Z.js} +1 -1
  22. package/dist/{chunk-Q33V5PEF.cjs → chunk-HRUK5TFW.cjs} +73 -12
  23. package/dist/{chunk-5HR5CSLC.cjs → chunk-JVFGK3DK.cjs} +69 -58
  24. package/dist/{chunk-JNMNM3SL.js → chunk-LZE2QZCT.js} +1 -1
  25. package/dist/{chunk-OS6OG7DN.js → chunk-N4MCWQZ7.js} +1 -1
  26. package/dist/{chunk-IJ6NSWKM.js → chunk-NJLPW4MM.js} +17 -6
  27. package/dist/{chunk-OY24XRYF.cjs → chunk-PEQWARCP.cjs} +12 -12
  28. package/dist/{chunk-XVPQNX7O.js → chunk-PPDUCZZV.js} +66 -5
  29. package/dist/{chunk-X7XBEX3O.cjs → chunk-QBZEC2JJ.cjs} +86 -86
  30. package/dist/{chunk-GHYJAMMS.js → chunk-ROURHJYT.js} +1 -1
  31. package/dist/{chunk-ZIE6EJGR.cjs → chunk-VNF7A2JA.cjs} +23 -23
  32. package/dist/{chunk-ISCAJBPQ.cjs → chunk-WFSYZO5Z.cjs} +1 -1
  33. package/dist/image-editor/index.cjs +788 -788
  34. package/dist/image-editor/index.js +4 -4
  35. package/dist/paint-editor/brush-model.d.cts +15 -1
  36. package/dist/paint-editor/brush-model.d.ts +15 -1
  37. package/dist/paint-editor/controller.d.cts +90 -9
  38. package/dist/paint-editor/controller.d.ts +90 -9
  39. package/dist/paint-editor/document.d.cts +8 -1
  40. package/dist/paint-editor/document.d.ts +8 -1
  41. package/dist/paint-editor/ferro-tools.d.cts +10 -0
  42. package/dist/paint-editor/ferro-tools.d.ts +10 -0
  43. package/dist/paint-editor/ferro.d.cts +134 -0
  44. package/dist/paint-editor/ferro.d.ts +134 -0
  45. package/dist/paint-editor/index.cjs +2138 -872
  46. package/dist/paint-editor/index.d.cts +2 -0
  47. package/dist/paint-editor/index.d.ts +2 -0
  48. package/dist/paint-editor/index.js +1446 -180
  49. package/dist/paint-editor/serialize.d.cts +3 -1
  50. package/dist/paint-editor/serialize.d.ts +3 -1
  51. package/dist/paint-editor/stroke.d.cts +17 -0
  52. package/dist/paint-editor/stroke.d.ts +17 -0
  53. package/dist/paint-editor/strokes.d.cts +11 -1
  54. package/dist/paint-editor/strokes.d.ts +11 -1
  55. package/dist/paint-editor/thick.d.cts +15 -2
  56. package/dist/paint-editor/thick.d.ts +15 -2
  57. package/dist/paint-editor/ui.d.cts +2 -0
  58. package/dist/paint-editor/ui.d.ts +2 -0
  59. package/dist/vector/index.cjs +998 -998
  60. package/dist/vector/index.js +4 -4
  61. package/package.json +1 -1
  62. package/skills/pixerate-brushes/SKILL.md +1 -0
  63. package/skills/pixerate-paint-editor/SKILL.md +12 -30
  64. package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
  65. package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
@@ -8,7 +8,7 @@ import {
8
8
  parseOpenTypeFont,
9
9
  removeAnchorsKeepingShape,
10
10
  traceField
11
- } from "../chunk-OS6OG7DN.js";
11
+ } from "../chunk-N4MCWQZ7.js";
12
12
  import {
13
13
  ADJUSTMENT_KEYS,
14
14
  ADJUSTMENT_KINDS,
@@ -409,7 +409,7 @@ import {
409
409
  writePsd,
410
410
  xyToTempTint,
411
411
  xyToUv
412
- } from "../chunk-XMSH66VU.js";
412
+ } from "../chunk-FNIWHSLI.js";
413
413
  import {
414
414
  flattenPath,
415
415
  rasterizeCoverage
@@ -683,14 +683,14 @@ import {
683
683
  warpMeshParamAt,
684
684
  warpMeshRows,
685
685
  warpPresetMesh
686
- } from "../chunk-4EM5NGCL.js";
686
+ } from "../chunk-HHX7ST6Z.js";
687
687
  import {
688
688
  IMAGE_PROFILE,
689
689
  checkBrush,
690
690
  closest,
691
691
  createBrushLibrary,
692
692
  resolveBrush
693
- } from "../chunk-XVPQNX7O.js";
693
+ } from "../chunk-PPDUCZZV.js";
694
694
  import "../chunk-JL6YH5WD.js";
695
695
  import "../chunk-RWDSSK2I.js";
696
696
  import {
@@ -149,6 +149,17 @@ export interface MutationSettings {
149
149
  }
150
150
  export declare const DEFAULT_MUTATION: Readonly<MutationSettings>;
151
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;
152
163
  export declare function normalizeImpasto(raw?: Partial<ImpastoSettings>, base?: ImpastoSettings): ImpastoSettings;
153
164
  /** Multi-coloured brush: `tip` lays the colours across the tip, `dab` changes colour from dab to dab. */
154
165
  export type MultiColorMode = 'off' | 'tip' | 'dab';
@@ -283,6 +294,8 @@ export interface PaintBrush {
283
294
  ghost?: GhostSettings;
284
295
  /** Mutation paint settings (missing when not a mutation brush). */
285
296
  mutation?: MutationSettings;
297
+ /** Ferrofluid paint: liquid ferrofluid it lays and its Rosensweig spike settings (amount 0: none). */
298
+ ferro: FerroSettings;
286
299
  /**
287
300
  * Dab engine: a stamp tip. Each dab (and each spray droplet) is this sticker's shape, an editor
288
301
  * stamp (`editor:star`) or any sticker ref from the sticker library, laid in the paint colour;
@@ -321,7 +334,7 @@ export interface SpraySettings {
321
334
  size: number;
322
335
  }
323
336
  export declare const DEFAULT_SPRAY: Readonly<SpraySettings>;
324
- 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' | 'livePatterns'>> & {
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'>> & {
325
338
  spray?: Partial<SpraySettings> | null;
326
339
  /** Tip grain (`null` removes it). */
327
340
  tipGrain?: Partial<TipGrainSettings> | null;
@@ -336,6 +349,7 @@ export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mix
336
349
  thermochromic?: Partial<ThermochromicSettings>;
337
350
  /** Thin-film iridescence (`null` removes it). */
338
351
  iridescence?: Partial<IridescenceSettings> | null;
352
+ ferro?: number | Partial<FerroSettings>;
339
353
  /** A gradient (or `null` for the editor's paint); a plain colour source clears it. */
340
354
  paint?: PaintSource | null;
341
355
  tip?: Partial<BrushSettings>;
@@ -149,6 +149,17 @@ export interface MutationSettings {
149
149
  }
150
150
  export declare const DEFAULT_MUTATION: Readonly<MutationSettings>;
151
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;
152
163
  export declare function normalizeImpasto(raw?: Partial<ImpastoSettings>, base?: ImpastoSettings): ImpastoSettings;
153
164
  /** Multi-coloured brush: `tip` lays the colours across the tip, `dab` changes colour from dab to dab. */
154
165
  export type MultiColorMode = 'off' | 'tip' | 'dab';
@@ -283,6 +294,8 @@ export interface PaintBrush {
283
294
  ghost?: GhostSettings;
284
295
  /** Mutation paint settings (missing when not a mutation brush). */
285
296
  mutation?: MutationSettings;
297
+ /** Ferrofluid paint: liquid ferrofluid it lays and its Rosensweig spike settings (amount 0: none). */
298
+ ferro: FerroSettings;
286
299
  /**
287
300
  * Dab engine: a stamp tip. Each dab (and each spray droplet) is this sticker's shape, an editor
288
301
  * stamp (`editor:star`) or any sticker ref from the sticker library, laid in the paint colour;
@@ -321,7 +334,7 @@ export interface SpraySettings {
321
334
  size: number;
322
335
  }
323
336
  export declare const DEFAULT_SPRAY: Readonly<SpraySettings>;
324
- 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' | 'livePatterns'>> & {
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'>> & {
325
338
  spray?: Partial<SpraySettings> | null;
326
339
  /** Tip grain (`null` removes it). */
327
340
  tipGrain?: Partial<TipGrainSettings> | null;
@@ -336,6 +349,7 @@ export type PaintBrushInput = Partial<Omit<PaintBrush, 'tip' | 'dynamics' | 'mix
336
349
  thermochromic?: Partial<ThermochromicSettings>;
337
350
  /** Thin-film iridescence (`null` removes it). */
338
351
  iridescence?: Partial<IridescenceSettings> | null;
352
+ ferro?: number | Partial<FerroSettings>;
339
353
  /** A gradient (or `null` for the editor's paint); a plain colour source clears it. */
340
354
  paint?: PaintSource | null;
341
355
  tip?: Partial<BrushSettings>;
@@ -23,7 +23,7 @@ import { type PaintBrushInput, type PaintBrush } from './brushes.cjs';
23
23
  import { type BrushLibrary, type BrushLibraryFile } from '../brushes/library.cjs';
24
24
  import { type BrushPatch, type UnsettableBrushFacet } from '../brushes/patch.cjs';
25
25
  import type { Brush } from '../brushes/types.cjs';
26
- import type { ColorScheme } from '../color/dual.cjs';
26
+ import { type ColorScheme, type ColorValue } from '../color/dual.cjs';
27
27
  import { type PaintCanvas, type PaintDocumentState, type PaintLayer } from './document.cjs';
28
28
  import { type ImageEncoder, type ImageFormat } from './encode.cjs';
29
29
  import type { PenPoint } from './engine.cjs';
@@ -63,6 +63,7 @@ import type { PaintDirector } from './presence.cjs';
63
63
  import type { PresenceActorInfo } from '../animation/presence.cjs';
64
64
  import { type PaintProposals } from './collaboration.cjs';
65
65
  import { type BreedStrokesResult, type ProposalGenomeResult, type StrokeMutationRates } from './evolution.cjs';
66
+ import { type PaintMagnet } from './ferro.cjs';
66
67
  export declare class PaintEditorError extends Error {
67
68
  constructor(message: string);
68
69
  }
@@ -205,8 +206,8 @@ export interface LiveStrokeUpdate {
205
206
  brushId?: string;
206
207
  /** Brush settings to change (as `setBrush` takes), on top of `brushId` or the stroke's brush. */
207
208
  brush?: Omit<PaintBrushInput, 'id'>;
208
- /** A new colour #rrggbb (replaces gradient or effect paint). */
209
- color?: string;
209
+ /** A new colour #rrggbb, or a light / dark pair (replaces gradient or effect paint). */
210
+ color?: ColorValue;
210
211
  /** Paint: a colour, gradient paint or effect paint; `null` back to the plain colour. */
211
212
  paint?: unknown;
212
213
  /** Another jitter seed (particles, scatter and texture fall differently). */
@@ -274,8 +275,12 @@ export interface PaintPreviewPlacement {
274
275
  export interface PaintStrokeInit {
275
276
  /** Default the active layer. */
276
277
  layerId?: string;
277
- /** Default the editor colour. */
278
- color?: string;
278
+ /**
279
+ * Default the editor colour. A light / dark pair (`DualColor`) is recorded with the stroke
280
+ * (`RecordedStroke.dualColor`): on a stroke layer the document draws its light side and the view
281
+ * the viewer's (`setColorScheme`); on a pixel layer it paints the scheme's side.
282
+ */
283
+ color?: ColorValue;
279
284
  /**
280
285
  * What this stroke paints with, instead of the editor's paint: a colour, gradient paint
281
286
  * (`@pixerate/editor/paint` `PaintSource`), or effect paint (render it first with
@@ -548,6 +553,9 @@ export declare class PaintEditorController {
548
553
  private readonly litCaches;
549
554
  /** The live impasto stroke's channels. */
550
555
  private liveThick;
556
+ /** The live ferrofluid stroke's channel. */
557
+ private liveFerro;
558
+ private nextMagnetId;
551
559
  /** The UV / blacklight setting (saved with the document, not undo steps). */
552
560
  private uvLight;
553
561
  private readonly uvCaches;
@@ -1058,9 +1066,29 @@ export declare class PaintEditorController {
1058
1066
  private brushColour;
1059
1067
  /**
1060
1068
  * The host's colour scheme: a paint colour that came from a brush's light / dark default
1061
- * resolves to the new side. Editor state, not an undo step.
1069
+ * resolves to the new side, and stroke layers show their dual-colour strokes in it. Only the
1070
+ * view changes: the document, history and change listeners don't hear of it.
1062
1071
  */
1063
1072
  setColorScheme(scheme: ColorScheme): void;
1073
+ /**
1074
+ * Stroke layers in the colour scheme: the document keeps every stroke layer drawn with each
1075
+ * stroke's `color` (a dual-colour stroke's light side), so saved and shared documents never
1076
+ * depend on who is looking. The view (display, image exports, the base a pointer stroke paints
1077
+ * on) draws dual-colour strokes in the scheme's side instead. One cached surface per layer,
1078
+ * brought up to date from the strokes that changed since it was drawn.
1079
+ */
1080
+ private schemeViews;
1081
+ private schemeCopies;
1082
+ /** Set while a stroke is painted for the document rather than the view (`runStroke` on a stroke layer): dual colours take this side. */
1083
+ private paintScheme;
1084
+ /** A stroke as the scheme shows it: itself, or (a dual-colour stroke) a copy in the scheme's side. */
1085
+ private schemeStroke;
1086
+ /** A stroke layer's surface in the scheme, or `null` when the document's pixels already show it. */
1087
+ private schemeSurface;
1088
+ /** `layers` with each stroke layer that holds dual-colour strokes shown in the scheme. */
1089
+ private schemeLayers;
1090
+ /** The paint colour's light / dark pair for a stroke with `brushId`, when the colour came from one (a brush's own or default colour, or `setColor`). */
1091
+ private dualPaint;
1064
1092
  /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
1065
1093
  setColorLocked(locked: boolean): void;
1066
1094
  /**
@@ -1071,8 +1099,12 @@ export declare class PaintEditorController {
1071
1099
  private takeBrushColour;
1072
1100
  /** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
1073
1101
  private ownColour;
1074
- /** Sets the paint colour (and goes back to one colour from gradient paint). */
1075
- setColor(color: string): string;
1102
+ /**
1103
+ * Sets the paint colour (and goes back to one colour from gradient paint). A light / dark pair
1104
+ * paints the colour scheme's side, follows `setColorScheme`, and is recorded with each stroke
1105
+ * painted in it. Returns the colour as #rrggbb.
1106
+ */
1107
+ setColor(color: ColorValue): string;
1076
1108
  /**
1077
1109
  * Sets what strokes paint with: one colour (`{ type: 'color', color }` or `#rrggbb`), or gradient
1078
1110
  * paint (`{ type: 'gradient', stops, space?, map?, softness? }`, see `@pixerate/editor/paint`),
@@ -1286,6 +1318,51 @@ export declare class PaintEditorController {
1286
1318
  layerId: string;
1287
1319
  flattened: boolean;
1288
1320
  };
1321
+ /** The document's `ferro` and `ferroSettings` without these layers. */
1322
+ private ferroWithout;
1323
+ /** Runs deterministic advection on all pixel layers with liquid ferrofluid toward `magnets`. */
1324
+ private advectDocumentFerro;
1325
+ /** Lists the magnets on the canvas. */
1326
+ listMagnets(): PaintMagnet[];
1327
+ /** Places a dipole magnet on the canvas and advects liquid ferrofluid toward it (one undo step). */
1328
+ addMagnet(input: {
1329
+ x: number;
1330
+ y: number;
1331
+ strength?: number;
1332
+ radius?: number;
1333
+ }): PaintMagnet;
1334
+ /** Moves or retunes a magnet and advects liquid ferrofluid toward the magnets (one undo step). */
1335
+ updateMagnet(id: string, update: {
1336
+ x?: number;
1337
+ y?: number;
1338
+ strength?: number;
1339
+ radius?: number;
1340
+ }, commitToHistory?: boolean): PaintMagnet;
1341
+ /** Removes magnets by id, or all magnets when `ids` is omitted or empty (one undo step). Returns the removed ids. */
1342
+ removeMagnets(ids?: readonly string[]): string[];
1343
+ /** Replaces the canvas magnets (used by replay). */
1344
+ replaceMagnets(magnets: readonly PaintMagnet[], advect?: boolean): void;
1345
+ /** Reads a layer's liquid ferrofluid coverage and active Rosensweig spike relief under the canvas magnets. */
1346
+ getFerrofluid(layerId?: string): {
1347
+ layerId: string;
1348
+ ferro: boolean;
1349
+ bounds: RasterRect | null;
1350
+ peak: number;
1351
+ coverage: number;
1352
+ activeSpikeArea: number;
1353
+ maxSpikeHeight: number;
1354
+ magnetCount: number;
1355
+ wavelength: number;
1356
+ threshold: number;
1357
+ };
1358
+ /** 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). */
1359
+ cureFerrofluid(layerId?: string): {
1360
+ layerId: string;
1361
+ cured: boolean;
1362
+ bounds: RasterRect | null;
1363
+ maxSpikeHeight: number;
1364
+ activeSpikeArea: number;
1365
+ };
1289
1366
  /**
1290
1367
  * A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain
1291
1368
  * scaled; every pixel layer empty; the same brushes, lighting and wet options). Dispose of it
@@ -1477,7 +1554,7 @@ export declare class PaintEditorController {
1477
1554
  * Linear Dodge (`::uv`) spill layer directly above it.
1478
1555
  */
1479
1556
  private uvLayers;
1480
- /** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
1557
+ /** The layers with thick paint and ferrofluid spikes lit (each thick or ferrofluid layer's pixels replaced by its lit tiles). */
1481
1558
  private litLayers;
1482
1559
  /**
1483
1560
  * Light paint over the (lit) layers: glow, rim and clarity in each layer's pixels, and its spill
@@ -1726,6 +1803,10 @@ export declare class PaintEditorController {
1726
1803
  liveStrokeArea(id: string): RasterRect | null;
1727
1804
  /** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
1728
1805
  private commitLiveStrokes;
1806
+ /**
1807
+ * The document with a layer's strokes set and its pixels redrawn. With `record` false (the same
1808
+ * strokes drawn for the colour scheme) the pixel lock doesn't apply and no event is recorded for replays.
1809
+ */
1729
1810
  private withLiveStrokes;
1730
1811
  /** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
1731
1812
  private strokeArea;
@@ -23,7 +23,7 @@ import { type PaintBrushInput, type PaintBrush } from './brushes.js';
23
23
  import { type BrushLibrary, type BrushLibraryFile } from '../brushes/library.js';
24
24
  import { type BrushPatch, type UnsettableBrushFacet } from '../brushes/patch.js';
25
25
  import type { Brush } from '../brushes/types.js';
26
- import type { ColorScheme } from '../color/dual.js';
26
+ import { type ColorScheme, type ColorValue } from '../color/dual.js';
27
27
  import { type PaintCanvas, type PaintDocumentState, type PaintLayer } from './document.js';
28
28
  import { type ImageEncoder, type ImageFormat } from './encode.js';
29
29
  import type { PenPoint } from './engine.js';
@@ -63,6 +63,7 @@ import type { PaintDirector } from './presence.js';
63
63
  import type { PresenceActorInfo } from '../animation/presence.js';
64
64
  import { type PaintProposals } from './collaboration.js';
65
65
  import { type BreedStrokesResult, type ProposalGenomeResult, type StrokeMutationRates } from './evolution.js';
66
+ import { type PaintMagnet } from './ferro.js';
66
67
  export declare class PaintEditorError extends Error {
67
68
  constructor(message: string);
68
69
  }
@@ -205,8 +206,8 @@ export interface LiveStrokeUpdate {
205
206
  brushId?: string;
206
207
  /** Brush settings to change (as `setBrush` takes), on top of `brushId` or the stroke's brush. */
207
208
  brush?: Omit<PaintBrushInput, 'id'>;
208
- /** A new colour #rrggbb (replaces gradient or effect paint). */
209
- color?: string;
209
+ /** A new colour #rrggbb, or a light / dark pair (replaces gradient or effect paint). */
210
+ color?: ColorValue;
210
211
  /** Paint: a colour, gradient paint or effect paint; `null` back to the plain colour. */
211
212
  paint?: unknown;
212
213
  /** Another jitter seed (particles, scatter and texture fall differently). */
@@ -274,8 +275,12 @@ export interface PaintPreviewPlacement {
274
275
  export interface PaintStrokeInit {
275
276
  /** Default the active layer. */
276
277
  layerId?: string;
277
- /** Default the editor colour. */
278
- color?: string;
278
+ /**
279
+ * Default the editor colour. A light / dark pair (`DualColor`) is recorded with the stroke
280
+ * (`RecordedStroke.dualColor`): on a stroke layer the document draws its light side and the view
281
+ * the viewer's (`setColorScheme`); on a pixel layer it paints the scheme's side.
282
+ */
283
+ color?: ColorValue;
279
284
  /**
280
285
  * What this stroke paints with, instead of the editor's paint: a colour, gradient paint
281
286
  * (`@pixerate/editor/paint` `PaintSource`), or effect paint (render it first with
@@ -548,6 +553,9 @@ export declare class PaintEditorController {
548
553
  private readonly litCaches;
549
554
  /** The live impasto stroke's channels. */
550
555
  private liveThick;
556
+ /** The live ferrofluid stroke's channel. */
557
+ private liveFerro;
558
+ private nextMagnetId;
551
559
  /** The UV / blacklight setting (saved with the document, not undo steps). */
552
560
  private uvLight;
553
561
  private readonly uvCaches;
@@ -1058,9 +1066,29 @@ export declare class PaintEditorController {
1058
1066
  private brushColour;
1059
1067
  /**
1060
1068
  * The host's colour scheme: a paint colour that came from a brush's light / dark default
1061
- * resolves to the new side. Editor state, not an undo step.
1069
+ * resolves to the new side, and stroke layers show their dual-colour strokes in it. Only the
1070
+ * view changes: the document, history and change listeners don't hear of it.
1062
1071
  */
1063
1072
  setColorScheme(scheme: ColorScheme): void;
1073
+ /**
1074
+ * Stroke layers in the colour scheme: the document keeps every stroke layer drawn with each
1075
+ * stroke's `color` (a dual-colour stroke's light side), so saved and shared documents never
1076
+ * depend on who is looking. The view (display, image exports, the base a pointer stroke paints
1077
+ * on) draws dual-colour strokes in the scheme's side instead. One cached surface per layer,
1078
+ * brought up to date from the strokes that changed since it was drawn.
1079
+ */
1080
+ private schemeViews;
1081
+ private schemeCopies;
1082
+ /** Set while a stroke is painted for the document rather than the view (`runStroke` on a stroke layer): dual colours take this side. */
1083
+ private paintScheme;
1084
+ /** A stroke as the scheme shows it: itself, or (a dual-colour stroke) a copy in the scheme's side. */
1085
+ private schemeStroke;
1086
+ /** A stroke layer's surface in the scheme, or `null` when the document's pixels already show it. */
1087
+ private schemeSurface;
1088
+ /** `layers` with each stroke layer that holds dual-colour strokes shown in the scheme. */
1089
+ private schemeLayers;
1090
+ /** The paint colour's light / dark pair for a stroke with `brushId`, when the colour came from one (a brush's own or default colour, or `setColor`). */
1091
+ private dualPaint;
1064
1092
  /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
1065
1093
  setColorLocked(locked: boolean): void;
1066
1094
  /**
@@ -1071,8 +1099,12 @@ export declare class PaintEditorController {
1071
1099
  private takeBrushColour;
1072
1100
  /** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
1073
1101
  private ownColour;
1074
- /** Sets the paint colour (and goes back to one colour from gradient paint). */
1075
- setColor(color: string): string;
1102
+ /**
1103
+ * Sets the paint colour (and goes back to one colour from gradient paint). A light / dark pair
1104
+ * paints the colour scheme's side, follows `setColorScheme`, and is recorded with each stroke
1105
+ * painted in it. Returns the colour as #rrggbb.
1106
+ */
1107
+ setColor(color: ColorValue): string;
1076
1108
  /**
1077
1109
  * Sets what strokes paint with: one colour (`{ type: 'color', color }` or `#rrggbb`), or gradient
1078
1110
  * paint (`{ type: 'gradient', stops, space?, map?, softness? }`, see `@pixerate/editor/paint`),
@@ -1286,6 +1318,51 @@ export declare class PaintEditorController {
1286
1318
  layerId: string;
1287
1319
  flattened: boolean;
1288
1320
  };
1321
+ /** The document's `ferro` and `ferroSettings` without these layers. */
1322
+ private ferroWithout;
1323
+ /** Runs deterministic advection on all pixel layers with liquid ferrofluid toward `magnets`. */
1324
+ private advectDocumentFerro;
1325
+ /** Lists the magnets on the canvas. */
1326
+ listMagnets(): PaintMagnet[];
1327
+ /** Places a dipole magnet on the canvas and advects liquid ferrofluid toward it (one undo step). */
1328
+ addMagnet(input: {
1329
+ x: number;
1330
+ y: number;
1331
+ strength?: number;
1332
+ radius?: number;
1333
+ }): PaintMagnet;
1334
+ /** Moves or retunes a magnet and advects liquid ferrofluid toward the magnets (one undo step). */
1335
+ updateMagnet(id: string, update: {
1336
+ x?: number;
1337
+ y?: number;
1338
+ strength?: number;
1339
+ radius?: number;
1340
+ }, commitToHistory?: boolean): PaintMagnet;
1341
+ /** Removes magnets by id, or all magnets when `ids` is omitted or empty (one undo step). Returns the removed ids. */
1342
+ removeMagnets(ids?: readonly string[]): string[];
1343
+ /** Replaces the canvas magnets (used by replay). */
1344
+ replaceMagnets(magnets: readonly PaintMagnet[], advect?: boolean): void;
1345
+ /** Reads a layer's liquid ferrofluid coverage and active Rosensweig spike relief under the canvas magnets. */
1346
+ getFerrofluid(layerId?: string): {
1347
+ layerId: string;
1348
+ ferro: boolean;
1349
+ bounds: RasterRect | null;
1350
+ peak: number;
1351
+ coverage: number;
1352
+ activeSpikeArea: number;
1353
+ maxSpikeHeight: number;
1354
+ magnetCount: number;
1355
+ wavelength: number;
1356
+ threshold: number;
1357
+ };
1358
+ /** 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). */
1359
+ cureFerrofluid(layerId?: string): {
1360
+ layerId: string;
1361
+ cured: boolean;
1362
+ bounds: RasterRect | null;
1363
+ maxSpikeHeight: number;
1364
+ activeSpikeArea: number;
1365
+ };
1289
1366
  /**
1290
1367
  * A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain
1291
1368
  * scaled; every pixel layer empty; the same brushes, lighting and wet options). Dispose of it
@@ -1477,7 +1554,7 @@ export declare class PaintEditorController {
1477
1554
  * Linear Dodge (`::uv`) spill layer directly above it.
1478
1555
  */
1479
1556
  private uvLayers;
1480
- /** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
1557
+ /** The layers with thick paint and ferrofluid spikes lit (each thick or ferrofluid layer's pixels replaced by its lit tiles). */
1481
1558
  private litLayers;
1482
1559
  /**
1483
1560
  * Light paint over the (lit) layers: glow, rim and clarity in each layer's pixels, and its spill
@@ -1726,6 +1803,10 @@ export declare class PaintEditorController {
1726
1803
  liveStrokeArea(id: string): RasterRect | null;
1727
1804
  /** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
1728
1805
  private commitLiveStrokes;
1806
+ /**
1807
+ * The document with a layer's strokes set and its pixels redrawn. With `record` false (the same
1808
+ * strokes drawn for the colour scheme) the pixel lock doesn't apply and no event is recorded for replays.
1809
+ */
1729
1810
  private withLiveStrokes;
1730
1811
  /** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
1731
1812
  private strokeArea;
@@ -13,6 +13,7 @@ import { type PaperSettings } from './paper.cjs';
13
13
  import type { RecordedStroke } from './strokes.cjs';
14
14
  import type { WetEvent } from './wet.cjs';
15
15
  import type { ThickChannels } from './thick.cjs';
16
+ import type { FerroSettings, PaintMagnet } from './ferro.cjs';
16
17
  import type { Sticker } from './stickers.cjs';
17
18
  import type { PaintEffectSource, PaintPaperEffect } from './effects.cjs';
18
19
  import type { HeatField } from './heat.cjs';
@@ -61,6 +62,12 @@ export interface PaintDocumentState {
61
62
  thermo?: Readonly<Record<string, TiledSurface>>;
62
63
  /** Coarse document-level heat field (1/4 resolution) for thermochromic ink and wet-paint drying. See `heat.ts`. */
63
64
  heat?: HeatField;
65
+ /** Ferrofluid paint: each layer's liquid ferrofluid channel, 0..255 per pixel in layer space (layers without ferrofluid are missing). See `ferro.ts`. */
66
+ ferro?: Readonly<Record<string, TiledSurface>>;
67
+ /** Per-layer Rosensweig spike settings (`wavelength`, `threshold`) from the last ferrofluid stroke on that layer. */
68
+ ferroSettings?: Readonly<Record<string, FerroSettings>>;
69
+ /** Dipole magnets placed on the canvas (document pixel coordinates). */
70
+ magnets?: PaintMagnet[];
64
71
  /** Thick paint: each layer's height and material channels (layers without relief are missing). */
65
72
  thick?: Readonly<Record<string, ThickChannels>>;
66
73
  /** Effect layers: the provider effect each one is drawn from (layers that are gone are ignored). */
@@ -79,7 +86,7 @@ export interface PaintDocumentState {
79
86
  /** A change a replay repeats besides strokes, recorded with how many strokes came before it. */
80
87
  export interface PaintEvent {
81
88
  at: number;
82
- kind: 'fill' | 'fill-region' | 'clear' | 'flatten' | 'stickers' | 'live-strokes';
89
+ kind: 'fill' | 'fill-region' | 'clear' | 'flatten' | 'stickers' | 'live-strokes' | 'magnets' | 'cure-ferrofluid';
83
90
  layerId: string;
84
91
  /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers or live strokes after an edit. */
85
92
  data?: Record<string, unknown>;
@@ -13,6 +13,7 @@ import { type PaperSettings } from './paper.js';
13
13
  import type { RecordedStroke } from './strokes.js';
14
14
  import type { WetEvent } from './wet.js';
15
15
  import type { ThickChannels } from './thick.js';
16
+ import type { FerroSettings, PaintMagnet } from './ferro.js';
16
17
  import type { Sticker } from './stickers.js';
17
18
  import type { PaintEffectSource, PaintPaperEffect } from './effects.js';
18
19
  import type { HeatField } from './heat.js';
@@ -61,6 +62,12 @@ export interface PaintDocumentState {
61
62
  thermo?: Readonly<Record<string, TiledSurface>>;
62
63
  /** Coarse document-level heat field (1/4 resolution) for thermochromic ink and wet-paint drying. See `heat.ts`. */
63
64
  heat?: HeatField;
65
+ /** Ferrofluid paint: each layer's liquid ferrofluid channel, 0..255 per pixel in layer space (layers without ferrofluid are missing). See `ferro.ts`. */
66
+ ferro?: Readonly<Record<string, TiledSurface>>;
67
+ /** Per-layer Rosensweig spike settings (`wavelength`, `threshold`) from the last ferrofluid stroke on that layer. */
68
+ ferroSettings?: Readonly<Record<string, FerroSettings>>;
69
+ /** Dipole magnets placed on the canvas (document pixel coordinates). */
70
+ magnets?: PaintMagnet[];
64
71
  /** Thick paint: each layer's height and material channels (layers without relief are missing). */
65
72
  thick?: Readonly<Record<string, ThickChannels>>;
66
73
  /** Effect layers: the provider effect each one is drawn from (layers that are gone are ignored). */
@@ -79,7 +86,7 @@ export interface PaintDocumentState {
79
86
  /** A change a replay repeats besides strokes, recorded with how many strokes came before it. */
80
87
  export interface PaintEvent {
81
88
  at: number;
82
- kind: 'fill' | 'fill-region' | 'clear' | 'flatten' | 'stickers' | 'live-strokes';
89
+ kind: 'fill' | 'fill-region' | 'clear' | 'flatten' | 'stickers' | 'live-strokes' | 'magnets' | 'cure-ferrofluid';
83
90
  layerId: string;
84
91
  /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers or live strokes after an edit. */
85
92
  data?: Record<string, unknown>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent tools for ferrofluid paint and magnets: ferrofluid strokes are painted with `paint_stroke`
3
+ * and a brush whose `ferro.amount` is above 0 (`ferrofluid`, `ferro-ink`); these tools place,
4
+ * move and remove magnets (`add_magnet`, `update_magnet`, `remove_magnets`, `list_magnets`), read
5
+ * a layer's liquid ferrofluid and live Rosensweig spike stats (`get_ferrofluid`), and cure the
6
+ * liquid spikes into solid impasto relief (`cure_ferrofluid`).
7
+ */
8
+ import type { PaintAgentTool } from './hosted.cjs';
9
+ import type { PaintEditorController } from './controller.cjs';
10
+ export declare function createFerroTools(c: PaintEditorController): PaintAgentTool[];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent tools for ferrofluid paint and magnets: ferrofluid strokes are painted with `paint_stroke`
3
+ * and a brush whose `ferro.amount` is above 0 (`ferrofluid`, `ferro-ink`); these tools place,
4
+ * move and remove magnets (`add_magnet`, `update_magnet`, `remove_magnets`, `list_magnets`), read
5
+ * a layer's liquid ferrofluid and live Rosensweig spike stats (`get_ferrofluid`), and cure the
6
+ * liquid spikes into solid impasto relief (`cure_ferrofluid`).
7
+ */
8
+ import type { PaintAgentTool } from './hosted.js';
9
+ import type { PaintEditorController } from './controller.js';
10
+ export declare function createFerroTools(c: PaintEditorController): PaintAgentTool[];