@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
@@ -18,7 +18,7 @@
18
18
 
19
19
 
20
20
 
21
- var _chunk5HR5CSLCcjs = require('../chunk-5HR5CSLC.cjs');
21
+ var _chunkJVFGK3DKcjs = require('../chunk-JVFGK3DK.cjs');
22
22
  require('../chunk-PFHJS4HE.cjs');
23
23
 
24
24
 
@@ -27,7 +27,7 @@ require('../chunk-PFHJS4HE.cjs');
27
27
 
28
28
 
29
29
 
30
- var _chunkQNNZA3V5cjs = require('../chunk-QNNZA3V5.cjs');
30
+ var _chunkH3EME3S5cjs = require('../chunk-H3EME3S5.cjs');
31
31
 
32
32
 
33
33
 
@@ -102,7 +102,7 @@ var _chunkQNNZA3V5cjs = require('../chunk-QNNZA3V5.cjs');
102
102
 
103
103
 
104
104
 
105
- var _chunkQ33V5PEFcjs = require('../chunk-Q33V5PEF.cjs');
105
+ var _chunkHRUK5TFWcjs = require('../chunk-HRUK5TFW.cjs');
106
106
 
107
107
 
108
108
  var _chunkRDFHWAHLcjs = require('../chunk-RDFHWAHL.cjs');
@@ -167,20 +167,20 @@ var ARIA = {
167
167
  "livePatterns.pattern": "Live pattern"
168
168
  };
169
169
  var aria = (path, label) => _nullishCoalesce(ARIA[path], () => ( `Brush ${label.toLowerCase()}`));
170
- function brushEditorSections(brush, profile = _chunkQ33V5PEFcjs.PAINT_PROFILE, context = {}) {
171
- const resolved = _chunkQ33V5PEFcjs.resolveBrush.call(void 0, brush);
172
- const engine = _chunkQ33V5PEFcjs.resolveEngine.call(void 0, brush);
173
- const fieldOf = (facet) => _chunkQ33V5PEFcjs.BRUSH_FIELDS.find((f) => f.facet === facet && f.engines.includes(engine));
174
- const facetOf = (enginePath) => _optionalChain([(_nullishCoalesce(_chunkQ33V5PEFcjs.BRUSH_FIELDS.find((f) => f.engine === enginePath && f.engines.includes(engine)), () => ( _chunkQ33V5PEFcjs.BRUSH_FIELDS.find((f) => f.engine === enginePath)))), 'optionalAccess', _ => _.facet]);
170
+ function brushEditorSections(brush, profile = _chunkHRUK5TFWcjs.PAINT_PROFILE, context = {}) {
171
+ const resolved = _chunkHRUK5TFWcjs.resolveBrush.call(void 0, brush);
172
+ const engine = _chunkHRUK5TFWcjs.resolveEngine.call(void 0, brush);
173
+ const fieldOf = (facet) => _chunkHRUK5TFWcjs.BRUSH_FIELDS.find((f) => f.facet === facet && f.engines.includes(engine));
174
+ const facetOf = (enginePath) => _optionalChain([(_nullishCoalesce(_chunkHRUK5TFWcjs.BRUSH_FIELDS.find((f) => f.engine === enginePath && f.engines.includes(engine)), () => ( _chunkHRUK5TFWcjs.BRUSH_FIELDS.find((f) => f.engine === enginePath)))), 'optionalAccess', _ => _.facet]);
175
175
  const value = (path) => {
176
- const own = _chunkQ33V5PEFcjs.getPath.call(void 0, brush, path);
176
+ const own = _chunkHRUK5TFWcjs.getPath.call(void 0, brush, path);
177
177
  if (own !== void 0) return own;
178
178
  const field = fieldOf(path);
179
179
  if (!field) return void 0;
180
- const v = _chunkQ33V5PEFcjs.getPath.call(void 0, resolved, field.engine);
180
+ const v = _chunkHRUK5TFWcjs.getPath.call(void 0, resolved, field.engine);
181
181
  return field.map ? field.map.toFacet(v) : v;
182
182
  };
183
- const issues = _chunkQ33V5PEFcjs.checkBrush.call(void 0, brush, profile).issues;
183
+ const issues = _chunkHRUK5TFWcjs.checkBrush.call(void 0, brush, profile).issues;
184
184
  const warned = new Map(issues.filter((i) => i.severity === "warning").map((i) => [i.facet, i.reason]));
185
185
  const refused = new Set(issues.filter((i) => i.severity === "error").map((i) => i.facet));
186
186
  const full = (s) => s === "full";
@@ -205,30 +205,30 @@ function brushEditorSections(brush, profile = _chunkQ33V5PEFcjs.PAINT_PROFILE, c
205
205
  };
206
206
  const isTool = brush.kind === "tool";
207
207
  const shape = brush.tip.shape;
208
- const sourceOptions = (_nullishCoalesce(context.sources, () => ( []))).map((x) => ({ value: _chunkQ33V5PEFcjs.brushSourceRef.call(void 0, x.id), label: x.name }));
208
+ const sourceOptions = (_nullishCoalesce(context.sources, () => ( []))).map((x) => ({ value: _chunkHRUK5TFWcjs.brushSourceRef.call(void 0, x.id), label: x.name }));
209
209
  const marks = _nullishCoalesce(_optionalChain([brush, 'access', _7 => _7.marks, 'optionalAccess', _8 => _8.kind]), () => ( "continuous"));
210
210
  const exchangeFull = full(_optionalChain([brush, 'access', _9 => _9.behaviour, 'optionalAccess', _10 => _10.exchange, 'optionalAccess', _11 => _11.reservoir]) ? profile.reservoir : profile.exchange);
211
211
  add("medium", "Medium", [
212
212
  profile.kinds.length > 1 && profile.kinds.includes(brush.kind) && choice("kind", "Kind", options(profile.kinds, { mark: "Lays a material", tool: "Works on paint" })),
213
- !isTool && choice("medium", "Medium", [{ value: "", label: "Any" }, ...options(_chunkQ33V5PEFcjs.BRUSH_MEDIA, MEDIUM_LABELS)], ""),
213
+ !isTool && choice("medium", "Medium", [{ value: "", label: "Any" }, ...options(_chunkHRUK5TFWcjs.BRUSH_MEDIA, MEDIUM_LABELS)], ""),
214
214
  !isTool && withNote({ kind: "text", path: "variety", label: "Variety", ariaLabel: "Brush variety", value: _nullishCoalesce(brush.variety, () => ( "")), placeholder: "pencil, oil, highlighter\u2026" }),
215
215
  isTool && !!brush.tool && profile.tools.includes(brush.tool) && choice("tool", "Tool", options(profile.tools, TOOL_LABELS))
216
216
  ]);
217
- const engineControls = _chunk5HR5CSLCcjs.brushEngineControls.call(void 0, resolved);
218
- const tipSliders = _chunk5HR5CSLCcjs.BRUSH_SLIDERS.filter((c) => c.path.startsWith("tip.") && c.path !== "tip.smoothing");
217
+ const engineControls = _chunkJVFGK3DKcjs.brushEngineControls.call(void 0, resolved);
218
+ const tipSliders = _chunkJVFGK3DKcjs.BRUSH_SLIDERS.filter((c) => c.path.startsWith("tip.") && c.path !== "tip.smoothing");
219
219
  add("shape", "Shape", [
220
220
  !refused.has("tip.shape") && choice("tip.shape", "Tip", options(profile.tipShapes.filter((s) => !isTool || s !== "stamp"), SHAPE_LABELS)),
221
221
  ...tipSliders.map((c) => slider(c)),
222
222
  engine === "dab" && choice("tip.blend", "Blend", _chunk2VX55NFMcjs.BLEND_MODES.map((m) => ({ value: m.id, label: m.label })), "normal"),
223
223
  ...shape === "bristle" && engineOk && engineControls ? engineControls.sliders.filter((c) => c.path.startsWith("bristle.")).map((c) => slider(c)) : [],
224
- shape === "stamp" && engineOk && withNote({ kind: "stamps", path: "tip.stamps", label: "Stamps", ariaLabel: "Brush stamps", value: [..._nullishCoalesce(value("tip.stamps"), () => ( []))], max: marks === "particles" ? _chunkQ33V5PEFcjs.MAX_DOT_TEXTURES : _chunkQ33V5PEFcjs.MAX_STICKER_STAMPS, options: marks === "objects" ? STAMP_OPTIONS : [...STAMP_OPTIONS, ...sourceOptions] })
224
+ shape === "stamp" && engineOk && withNote({ kind: "stamps", path: "tip.stamps", label: "Stamps", ariaLabel: "Brush stamps", value: [..._nullishCoalesce(value("tip.stamps"), () => ( []))], max: marks === "particles" ? _chunkHRUK5TFWcjs.MAX_DOT_TEXTURES : _chunkHRUK5TFWcjs.MAX_STICKER_STAMPS, options: marks === "objects" ? STAMP_OPTIONS : [...STAMP_OPTIONS, ...sourceOptions] })
225
225
  ]);
226
226
  const dabs = DAB_ENGINES.has(engine) && engineOk && shape !== "bristle" && marks !== "particles" && marks !== "objects";
227
227
  const grain = brush.tip.grain;
228
228
  if (dabs && full(_nullishCoalesce(profile.tipGrain, () => ( "none")))) {
229
229
  add("grain", "Tip grain", [
230
- choice("tip.grain.source", "Texture", [{ value: "", label: "None" }, ...options(_chunkQ33V5PEFcjs.TIP_GRAIN_PATTERNS), ...sourceOptions], ""),
231
- !!grain && choice("tip.grain.mode", "Grain moves", options(_chunkQ33V5PEFcjs.BRUSH_GRAIN_MODES, GRAIN_MODE_LABELS), "texturised"),
230
+ choice("tip.grain.source", "Texture", [{ value: "", label: "None" }, ...options(_chunkHRUK5TFWcjs.TIP_GRAIN_PATTERNS), ...sourceOptions], ""),
231
+ !!grain && choice("tip.grain.mode", "Grain moves", options(_chunkHRUK5TFWcjs.BRUSH_GRAIN_MODES, GRAIN_MODE_LABELS), "texturised"),
232
232
  !!grain && withNote({ kind: "slider", path: "tip.grain.scale", label: "Grain scale", ariaLabel: "Brush grain scale", min: 0.1, max: 8, step: 0.05, value: _nullishCoalesce(grain.scale, () => ( 1)) }),
233
233
  !!grain && withNote({ kind: "slider", path: "tip.grain.depth", label: "Grain depth", ariaLabel: "Brush grain depth", min: 0, max: 1, step: 0.01, value: _nullishCoalesce(grain.depth, () => ( 0.6)) }),
234
234
  !!grain && withNote({ kind: "slider", path: "tip.grain.contrast", label: "Grain contrast", ariaLabel: "Brush grain contrast", min: 0, max: 3, step: 0.05, value: _nullishCoalesce(grain.contrast, () => ( 1)) }),
@@ -241,28 +241,28 @@ function brushEditorSections(brush, profile = _chunkQ33V5PEFcjs.PAINT_PROFILE, c
241
241
  const render = value("marks.render");
242
242
  add("marks", "Marks", [
243
243
  choice("marks.kind", "Laid as", options(profile.marks, MARKS_LABELS), "continuous"),
244
- marks === "particles" && choice("marks.physics", "Particles", options(_chunkQ33V5PEFcjs.PARTICLE_KINDS, PHYSICS_LABELS)),
245
- marks === "particles" && choice("marks.render", "Drawn as", options(_chunkQ33V5PEFcjs.PARTICLE_RENDERS, RENDER_LABELS)),
246
- marks === "particles" && render === "ribbon" && choice("marks.ribbonPairs", "Ribbon joins", options(_chunkQ33V5PEFcjs.RIBBON_PAIRS, { adjacent: "Neighbours", "first-last": "Outermost" })),
247
- marks === "particles" && shape === "stamp" && choice("marks.stampOrder", "Texture order", options(_chunkQ33V5PEFcjs.DOT_TEXTURE_ORDERS, { sequence: "In turn", random: "Random" })),
248
- marks === "particles" && shape === "stamp" && choice("marks.stampRotation", "Texture turn", options(_chunkQ33V5PEFcjs.DOT_ROTATIONS, { follow: "Follow motion", random: "Random", fixed: "Tip angle" })),
244
+ marks === "particles" && choice("marks.physics", "Particles", options(_chunkHRUK5TFWcjs.PARTICLE_KINDS, PHYSICS_LABELS)),
245
+ marks === "particles" && choice("marks.render", "Drawn as", options(_chunkHRUK5TFWcjs.PARTICLE_RENDERS, RENDER_LABELS)),
246
+ marks === "particles" && render === "ribbon" && choice("marks.ribbonPairs", "Ribbon joins", options(_chunkHRUK5TFWcjs.RIBBON_PAIRS, { adjacent: "Neighbours", "first-last": "Outermost" })),
247
+ marks === "particles" && shape === "stamp" && choice("marks.stampOrder", "Texture order", options(_chunkHRUK5TFWcjs.DOT_TEXTURE_ORDERS, { sequence: "In turn", random: "Random" })),
248
+ marks === "particles" && shape === "stamp" && choice("marks.stampRotation", "Texture turn", options(_chunkHRUK5TFWcjs.DOT_ROTATIONS, { follow: "Follow motion", random: "Random", fixed: "Tip angle" })),
249
249
  ...particles.map((c) => slider(c)),
250
250
  ...objects.map((c) => slider(c))
251
251
  ]);
252
252
  }
253
- const feel = _chunk5HR5CSLCcjs.BRUSH_SLIDERS.filter((c) => ["tip.smoothing", "pressureSmoothing", "dynamics.minSize", "dynamics.tiltSize", "dynamics.velocitySize"].includes(c.path)).filter((c) => !(c.path === "dynamics.tiltSize" && !full(profile.tilt)) && !(c.path === "dynamics.velocitySize" && !full(profile.velocity)));
253
+ const feel = _chunkJVFGK3DKcjs.BRUSH_SLIDERS.filter((c) => ["tip.smoothing", "pressureSmoothing", "dynamics.minSize", "dynamics.tiltSize", "dynamics.velocitySize"].includes(c.path)).filter((c) => !(c.path === "dynamics.tiltSize" && !full(profile.tilt)) && !(c.path === "dynamics.velocitySize" && !full(profile.velocity)));
254
254
  const stampDabs = dabs && shape === "stamp";
255
255
  const jitter = (path, label, max = 1, step = 0.01) => withNote({ kind: "slider", path, label, ariaLabel: aria(path, label), min: 0, max, step, value: Number(_nullishCoalesce(value(path), () => ( 0))) });
256
256
  add("feel", "Feel", [
257
257
  ...feel.map((c) => slider(c)),
258
258
  toggle("dynamics.pressureSize", "Pressure \u2192 size"),
259
259
  toggle("dynamics.pressureOpacity", "Pressure \u2192 opacity"),
260
- choice("dynamics.rotation", "Rotation", options(_chunkQ33V5PEFcjs.BRUSH_ROTATIONS, ROTATION_LABELS)),
260
+ choice("dynamics.rotation", "Rotation", options(_chunkHRUK5TFWcjs.BRUSH_ROTATIONS, ROTATION_LABELS)),
261
261
  stampDabs && full(_nullishCoalesce(profile.dabCount, () => ( "none"))) && withNote({ kind: "slider", path: "dynamics.count", label: "Count", ariaLabel: "Brush dab count", min: 1, max: 16, step: 1, value: _nullishCoalesce(_optionalChain([brush, 'access', _12 => _12.dynamics, 'optionalAccess', _13 => _13.count]), () => ( 1)) }),
262
262
  stampDabs && jitter("dynamics.scatter", "Scatter", 4),
263
263
  stampDabs && jitter("dynamics.angleJitter", "Rotation jitter"),
264
264
  stampDabs && jitter("dynamics.sizeJitter", "Size jitter"),
265
- stampDabs && choice("dynamics.flip", "Flip", options(_chunkQ33V5PEFcjs.BRUSH_FLIPS, FLIP_LABELS), "none")
265
+ stampDabs && choice("dynamics.flip", "Flip", options(_chunkHRUK5TFWcjs.BRUSH_FLIPS, FLIP_LABELS), "none")
266
266
  ]);
267
267
  const surfaces = profile.surfaces;
268
268
  const engineSliders = (engineOk ? _nullishCoalesce(_optionalChain([engineControls, 'optionalAccess', _14 => _14.sliders]), () => ( [])) : []).filter((c) => !/^(bristle|particle|sticker|iridescence)\./.test(c.path) && c.path !== "light.amount" && c.path !== "impasto.rim" && c.path !== "impasto.clarity");
@@ -270,8 +270,8 @@ function brushEditorSections(brush, profile = _chunkQ33V5PEFcjs.PAINT_PROFILE, c
270
270
  add("behaviour", _nullishCoalesce(_optionalChain([engineControls, 'optionalAccess', _17 => _17.title]), () => ( "Behaviour")), [
271
271
  !isTool && surfaces.length > 1 && !refused.has("behaviour.surface") && choice("behaviour.surface", "Surface", options(surfaces, SURFACE_LABELS), "dry"),
272
272
  full(profile.grain) && slider({ path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 }, "behaviour.grain"),
273
- engine === "wet" && engineOk && !isTool && choice("behaviour.wet.medium", "Medium", options(_chunkQ33V5PEFcjs.WET_MEDIA, WET_MEDIUM_LABELS), "watercolor"),
274
- engine === "impasto" && engineOk && choice("behaviour.thick.applicator", "Tool", options(_chunkQ33V5PEFcjs.IMPASTO_TOOLS, APPLICATOR_LABELS), "brush"),
273
+ engine === "wet" && engineOk && !isTool && choice("behaviour.wet.medium", "Medium", options(_chunkHRUK5TFWcjs.WET_MEDIA, WET_MEDIUM_LABELS), "watercolor"),
274
+ engine === "impasto" && engineOk && choice("behaviour.thick.applicator", "Tool", options(_chunkHRUK5TFWcjs.IMPASTO_TOOLS, APPLICATOR_LABELS), "brush"),
275
275
  ...engineSliders.map((c) => exchangeOk(facetOf(c.path)) ? slider(c) : null),
276
276
  engineOk && _optionalChain([engineControls, 'optionalAccess', _18 => _18.mixingPath]) && (engine === "wet" || exchangeFull) && fieldOf("behaviour.mixing") && choice("behaviour.mixing", "Mixing", options(["pigment", "rgb"], { pigment: "Pigment", rgb: "RGB" })),
277
277
  engineOk && _optionalChain([engineControls, 'optionalAccess', _19 => _19.dirty]) && exchangeFull && fieldOf("behaviour.exchange.dirty") && toggle("behaviour.exchange.dirty", "Dirty brush"),
@@ -313,7 +313,7 @@ function brushEditorSections(brush, profile = _chunkQ33V5PEFcjs.PAINT_PROFILE, c
313
313
  label: "Palette",
314
314
  ariaLabel: "Brush palette",
315
315
  value: (_nullishCoalesce(_optionalChain([brush, 'access', _32 => _32.colour, 'optionalAccess', _33 => _33.palette]), () => ( []))).flatMap((c) => _chunkMLLTWAXHcjs.isDualColor.call(void 0, c) ? [{ light: _chunkJBZRVLXVcjs.brushColourHex.call(void 0, c, "light"), dark: _chunkJBZRVLXVcjs.brushColourHex.call(void 0, c, "dark") }] : hex(c) ? [hex(c)] : []),
316
- max: _chunkQ33V5PEFcjs.MAX_BRUSH_PALETTE
316
+ max: _chunkHRUK5TFWcjs.MAX_BRUSH_PALETTE
317
317
  }),
318
318
  toggle("colour.own", "Always its own colour")
319
319
  ]);
@@ -363,15 +363,15 @@ function hiddenLabel(issue, b) {
363
363
  case "dynamics.tilt":
364
364
  return "tilt dynamics";
365
365
  case "colour.default":
366
- return `a ${_chunkQ33V5PEFcjs.brushColourKind.call(void 0, b.colour.default)} default colour`;
366
+ return `a ${_chunkHRUK5TFWcjs.brushColourKind.call(void 0, b.colour.default)} default colour`;
367
367
  default:
368
368
  return null;
369
369
  }
370
370
  }
371
371
  var sentence = (words) => words.length < 2 ? _nullishCoalesce(words[0], () => ( "")) : `${words.slice(0, -1).join(", ")} and ${words.at(-1)}`;
372
- function brushEditorScope(brush, profile = _chunkQ33V5PEFcjs.PAINT_PROFILE) {
372
+ function brushEditorScope(brush, profile = _chunkHRUK5TFWcjs.PAINT_PROFILE) {
373
373
  const hidden = [];
374
- for (const issue of _chunkQ33V5PEFcjs.checkBrush.call(void 0, brush, profile).issues) {
374
+ for (const issue of _chunkHRUK5TFWcjs.checkBrush.call(void 0, brush, profile).issues) {
375
375
  const label = hiddenLabel(issue, brush);
376
376
  if (label && !hidden.some((h) => h.label === label)) hidden.push({ facet: issue.facet, label });
377
377
  }
@@ -384,17 +384,17 @@ function newBrushFor(profile, from, name = "New brush") {
384
384
  const id = brushIdFromName(name);
385
385
  const base = { id, name, kind: profile.kinds.includes("mark") ? "mark" : profile.kinds[0], ...profile.kinds.includes("mark") ? {} : { tool: profile.tools[0] }, tip: { shape: profile.tipShapes.includes("round") ? "round" : profile.tipShapes[0] } };
386
386
  if (from) {
387
- const stripped = _chunkQ33V5PEFcjs.degrade.call(void 0, from, profile, { ignored: true });
387
+ const stripped = _chunkHRUK5TFWcjs.degrade.call(void 0, from, profile, { ignored: true });
388
388
  if (!stripped.issues.length) return { ...stripped.brush, id, name };
389
389
  }
390
390
  return base;
391
391
  }
392
392
  async function saveBrushEdit(library, options2) {
393
393
  const { original, profile } = options2;
394
- const check = _chunkQ33V5PEFcjs.checkBrushEdit.call(void 0, original, options2.brush, profile);
394
+ const check = _chunkHRUK5TFWcjs.checkBrushEdit.call(void 0, original, options2.brush, profile);
395
395
  if (!check.ok) {
396
396
  const errors = check.issues.filter((i) => i.severity === "error").map((i) => `${i.facet}: ${i.reason} ${i.fix}`);
397
- throw new (0, _chunkQ33V5PEFcjs.BrushLibraryError)("invalid", `${profile.name} can't use this brush: ${errors.join(" ")} Nothing was saved.`);
397
+ throw new (0, _chunkHRUK5TFWcjs.BrushLibraryError)("invalid", `${profile.name} can't use this brush: ${errors.join(" ")} Nothing was saved.`);
398
398
  }
399
399
  const inPlace = !!original && options2.name === void 0 && !library.isBuiltin(original.id) && original.id === options2.brush.id && !!await library.get(original.id);
400
400
  let brush = options2.brush;
@@ -421,6 +421,7 @@ function setBrushEditorValue(brush, path, value) {
421
421
  delete next.iridescence;
422
422
  delete next.ghost;
423
423
  delete next.mutation;
424
+ delete next.ferro;
424
425
  delete next.colour;
425
426
  next.tool = _nullishCoalesce(next.tool, () => ( "eraser"));
426
427
  if (next.tip.shape === "stamp") next.tip = { shape: "round", ...next.tip.size !== void 0 ? { size: next.tip.size } : {} };
@@ -442,13 +443,13 @@ function setBrushEditorValue(brush, path, value) {
442
443
  if (value !== "bristle") delete next.tip.bristle;
443
444
  } else if (value === void 0 || value === "") {
444
445
  const keys = path.split(".");
445
- const parent = _chunkQ33V5PEFcjs.getPath.call(void 0, next, keys.slice(0, -1).join("."));
446
+ const parent = _chunkHRUK5TFWcjs.getPath.call(void 0, next, keys.slice(0, -1).join("."));
446
447
  if (keys.length === 1) delete next[keys[0]];
447
448
  else if (parent) delete parent[keys[keys.length - 1]];
448
- } else _chunkQ33V5PEFcjs.setPath.call(void 0, next, path, value);
449
+ } else _chunkHRUK5TFWcjs.setPath.call(void 0, next, path, value);
449
450
  const problems = [];
450
451
  try {
451
- return { brush: _chunkQ33V5PEFcjs.normalizeBrushSpec.call(void 0, JSON.parse(JSON.stringify(next)), problems), problems };
452
+ return { brush: _chunkHRUK5TFWcjs.normalizeBrushSpec.call(void 0, JSON.parse(JSON.stringify(next)), problems), problems };
452
453
  } catch (e) {
453
454
  return { brush, problems: [e.message] };
454
455
  }
@@ -464,7 +465,7 @@ function brushLibraryGroups(entries, filter = {}) {
464
465
  const text = words([b.id, b.name, _nullishCoalesce(b.variety, () => ( "")), _nullishCoalesce(b.medium, () => ( "")), _nullishCoalesce(b.tool, () => ( "")), ..._nullishCoalesce(b.tags, () => ( []))].join(" "));
465
466
  if (!wanted.every((w) => text.some((t) => t.startsWith(w)))) continue;
466
467
  }
467
- if (filter.profile && !_chunkQ33V5PEFcjs.checkBrush.call(void 0, b, filter.profile).ok) continue;
468
+ if (filter.profile && !_chunkHRUK5TFWcjs.checkBrush.call(void 0, b, filter.profile).ok) continue;
468
469
  const id = b.kind === "tool" ? "tools" : _nullishCoalesce(b.medium, () => ( "other"));
469
470
  let g = groups.get(id);
470
471
  if (!g) groups.set(id, g = { id, title: id === "tools" ? "Tools" : id === "other" ? "Other" : MEDIUM_LABELS[id], varieties: [] });
@@ -480,7 +481,7 @@ async function imageFileToPngDataUrl(file) {
480
481
  const bytes = new Uint8Array(await file.arrayBuffer());
481
482
  if (_chunkW5M3J7PVcjs.isPng.call(void 0, bytes)) return `data:image/png;base64,${_chunkW5M3J7PVcjs.bytesToBase64.call(void 0, bytes)}`;
482
483
  const g = globalThis;
483
- if (!g.createImageBitmap || !g.OffscreenCanvas) throw new (0, _chunkQ33V5PEFcjs.BrushSourceError)("Only PNG images can be read here; convert the image to PNG.");
484
+ if (!g.createImageBitmap || !g.OffscreenCanvas) throw new (0, _chunkHRUK5TFWcjs.BrushSourceError)("Only PNG images can be read here; convert the image to PNG.");
484
485
  const bitmap = await g.createImageBitmap(file);
485
486
  const canvas = new g.OffscreenCanvas(bitmap.width, bitmap.height);
486
487
  canvas.getContext("2d").drawImage(bitmap, 0, 0);
@@ -490,7 +491,7 @@ async function imageFileToPngDataUrl(file) {
490
491
  async function addBrushSourceFromFile(library, file, options2 = {}) {
491
492
  const name = (_nullishCoalesce(_nullishCoalesce(options2.name, () => ( _optionalChain([file, 'access', _38 => _38.name, 'optionalAccess', _39 => _39.replace, 'call', _40 => _40(/\.[a-z0-9]+$/i, "")]))), () => ( "Image"))).trim() || "Image";
492
493
  const result = await library.addSource({ name, image: await imageFileToPngDataUrl(file), origin: file.name, ...options2.channel ? { channel: options2.channel } : {} });
493
- return { ...result, ref: _chunkQ33V5PEFcjs.brushSourceRef.call(void 0, result.source.id) };
494
+ return { ...result, ref: _chunkHRUK5TFWcjs.brushSourceRef.call(void 0, result.source.id) };
494
495
  }
495
496
  function withBrushSource(brush, ref, as) {
496
497
  if (as === "grain") return setBrushEditorValue(brush, "tip.grain.source", ref);
@@ -508,11 +509,11 @@ var BRUSH_PROFILE_JSON_SCHEMA = {
508
509
  properties: {
509
510
  id: { type: "string", description: "paint, image, canvas, vector\u2026" },
510
511
  name: { type: "string", description: "Display name, used in messages." },
511
- kinds: { type: "array", items: { type: "string", enum: [..._chunkQ33V5PEFcjs.BRUSH_KINDS] }, description: "Brush kinds it has." },
512
- tools: { type: "array", items: { type: "string", enum: [..._chunkQ33V5PEFcjs.BRUSH_TOOLS] }, description: "Tools it has (kind tool)." },
513
- tipShapes: { type: "array", items: { type: "string", enum: [..._chunkQ33V5PEFcjs.BRUSH_TIP_SHAPES] }, description: "Tip shapes it renders." },
514
- marks: { type: "array", items: { type: "string", enum: [..._chunkQ33V5PEFcjs.BRUSH_MARKS_KINDS] }, description: "Marks kinds it renders." },
515
- surfaces: { type: "array", items: { type: "string", enum: [..._chunkQ33V5PEFcjs.BRUSH_SURFACES] }, description: "Surfaces it renders." },
512
+ kinds: { type: "array", items: { type: "string", enum: [..._chunkHRUK5TFWcjs.BRUSH_KINDS] }, description: "Brush kinds it has." },
513
+ tools: { type: "array", items: { type: "string", enum: [..._chunkHRUK5TFWcjs.BRUSH_TOOLS] }, description: "Tools it has (kind tool)." },
514
+ tipShapes: { type: "array", items: { type: "string", enum: [..._chunkHRUK5TFWcjs.BRUSH_TIP_SHAPES] }, description: "Tip shapes it renders." },
515
+ marks: { type: "array", items: { type: "string", enum: [..._chunkHRUK5TFWcjs.BRUSH_MARKS_KINDS] }, description: "Marks kinds it renders." },
516
+ surfaces: { type: "array", items: { type: "string", enum: [..._chunkHRUK5TFWcjs.BRUSH_SURFACES] }, description: "Surfaces it renders." },
516
517
  exchange: { ...SUPPORT, description: "Picking paint up and mixing it in." },
517
518
  reservoir: { ...SUPPORT, description: "The Mixer Brush reservoir." },
518
519
  grain: { ...SUPPORT, description: "Paper grain response." },
@@ -524,7 +525,8 @@ var BRUSH_PROFILE_JSON_SCHEMA = {
524
525
  tipGrain: { ...SUPPORT, description: "Texture inside the tip (tip.grain); missing: none." },
525
526
  dabCount: { ...SUPPORT, description: "Several dabs per spacing step (dynamics.count); missing: none." },
526
527
  livePatterns: { ...SUPPORT, description: "Gray-Scott reaction-diffusion patterns in wet paint (livePatterns); missing: none." },
527
- iridescence: { ...SUPPORT, description: "Thin-film iridescence (iridescence); missing: none." }
528
+ iridescence: { ...SUPPORT, description: "Thin-film iridescence (iridescence); missing: none." },
529
+ ferro: { ...SUPPORT, description: "Liquid ferrofluid (ferro); missing: none." }
528
530
  },
529
531
  required: ["id", "name", "kinds", "tools", "tipShapes", "marks", "surfaces", "exchange", "reservoir", "grain", "light", "motion", "colours", "velocity", "tilt"],
530
532
  additionalProperties: false
@@ -538,16 +540,16 @@ var BRUSH_LIBRARY_FILE_JSON_SCHEMA = {
538
540
  brushes: { type: "array", items: { $ref: "brush.schema.json" }, description: "The brushes, each a facet brush (brush.schema.json)." },
539
541
  sources: {
540
542
  type: "array",
541
- maxItems: _chunkQ33V5PEFcjs.MAX_BRUSH_SOURCES,
543
+ maxItems: _chunkHRUK5TFWcjs.MAX_BRUSH_SOURCES,
542
544
  description: "The image sources the brushes name (source:<id> in tip.stamps or tip.grain.source), bytes included.",
543
545
  items: {
544
546
  type: "object",
545
547
  description: "An image source: a grey mask, 255 paints (or is a grain peak).",
546
548
  properties: {
547
- id: { type: "string", pattern: _chunkQ33V5PEFcjs.BRUSH_ID.source, description: "Source id." },
549
+ id: { type: "string", pattern: _chunkHRUK5TFWcjs.BRUSH_ID.source, description: "Source id." },
548
550
  name: { type: "string", maxLength: 80, description: "Display name." },
549
- width: { type: "integer", minimum: 1, maximum: _chunkQ33V5PEFcjs.MAX_BRUSH_SOURCE_SIZE, description: "Width in px." },
550
- height: { type: "integer", minimum: 1, maximum: _chunkQ33V5PEFcjs.MAX_BRUSH_SOURCE_SIZE, description: "Height in px." },
551
+ width: { type: "integer", minimum: 1, maximum: _chunkHRUK5TFWcjs.MAX_BRUSH_SOURCE_SIZE, description: "Width in px." },
552
+ height: { type: "integer", minimum: 1, maximum: _chunkHRUK5TFWcjs.MAX_BRUSH_SOURCE_SIZE, description: "Height in px." },
551
553
  image: { type: "string", pattern: "^data:image/png;base64,", description: "The mask as a greyscale PNG data: URL." },
552
554
  licence: { type: "string", maxLength: 2e3, description: "The licence or terms it came with." },
553
555
  origin: { type: "string", maxLength: 200, description: "Where it came from (a file or pack name)." }
@@ -561,7 +563,7 @@ var BRUSH_LIBRARY_FILE_JSON_SCHEMA = {
561
563
  additionalProperties: false
562
564
  };
563
565
  function migrationInputs() {
564
- const pick = (id) => _chunkQ33V5PEFcjs.brushToJSON.call(void 0, _chunkQ33V5PEFcjs.BUILTIN_BRUSHES.find((b) => b.id === id));
566
+ const pick = (id) => _chunkHRUK5TFWcjs.brushToJSON.call(void 0, _chunkHRUK5TFWcjs.BUILTIN_BRUSHES.find((b) => b.id === id));
565
567
  return [
566
568
  { id: "plain", name: "Plain" },
567
569
  pick("pencil-6b"),
@@ -583,34 +585,34 @@ function migrationInputs() {
583
585
  function editorSectionFixtures() {
584
586
  const ids = ["highlighter", "chalk", "watercolor-wash", "hair-strands", "mixer-brush", "blender", "spray-can"];
585
587
  return ids.flatMap(
586
- (id) => Object.keys(_chunkQ33V5PEFcjs.BRUSH_HOST_PROFILES).map((host) => {
587
- const brush = _chunkQ33V5PEFcjs.BRUSH_PRESETS.find((b) => b.id === id);
588
- return { brush: id, host, sections: brushEditorSections(brush, _chunkQ33V5PEFcjs.BRUSH_HOST_PROFILES[host]) };
588
+ (id) => Object.keys(_chunkHRUK5TFWcjs.BRUSH_HOST_PROFILES).map((host) => {
589
+ const brush = _chunkHRUK5TFWcjs.BRUSH_PRESETS.find((b) => b.id === id);
590
+ return { brush: id, host, sections: brushEditorSections(brush, _chunkHRUK5TFWcjs.BRUSH_HOST_PROFILES[host]) };
589
591
  })
590
592
  );
591
593
  }
592
594
  function toolDefinitions() {
593
- const library = _chunkQ33V5PEFcjs.createBrushLibrary.call(void 0, );
594
- const toolsFor = (host) => _chunk5HR5CSLCcjs.createBrushLibraryTools.call(void 0, library, { profile: _chunkQ33V5PEFcjs.BRUSH_HOST_PROFILES[host], renderPreview: () => ({ data: "", mimeType: "image/png" }) });
595
+ const library = _chunkHRUK5TFWcjs.createBrushLibrary.call(void 0, );
596
+ const toolsFor = (host) => _chunkJVFGK3DKcjs.createBrushLibraryTools.call(void 0, library, { profile: _chunkHRUK5TFWcjs.BRUSH_HOST_PROFILES[host], renderPreview: () => ({ data: "", mimeType: "image/png" }) });
595
597
  return {
596
598
  tools: toolsFor("paint").map((t) => ({ name: t.name, title: t.title, description: t.description, inputSchema: t.inputSchema, annotations: t.annotations })),
597
- hosts: Object.fromEntries(Object.keys(_chunkQ33V5PEFcjs.BRUSH_HOST_PROFILES).map((host) => [host, toolsFor(host).map((t) => t.name)]))
599
+ hosts: Object.fromEntries(Object.keys(_chunkHRUK5TFWcjs.BRUSH_HOST_PROFILES).map((host) => [host, toolsFor(host).map((t) => t.name)]))
598
600
  };
599
601
  }
600
602
  function brushContracts() {
601
- const presets = _chunkQ33V5PEFcjs.BRUSH_PRESETS.map((b) => JSON.parse(JSON.stringify(b)));
602
- const resolved = _chunkQ33V5PEFcjs.BRUSH_PRESETS.map((b) => ({ id: b.id, engine: _chunkQ33V5PEFcjs.resolveEngine.call(void 0, b), category: _chunkQ33V5PEFcjs.resolveCategory.call(void 0, b), brush: _chunkQ33V5PEFcjs.brushToJSON.call(void 0, _chunkQ33V5PEFcjs.resolveBrush.call(void 0, b)) }));
603
- const profiles = Object.values(_chunkQ33V5PEFcjs.BRUSH_HOST_PROFILES);
604
- const checks = _chunkQ33V5PEFcjs.BRUSH_PRESETS.map((b) => ({
603
+ const presets = _chunkHRUK5TFWcjs.BRUSH_PRESETS.map((b) => JSON.parse(JSON.stringify(b)));
604
+ const resolved = _chunkHRUK5TFWcjs.BRUSH_PRESETS.map((b) => ({ id: b.id, engine: _chunkHRUK5TFWcjs.resolveEngine.call(void 0, b), category: _chunkHRUK5TFWcjs.resolveCategory.call(void 0, b), brush: _chunkHRUK5TFWcjs.brushToJSON.call(void 0, _chunkHRUK5TFWcjs.resolveBrush.call(void 0, b)) }));
605
+ const profiles = Object.values(_chunkHRUK5TFWcjs.BRUSH_HOST_PROFILES);
606
+ const checks = _chunkHRUK5TFWcjs.BRUSH_PRESETS.map((b) => ({
605
607
  id: b.id,
606
608
  hosts: Object.fromEntries(profiles.map((p) => {
607
- const c = _chunkQ33V5PEFcjs.checkBrush.call(void 0, b, p);
609
+ const c = _chunkHRUK5TFWcjs.checkBrush.call(void 0, b, p);
608
610
  return [p.id, { ok: c.ok, issues: c.issues.map(({ facet, severity }) => ({ facet, severity })) }];
609
611
  }))
610
612
  }));
611
- const migrations = migrationInputs().map((input) => ({ input, output: _chunkQ33V5PEFcjs.migrateBrushV1.call(void 0, input), resolved: _chunkQ33V5PEFcjs.brushToJSON.call(void 0, _chunkQ33V5PEFcjs.resolveBrush.call(void 0, _chunkQ33V5PEFcjs.migrateBrushV1.call(void 0, input))), engine: _chunkQ33V5PEFcjs.normalizePaintBrush.call(void 0, input).engine }));
613
+ const migrations = migrationInputs().map((input) => ({ input, output: _chunkHRUK5TFWcjs.migrateBrushV1.call(void 0, input), resolved: _chunkHRUK5TFWcjs.brushToJSON.call(void 0, _chunkHRUK5TFWcjs.resolveBrush.call(void 0, _chunkHRUK5TFWcjs.migrateBrushV1.call(void 0, input))), engine: _chunkHRUK5TFWcjs.normalizePaintBrush.call(void 0, input).engine }));
612
614
  const files = {
613
- "brush.schema.json": { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "brush.schema.json", ..._chunkQ33V5PEFcjs.BRUSH_JSON_SCHEMA },
615
+ "brush.schema.json": { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "brush.schema.json", ..._chunkHRUK5TFWcjs.BRUSH_JSON_SCHEMA },
614
616
  "profile.schema.json": { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "profile.schema.json", ...BRUSH_PROFILE_JSON_SCHEMA },
615
617
  "library-file.schema.json": { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "library-file.schema.json", ...BRUSH_LIBRARY_FILE_JSON_SCHEMA },
616
618
  "presets.json": presets,
@@ -624,7 +626,7 @@ function brushContracts() {
624
626
  files["manifest.json"] = {
625
627
  domain: "brushes",
626
628
  formatVersion: BRUSH_CONTRACT_FORMAT_VERSION,
627
- limits: { maxPalette: _chunkQ33V5PEFcjs.MAX_BRUSH_PALETTE },
629
+ limits: { maxPalette: _chunkHRUK5TFWcjs.MAX_BRUSH_PALETTE },
628
630
  counts: { presets: presets.length, profiles: profiles.length, migrations: migrations.length, editorSections: files["editor-sections.json"].length },
629
631
  files: Object.keys(files).sort()
630
632
  };
@@ -725,4 +727,4 @@ function brushContracts() {
725
727
 
726
728
 
727
729
 
728
- exports.AbrError = _chunk5HR5CSLCcjs.AbrError; exports.BRUSH_CONTRACT_FORMAT_VERSION = BRUSH_CONTRACT_FORMAT_VERSION; exports.BRUSH_FACET_SCHEMAS = _chunkQ33V5PEFcjs.BRUSH_FACET_SCHEMAS; exports.BRUSH_FLIPS = _chunkQ33V5PEFcjs.BRUSH_FLIPS; exports.BRUSH_GRAIN_MODES = _chunkQ33V5PEFcjs.BRUSH_GRAIN_MODES; exports.BRUSH_GRAIN_SOURCE_PATTERN = _chunkQ33V5PEFcjs.BRUSH_GRAIN_SOURCE_PATTERN; exports.BRUSH_HOST_PROFILES = _chunkQ33V5PEFcjs.BRUSH_HOST_PROFILES; exports.BRUSH_JSON_SCHEMA = _chunkQ33V5PEFcjs.BRUSH_JSON_SCHEMA; exports.BRUSH_KINDS = _chunkQ33V5PEFcjs.BRUSH_KINDS; exports.BRUSH_LIBRARY_FILE_JSON_SCHEMA = BRUSH_LIBRARY_FILE_JSON_SCHEMA; exports.BRUSH_MARKS_KINDS = _chunkQ33V5PEFcjs.BRUSH_MARKS_KINDS; exports.BRUSH_MEDIA = _chunkQ33V5PEFcjs.BRUSH_MEDIA; exports.BRUSH_PACK_FORMATS = _chunk5HR5CSLCcjs.BRUSH_PACK_FORMATS; exports.BRUSH_PATCH_PROPERTIES = _chunk5HR5CSLCcjs.BRUSH_PATCH_PROPERTIES; exports.BRUSH_PRESETS = _chunkQ33V5PEFcjs.BRUSH_PRESETS; exports.BRUSH_PREVIEW_DARK_PAPER = _chunkQNNZA3V5cjs.BRUSH_PREVIEW_DARK_PAPER; exports.BRUSH_PREVIEW_FALLBACKS = _chunkQNNZA3V5cjs.BRUSH_PREVIEW_FALLBACKS; exports.BRUSH_PREVIEW_MIN_CONTRAST = _chunkQNNZA3V5cjs.BRUSH_PREVIEW_MIN_CONTRAST; exports.BRUSH_PROFILE_JSON_SCHEMA = BRUSH_PROFILE_JSON_SCHEMA; exports.BRUSH_SOURCE_CHANNELS = _chunkQ33V5PEFcjs.BRUSH_SOURCE_CHANNELS; exports.BRUSH_SOURCE_PROVIDER = _chunkQ33V5PEFcjs.BRUSH_SOURCE_PROVIDER; exports.BRUSH_SURFACES = _chunkQ33V5PEFcjs.BRUSH_SURFACES; exports.BRUSH_TIP_SHAPES = _chunkQ33V5PEFcjs.BRUSH_TIP_SHAPES; exports.BRUSH_TOOLS = _chunkQ33V5PEFcjs.BRUSH_TOOLS; exports.BRUSH_UNSET_PROPERTY = _chunk5HR5CSLCcjs.BRUSH_UNSET_PROPERTY; exports.BRUSH_WORD = _chunkQ33V5PEFcjs.BRUSH_WORD; exports.BrushLibraryError = _chunkQ33V5PEFcjs.BrushLibraryError; exports.BrushPackError = _chunk5HR5CSLCcjs.BrushPackError; exports.BrushSourceError = _chunkQ33V5PEFcjs.BrushSourceError; exports.CANVAS_PROFILE = _chunkQ33V5PEFcjs.CANVAS_PROFILE; exports.GHOST_TRIGGERS = _chunkQ33V5PEFcjs.GHOST_TRIGGERS2; exports.IMAGE_PROFILE = _chunkQ33V5PEFcjs.IMAGE_PROFILE; exports.MAX_BRUSH_PACK_BYTES = _chunk5HR5CSLCcjs.MAX_BRUSH_PACK_BYTES; exports.MAX_BRUSH_PALETTE = _chunkQ33V5PEFcjs.MAX_BRUSH_PALETTE; exports.MAX_BRUSH_SOURCES = _chunkQ33V5PEFcjs.MAX_BRUSH_SOURCES; exports.MAX_BRUSH_SOURCE_INPUT_PIXELS = _chunkQ33V5PEFcjs.MAX_BRUSH_SOURCE_INPUT_PIXELS; exports.MAX_BRUSH_SOURCE_SIZE = _chunkQ33V5PEFcjs.MAX_BRUSH_SOURCE_SIZE; exports.MAX_PACK_BRUSHES = _chunk5HR5CSLCcjs.MAX_PACK_BRUSHES; exports.PAINT_PROFILE = _chunkQ33V5PEFcjs.PAINT_PROFILE; exports.TIP_GRAIN_PATTERNS = _chunkQ33V5PEFcjs.TIP_GRAIN_PATTERNS; exports.UNSETTABLE_BRUSH_FACETS = _chunk5HR5CSLCcjs.UNSETTABLE_BRUSH_FACETS; exports.VECTOR_PROFILE = _chunkQ33V5PEFcjs.VECTOR_PROFILE; exports.WET_BRUSH_TOOLS = _chunkQ33V5PEFcjs.WET_BRUSH_TOOLS; exports.addBrushSourceFromFile = addBrushSourceFromFile; exports.brushColourHex = _chunkJBZRVLXVcjs.brushColourHex; exports.brushColourKind = _chunkQ33V5PEFcjs.brushColourKind; exports.brushContracts = brushContracts; exports.brushEditorScope = brushEditorScope; exports.brushEditorSections = brushEditorSections; exports.brushIdFromName = brushIdFromName; exports.brushLibraryGroups = brushLibraryGroups; exports.brushPackFormat = _chunk5HR5CSLCcjs.brushPackFormat; exports.brushPreviewColours = _chunkQNNZA3V5cjs.brushPreviewColours; exports.brushPreviewStrength = _chunkQNNZA3V5cjs.brushPreviewStrength; exports.brushPreviewsOnDark = _chunkQNNZA3V5cjs.brushPreviewsOnDark; exports.brushSourceFromMask = _chunkQ33V5PEFcjs.brushSourceFromMask; exports.brushSourceIdFromName = _chunkQ33V5PEFcjs.brushSourceIdFromName; exports.brushSourceIdOf = _chunkQ33V5PEFcjs.brushSourceIdOf; exports.brushSourceIds = _chunkQ33V5PEFcjs.brushSourceIds; exports.brushSourceRef = _chunkQ33V5PEFcjs.brushSourceRef; exports.changedBrushPaths = _chunk5HR5CSLCcjs.changedBrushPaths; exports.checkBrush = _chunkQ33V5PEFcjs.checkBrush; exports.checkBrushEdit = _chunkQ33V5PEFcjs.checkBrushEdit; exports.createBrushLibrary = _chunkQ33V5PEFcjs.createBrushLibrary; exports.createBrushLibraryToolProvider = _chunk5HR5CSLCcjs.createBrushLibraryToolProvider; exports.createBrushLibraryTools = _chunk5HR5CSLCcjs.createBrushLibraryTools; exports.decodeBrushSource = _chunkQ33V5PEFcjs.decodeBrushSource; exports.degrade = _chunkQ33V5PEFcjs.degrade; exports.imageFileToPngDataUrl = imageFileToPngDataUrl; exports.importBrushFile = _chunk5HR5CSLCcjs.importBrushFile; exports.importBrushPack = _chunk5HR5CSLCcjs.importBrushPack; exports.isBrushLibrary = _chunkQ33V5PEFcjs.isBrushLibrary; exports.isBrushSourceRef = _chunkQ33V5PEFcjs.isBrushSourceRef; exports.listPresets = _chunkQ33V5PEFcjs.listPresets; exports.makeBrushSource = _chunkQ33V5PEFcjs.makeBrushSource; exports.mergeBrushPatch = _chunk5HR5CSLCcjs.mergeBrushPatch; exports.migrateBrushV1 = _chunkQ33V5PEFcjs.migrateBrushV1; exports.newBrushFor = newBrushFor; exports.normalizeBrushSpec = _chunkQ33V5PEFcjs.normalizeBrushSpec; exports.parseBrushLibrary = _chunkQ33V5PEFcjs.parseBrushLibrary2; exports.parseBrushLibrarySources = _chunkQ33V5PEFcjs.parseBrushLibrarySources; exports.readAbr = _chunk5HR5CSLCcjs.readAbr; exports.readBrushPack = _chunk5HR5CSLCcjs.readBrushPack; exports.readBrushSource = _chunkQ33V5PEFcjs.readBrushSource; exports.resolveBrush = _chunkQ33V5PEFcjs.resolveBrush; exports.resolveCategory = _chunkQ33V5PEFcjs.resolveCategory; exports.resolveEngine = _chunkQ33V5PEFcjs.resolveEngine; exports.resolveWetMedium = _chunkQ33V5PEFcjs.resolveWetMedium; exports.saveBrushEdit = saveBrushEdit; exports.setBrushEditorValue = setBrushEditorValue; exports.snapshotBrushLibrary = _chunkQ33V5PEFcjs.snapshotBrushLibrary; exports.summarizeBrushSource = _chunkQ33V5PEFcjs.summarizeBrushSource; exports.withBrushSource = withBrushSource; exports.withKeptIssues = _chunkQ33V5PEFcjs.withKeptIssues;
730
+ exports.AbrError = _chunkJVFGK3DKcjs.AbrError; exports.BRUSH_CONTRACT_FORMAT_VERSION = BRUSH_CONTRACT_FORMAT_VERSION; exports.BRUSH_FACET_SCHEMAS = _chunkHRUK5TFWcjs.BRUSH_FACET_SCHEMAS; exports.BRUSH_FLIPS = _chunkHRUK5TFWcjs.BRUSH_FLIPS; exports.BRUSH_GRAIN_MODES = _chunkHRUK5TFWcjs.BRUSH_GRAIN_MODES; exports.BRUSH_GRAIN_SOURCE_PATTERN = _chunkHRUK5TFWcjs.BRUSH_GRAIN_SOURCE_PATTERN; exports.BRUSH_HOST_PROFILES = _chunkHRUK5TFWcjs.BRUSH_HOST_PROFILES; exports.BRUSH_JSON_SCHEMA = _chunkHRUK5TFWcjs.BRUSH_JSON_SCHEMA; exports.BRUSH_KINDS = _chunkHRUK5TFWcjs.BRUSH_KINDS; exports.BRUSH_LIBRARY_FILE_JSON_SCHEMA = BRUSH_LIBRARY_FILE_JSON_SCHEMA; exports.BRUSH_MARKS_KINDS = _chunkHRUK5TFWcjs.BRUSH_MARKS_KINDS; exports.BRUSH_MEDIA = _chunkHRUK5TFWcjs.BRUSH_MEDIA; exports.BRUSH_PACK_FORMATS = _chunkJVFGK3DKcjs.BRUSH_PACK_FORMATS; exports.BRUSH_PATCH_PROPERTIES = _chunkJVFGK3DKcjs.BRUSH_PATCH_PROPERTIES; exports.BRUSH_PRESETS = _chunkHRUK5TFWcjs.BRUSH_PRESETS; exports.BRUSH_PREVIEW_DARK_PAPER = _chunkH3EME3S5cjs.BRUSH_PREVIEW_DARK_PAPER; exports.BRUSH_PREVIEW_FALLBACKS = _chunkH3EME3S5cjs.BRUSH_PREVIEW_FALLBACKS; exports.BRUSH_PREVIEW_MIN_CONTRAST = _chunkH3EME3S5cjs.BRUSH_PREVIEW_MIN_CONTRAST; exports.BRUSH_PROFILE_JSON_SCHEMA = BRUSH_PROFILE_JSON_SCHEMA; exports.BRUSH_SOURCE_CHANNELS = _chunkHRUK5TFWcjs.BRUSH_SOURCE_CHANNELS; exports.BRUSH_SOURCE_PROVIDER = _chunkHRUK5TFWcjs.BRUSH_SOURCE_PROVIDER; exports.BRUSH_SURFACES = _chunkHRUK5TFWcjs.BRUSH_SURFACES; exports.BRUSH_TIP_SHAPES = _chunkHRUK5TFWcjs.BRUSH_TIP_SHAPES; exports.BRUSH_TOOLS = _chunkHRUK5TFWcjs.BRUSH_TOOLS; exports.BRUSH_UNSET_PROPERTY = _chunkJVFGK3DKcjs.BRUSH_UNSET_PROPERTY; exports.BRUSH_WORD = _chunkHRUK5TFWcjs.BRUSH_WORD; exports.BrushLibraryError = _chunkHRUK5TFWcjs.BrushLibraryError; exports.BrushPackError = _chunkJVFGK3DKcjs.BrushPackError; exports.BrushSourceError = _chunkHRUK5TFWcjs.BrushSourceError; exports.CANVAS_PROFILE = _chunkHRUK5TFWcjs.CANVAS_PROFILE; exports.GHOST_TRIGGERS = _chunkHRUK5TFWcjs.GHOST_TRIGGERS2; exports.IMAGE_PROFILE = _chunkHRUK5TFWcjs.IMAGE_PROFILE; exports.MAX_BRUSH_PACK_BYTES = _chunkJVFGK3DKcjs.MAX_BRUSH_PACK_BYTES; exports.MAX_BRUSH_PALETTE = _chunkHRUK5TFWcjs.MAX_BRUSH_PALETTE; exports.MAX_BRUSH_SOURCES = _chunkHRUK5TFWcjs.MAX_BRUSH_SOURCES; exports.MAX_BRUSH_SOURCE_INPUT_PIXELS = _chunkHRUK5TFWcjs.MAX_BRUSH_SOURCE_INPUT_PIXELS; exports.MAX_BRUSH_SOURCE_SIZE = _chunkHRUK5TFWcjs.MAX_BRUSH_SOURCE_SIZE; exports.MAX_PACK_BRUSHES = _chunkJVFGK3DKcjs.MAX_PACK_BRUSHES; exports.PAINT_PROFILE = _chunkHRUK5TFWcjs.PAINT_PROFILE; exports.TIP_GRAIN_PATTERNS = _chunkHRUK5TFWcjs.TIP_GRAIN_PATTERNS; exports.UNSETTABLE_BRUSH_FACETS = _chunkJVFGK3DKcjs.UNSETTABLE_BRUSH_FACETS; exports.VECTOR_PROFILE = _chunkHRUK5TFWcjs.VECTOR_PROFILE; exports.WET_BRUSH_TOOLS = _chunkHRUK5TFWcjs.WET_BRUSH_TOOLS; exports.addBrushSourceFromFile = addBrushSourceFromFile; exports.brushColourHex = _chunkJBZRVLXVcjs.brushColourHex; exports.brushColourKind = _chunkHRUK5TFWcjs.brushColourKind; exports.brushContracts = brushContracts; exports.brushEditorScope = brushEditorScope; exports.brushEditorSections = brushEditorSections; exports.brushIdFromName = brushIdFromName; exports.brushLibraryGroups = brushLibraryGroups; exports.brushPackFormat = _chunkJVFGK3DKcjs.brushPackFormat; exports.brushPreviewColours = _chunkH3EME3S5cjs.brushPreviewColours; exports.brushPreviewStrength = _chunkH3EME3S5cjs.brushPreviewStrength; exports.brushPreviewsOnDark = _chunkH3EME3S5cjs.brushPreviewsOnDark; exports.brushSourceFromMask = _chunkHRUK5TFWcjs.brushSourceFromMask; exports.brushSourceIdFromName = _chunkHRUK5TFWcjs.brushSourceIdFromName; exports.brushSourceIdOf = _chunkHRUK5TFWcjs.brushSourceIdOf; exports.brushSourceIds = _chunkHRUK5TFWcjs.brushSourceIds; exports.brushSourceRef = _chunkHRUK5TFWcjs.brushSourceRef; exports.changedBrushPaths = _chunkJVFGK3DKcjs.changedBrushPaths; exports.checkBrush = _chunkHRUK5TFWcjs.checkBrush; exports.checkBrushEdit = _chunkHRUK5TFWcjs.checkBrushEdit; exports.createBrushLibrary = _chunkHRUK5TFWcjs.createBrushLibrary; exports.createBrushLibraryToolProvider = _chunkJVFGK3DKcjs.createBrushLibraryToolProvider; exports.createBrushLibraryTools = _chunkJVFGK3DKcjs.createBrushLibraryTools; exports.decodeBrushSource = _chunkHRUK5TFWcjs.decodeBrushSource; exports.degrade = _chunkHRUK5TFWcjs.degrade; exports.imageFileToPngDataUrl = imageFileToPngDataUrl; exports.importBrushFile = _chunkJVFGK3DKcjs.importBrushFile; exports.importBrushPack = _chunkJVFGK3DKcjs.importBrushPack; exports.isBrushLibrary = _chunkHRUK5TFWcjs.isBrushLibrary; exports.isBrushSourceRef = _chunkHRUK5TFWcjs.isBrushSourceRef; exports.listPresets = _chunkHRUK5TFWcjs.listPresets; exports.makeBrushSource = _chunkHRUK5TFWcjs.makeBrushSource; exports.mergeBrushPatch = _chunkJVFGK3DKcjs.mergeBrushPatch; exports.migrateBrushV1 = _chunkHRUK5TFWcjs.migrateBrushV1; exports.newBrushFor = newBrushFor; exports.normalizeBrushSpec = _chunkHRUK5TFWcjs.normalizeBrushSpec; exports.parseBrushLibrary = _chunkHRUK5TFWcjs.parseBrushLibrary2; exports.parseBrushLibrarySources = _chunkHRUK5TFWcjs.parseBrushLibrarySources; exports.readAbr = _chunkJVFGK3DKcjs.readAbr; exports.readBrushPack = _chunkJVFGK3DKcjs.readBrushPack; exports.readBrushSource = _chunkHRUK5TFWcjs.readBrushSource; exports.resolveBrush = _chunkHRUK5TFWcjs.resolveBrush; exports.resolveCategory = _chunkHRUK5TFWcjs.resolveCategory; exports.resolveEngine = _chunkHRUK5TFWcjs.resolveEngine; exports.resolveWetMedium = _chunkHRUK5TFWcjs.resolveWetMedium; exports.saveBrushEdit = saveBrushEdit; exports.setBrushEditorValue = setBrushEditorValue; exports.snapshotBrushLibrary = _chunkHRUK5TFWcjs.snapshotBrushLibrary; exports.summarizeBrushSource = _chunkHRUK5TFWcjs.summarizeBrushSource; exports.withBrushSource = withBrushSource; exports.withKeptIssues = _chunkHRUK5TFWcjs.withKeptIssues;
@@ -18,7 +18,7 @@ import {
18
18
  mergeBrushPatch,
19
19
  readAbr,
20
20
  readBrushPack
21
- } from "../chunk-IJ6NSWKM.js";
21
+ } from "../chunk-NJLPW4MM.js";
22
22
  import "../chunk-VE6UKYQ2.js";
23
23
  import {
24
24
  BRUSH_PREVIEW_DARK_PAPER,
@@ -27,7 +27,7 @@ import {
27
27
  brushPreviewColours,
28
28
  brushPreviewStrength,
29
29
  brushPreviewsOnDark
30
- } from "../chunk-GHYJAMMS.js";
30
+ } from "../chunk-ROURHJYT.js";
31
31
  import {
32
32
  BRUSH_FACET_SCHEMAS,
33
33
  BRUSH_FIELDS,
@@ -102,7 +102,7 @@ import {
102
102
  snapshotBrushLibrary,
103
103
  summarizeBrushSource,
104
104
  withKeptIssues
105
- } from "../chunk-XVPQNX7O.js";
105
+ } from "../chunk-PPDUCZZV.js";
106
106
  import {
107
107
  STAMP_KINDS
108
108
  } from "../chunk-JL6YH5WD.js";
@@ -421,6 +421,7 @@ function setBrushEditorValue(brush, path, value) {
421
421
  delete next.iridescence;
422
422
  delete next.ghost;
423
423
  delete next.mutation;
424
+ delete next.ferro;
424
425
  delete next.colour;
425
426
  next.tool = next.tool ?? "eraser";
426
427
  if (next.tip.shape === "stamp") next.tip = { shape: "round", ...next.tip.size !== void 0 ? { size: next.tip.size } : {} };
@@ -524,7 +525,8 @@ var BRUSH_PROFILE_JSON_SCHEMA = {
524
525
  tipGrain: { ...SUPPORT, description: "Texture inside the tip (tip.grain); missing: none." },
525
526
  dabCount: { ...SUPPORT, description: "Several dabs per spacing step (dynamics.count); missing: none." },
526
527
  livePatterns: { ...SUPPORT, description: "Gray-Scott reaction-diffusion patterns in wet paint (livePatterns); missing: none." },
527
- iridescence: { ...SUPPORT, description: "Thin-film iridescence (iridescence); missing: none." }
528
+ iridescence: { ...SUPPORT, description: "Thin-film iridescence (iridescence); missing: none." },
529
+ ferro: { ...SUPPORT, description: "Liquid ferrofluid (ferro); missing: none." }
528
530
  },
529
531
  required: ["id", "name", "kinds", "tools", "tipShapes", "marks", "surfaces", "exchange", "reservoir", "grain", "light", "motion", "colours", "velocity", "tilt"],
530
532
  additionalProperties: false
@@ -5,15 +5,15 @@
5
5
  * means the same everywhere.
6
6
  */
7
7
  import type { JsonSchema } from '../tools/schema.cjs';
8
- import type { Brush } from './types.cjs';
9
- /** A brush's facets, any of them, as a patch takes them (the tip's `shape` may be left out). */
8
+ import type { Brush, BrushFerro } from './types.cjs';
9
+ /** A brush's facets, any of them, as a patch takes them (the tip's `shape` may be left out, and `ferro` accepts a `0..1` amount shorthand). */
10
10
  export type BrushPatch = {
11
- [K in Exclude<keyof Brush, 'id'>]?: K extends 'tip' ? Partial<Brush['tip']> : Brush[K];
11
+ [K in Exclude<keyof Brush, 'id'>]?: K extends 'tip' ? Partial<Brush['tip']> : K extends 'ferro' ? number | BrushFerro : Brush[K];
12
12
  };
13
13
  /** The schema of every facet except `id` and `name`, as tool arguments (a patch may leave out `tip.shape`). */
14
14
  export declare const BRUSH_PATCH_PROPERTIES: Readonly<Record<string, JsonSchema>>;
15
15
  /** The facets a patch can remove (`unset`), back to the engine's defaults. */
16
- export declare const UNSETTABLE_BRUSH_FACETS: readonly ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "motion", "livePatterns", "colour"];
16
+ export declare const UNSETTABLE_BRUSH_FACETS: readonly ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "ferro", "motion", "livePatterns", "colour"];
17
17
  export type UnsettableBrushFacet = (typeof UNSETTABLE_BRUSH_FACETS)[number];
18
18
  /** The `unset` argument of tools that take a patch. */
19
19
  export declare const BRUSH_UNSET_PROPERTY: JsonSchema;
@@ -5,15 +5,15 @@
5
5
  * means the same everywhere.
6
6
  */
7
7
  import type { JsonSchema } from '../tools/schema.js';
8
- import type { Brush } from './types.js';
9
- /** A brush's facets, any of them, as a patch takes them (the tip's `shape` may be left out). */
8
+ import type { Brush, BrushFerro } from './types.js';
9
+ /** A brush's facets, any of them, as a patch takes them (the tip's `shape` may be left out, and `ferro` accepts a `0..1` amount shorthand). */
10
10
  export type BrushPatch = {
11
- [K in Exclude<keyof Brush, 'id'>]?: K extends 'tip' ? Partial<Brush['tip']> : Brush[K];
11
+ [K in Exclude<keyof Brush, 'id'>]?: K extends 'tip' ? Partial<Brush['tip']> : K extends 'ferro' ? number | BrushFerro : Brush[K];
12
12
  };
13
13
  /** The schema of every facet except `id` and `name`, as tool arguments (a patch may leave out `tip.shape`). */
14
14
  export declare const BRUSH_PATCH_PROPERTIES: Readonly<Record<string, JsonSchema>>;
15
15
  /** The facets a patch can remove (`unset`), back to the engine's defaults. */
16
- export declare const UNSETTABLE_BRUSH_FACETS: readonly ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "motion", "livePatterns", "colour"];
16
+ export declare const UNSETTABLE_BRUSH_FACETS: readonly ["description", "tags", "medium", "variety", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "ferro", "motion", "livePatterns", "colour"];
17
17
  export type UnsettableBrushFacet = (typeof UNSETTABLE_BRUSH_FACETS)[number];
18
18
  /** The `unset` argument of tools that take a patch. */
19
19
  export declare const BRUSH_UNSET_PROPERTY: JsonSchema;
@@ -43,6 +43,8 @@ export interface BrushHostProfile {
43
43
  livePatterns?: BrushSupport;
44
44
  /** Structural colour (`iridescence`); missing: `none`. */
45
45
  iridescence?: BrushSupport;
46
+ /** Liquid ferrofluid (`ferro`); missing: `none`. */
47
+ ferro?: BrushSupport;
46
48
  }
47
49
  export interface BrushIssue {
48
50
  /** The facet path the issue is about, e.g. `tip.shape`, `light`, `behaviour.exchange.reservoir`. */
@@ -43,6 +43,8 @@ export interface BrushHostProfile {
43
43
  livePatterns?: BrushSupport;
44
44
  /** Structural colour (`iridescence`); missing: `none`. */
45
45
  iridescence?: BrushSupport;
46
+ /** Liquid ferrofluid (`ferro`); missing: `none`. */
47
+ ferro?: BrushSupport;
46
48
  }
47
49
  export interface BrushIssue {
48
50
  /** The facet path the issue is about, e.g. `tip.shape`, `light`, `behaviour.exchange.reservoir`. */
@@ -33,6 +33,7 @@ export declare const BRUSH_FACET_SCHEMAS: {
33
33
  readonly iridescence: JsonSchema;
34
34
  readonly ghost: JsonSchema;
35
35
  readonly mutation: JsonSchema;
36
+ readonly ferro: JsonSchema;
36
37
  readonly motion: JsonSchema;
37
38
  readonly livePatterns: JsonSchema;
38
39
  readonly colour: JsonSchema;
@@ -33,6 +33,7 @@ export declare const BRUSH_FACET_SCHEMAS: {
33
33
  readonly iridescence: JsonSchema;
34
34
  readonly ghost: JsonSchema;
35
35
  readonly mutation: JsonSchema;
36
+ readonly ferro: JsonSchema;
36
37
  readonly motion: JsonSchema;
37
38
  readonly livePatterns: JsonSchema;
38
39
  readonly colour: JsonSchema;
@@ -360,6 +360,15 @@ export interface BrushMutation {
360
360
  /** Seconds the paint takes to sink away once claimed, 0.5..10 (default 2). */
361
361
  fade?: number;
362
362
  }
363
+ /** Liquid ferrofluid paint that rises into glossy Rosensweig spikes near magnets until cured. */
364
+ export interface BrushFerro {
365
+ /** Ferrofluid per dab, 0..1 (default 0: none). */
366
+ amount?: number;
367
+ /** Capillary wavelength (spike-to-spike spacing) in pixels, 4..64 (default 16). */
368
+ wavelength?: number;
369
+ /** Critical magnetic field |B| threshold where spikes begin to rise, 0.01..0.95 (default 0.12). */
370
+ threshold?: number;
371
+ }
363
372
  /** Animation after painting. */
364
373
  export interface BrushMotion {
365
374
  /** Effect paint keeps playing on stroke layers. */
@@ -431,6 +440,7 @@ export interface Brush {
431
440
  iridescence?: BrushIridescence;
432
441
  ghost?: BrushGhost;
433
442
  mutation?: BrushMutation;
443
+ ferro?: BrushFerro;
434
444
  motion?: BrushMotion;
435
445
  livePatterns?: BrushLivePatterns;
436
446
  colour?: BrushColour;
@@ -360,6 +360,15 @@ export interface BrushMutation {
360
360
  /** Seconds the paint takes to sink away once claimed, 0.5..10 (default 2). */
361
361
  fade?: number;
362
362
  }
363
+ /** Liquid ferrofluid paint that rises into glossy Rosensweig spikes near magnets until cured. */
364
+ export interface BrushFerro {
365
+ /** Ferrofluid per dab, 0..1 (default 0: none). */
366
+ amount?: number;
367
+ /** Capillary wavelength (spike-to-spike spacing) in pixels, 4..64 (default 16). */
368
+ wavelength?: number;
369
+ /** Critical magnetic field |B| threshold where spikes begin to rise, 0.01..0.95 (default 0.12). */
370
+ threshold?: number;
371
+ }
363
372
  /** Animation after painting. */
364
373
  export interface BrushMotion {
365
374
  /** Effect paint keeps playing on stroke layers. */
@@ -431,6 +440,7 @@ export interface Brush {
431
440
  iridescence?: BrushIridescence;
432
441
  ghost?: BrushGhost;
433
442
  mutation?: BrushMutation;
443
+ ferro?: BrushFerro;
434
444
  motion?: BrushMotion;
435
445
  livePatterns?: BrushLivePatterns;
436
446
  colour?: BrushColour;