@pixerate/editor 0.275.1 → 0.277.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/animation-editor/index.cjs +62 -62
  2. package/dist/animation-editor/index.js +6 -6
  3. package/dist/brushes/index.cjs +84 -71
  4. package/dist/brushes/index.js +20 -7
  5. package/dist/brushes/patch.d.cts +4 -4
  6. package/dist/brushes/patch.d.ts +4 -4
  7. package/dist/brushes/profiles.d.cts +4 -0
  8. package/dist/brushes/profiles.d.ts +4 -0
  9. package/dist/brushes/schema.d.cts +2 -0
  10. package/dist/brushes/schema.d.ts +2 -0
  11. package/dist/brushes/types.d.cts +32 -1
  12. package/dist/brushes/types.d.ts +32 -1
  13. package/dist/{brushstrokeRender-2RQL46WB.cjs → brushstrokeRender-7HOMKZNO.cjs} +21 -21
  14. package/dist/{brushstrokeRender-WGY4DZHF.js → brushstrokeRender-EIK3625T.js} +5 -5
  15. package/dist/canvas/index.cjs +13 -13
  16. package/dist/canvas/index.js +1 -1
  17. package/dist/{chunk-T6KNJVBP.cjs → chunk-4NZTK7LM.cjs} +35 -35
  18. package/dist/{chunk-LMG57L5A.js → chunk-CTHWXW5M.js} +121 -8
  19. package/dist/{chunk-43XJC3PP.js → chunk-DWPFGWW5.js} +1 -1
  20. package/dist/{chunk-MSWLJI4G.js → chunk-FNIWHSLI.js} +2 -2
  21. package/dist/{chunk-JDXWLCXF.cjs → chunk-H3EME3S5.cjs} +2 -2
  22. package/dist/{chunk-Y2ISSVPH.js → chunk-HHX7ST6Z.js} +167 -9
  23. package/dist/{chunk-6QNDGMK7.cjs → chunk-HRUK5TFW.cjs} +187 -45
  24. package/dist/{chunk-NIBFKXMD.cjs → chunk-IFWLZZCW.cjs} +619 -2
  25. package/dist/{chunk-W6JDUXP3.js → chunk-J23JOHPR.js} +1 -1
  26. package/dist/{chunk-HVO6XCI4.js → chunk-JPB65KQ6.js} +1 -1
  27. package/dist/{chunk-Y354ETAE.cjs → chunk-JVFGK3DK.cjs} +69 -58
  28. package/dist/{chunk-ACTHSQOI.js → chunk-LDZOXESA.js} +618 -1
  29. package/dist/{chunk-IEEY5RJV.js → chunk-LZE2QZCT.js} +1 -1
  30. package/dist/{chunk-DHC4E2DU.cjs → chunk-MITQGHN3.cjs} +121 -8
  31. package/dist/{chunk-75SYKUOT.js → chunk-N4MCWQZ7.js} +1 -1
  32. package/dist/{chunk-G5TCMKX5.js → chunk-NJLPW4MM.js} +17 -6
  33. package/dist/{chunk-STLTOISW.cjs → chunk-NPPMKMCU.cjs} +17 -17
  34. package/dist/{chunk-OLZXXAER.cjs → chunk-PEQWARCP.cjs} +408 -250
  35. package/dist/{chunk-ZM7IKQ4X.js → chunk-PPDUCZZV.js} +152 -10
  36. package/dist/{chunk-RAPNRK4R.cjs → chunk-QBZEC2JJ.cjs} +88 -88
  37. package/dist/{chunk-RJGW3RQS.js → chunk-ROURHJYT.js} +1 -1
  38. package/dist/{chunk-62MFUHSI.cjs → chunk-UQXULAMQ.cjs} +16 -16
  39. package/dist/{chunk-YYZC2EQO.cjs → chunk-VNF7A2JA.cjs} +23 -23
  40. package/dist/{chunk-C6VB7CPY.cjs → chunk-WFSYZO5Z.cjs} +1 -1
  41. package/dist/image-editor/index.cjs +880 -852
  42. package/dist/image-editor/index.js +36 -8
  43. package/dist/paint/index.cjs +33 -5
  44. package/dist/paint/index.d.cts +1 -0
  45. package/dist/paint/index.d.ts +1 -0
  46. package/dist/paint/index.js +32 -4
  47. package/dist/paint/live-patterns.d.cts +149 -0
  48. package/dist/paint/live-patterns.d.ts +149 -0
  49. package/dist/paint/wet/world.d.cts +13 -0
  50. package/dist/paint/wet/world.d.ts +13 -0
  51. package/dist/paint-editor/brush-model.d.cts +21 -1
  52. package/dist/paint-editor/brush-model.d.ts +21 -1
  53. package/dist/paint-editor/controller.d.cts +102 -1
  54. package/dist/paint-editor/controller.d.ts +102 -1
  55. package/dist/paint-editor/document.d.cts +8 -1
  56. package/dist/paint-editor/document.d.ts +8 -1
  57. package/dist/paint-editor/ferro-tools.d.cts +10 -0
  58. package/dist/paint-editor/ferro-tools.d.ts +10 -0
  59. package/dist/paint-editor/ferro.d.cts +134 -0
  60. package/dist/paint-editor/ferro.d.ts +134 -0
  61. package/dist/paint-editor/index.cjs +2291 -904
  62. package/dist/paint-editor/index.d.cts +2 -0
  63. package/dist/paint-editor/index.d.ts +2 -0
  64. package/dist/paint-editor/index.js +1533 -146
  65. package/dist/paint-editor/stroke.d.cts +14 -0
  66. package/dist/paint-editor/stroke.d.ts +14 -0
  67. package/dist/paint-editor/thick.d.cts +15 -2
  68. package/dist/paint-editor/thick.d.ts +15 -2
  69. package/dist/paint-editor/ui.d.cts +7 -1
  70. package/dist/paint-editor/ui.d.ts +7 -1
  71. package/dist/paint-editor/wet-tools.d.cts +42 -0
  72. package/dist/paint-editor/wet-tools.d.ts +42 -0
  73. package/dist/paint-editor/wet.d.cts +22 -1
  74. package/dist/paint-editor/wet.d.ts +22 -1
  75. package/dist/vector/index.cjs +1002 -1002
  76. package/dist/vector/index.js +7 -7
  77. package/package.json +1 -1
  78. package/skills/pixerate-brushes/SKILL.md +1 -0
  79. package/skills/pixerate-paint-editor/SKILL.md +11 -29
  80. package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
  81. package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
@@ -19,7 +19,11 @@ var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
19
19
 
20
20
 
21
21
 
22
- var _chunkNIBFKXMDcjs = require('./chunk-NIBFKXMD.cjs');
22
+
23
+
24
+
25
+
26
+ var _chunkIFWLZZCWcjs = require('./chunk-IFWLZZCW.cjs');
23
27
 
24
28
 
25
29
  var _chunk2VX55NFMcjs = require('./chunk-2VX55NFM.cjs');
@@ -278,6 +282,29 @@ var BRUSH_PRESETS = [
278
282
  behaviour: { surface: "wet", exchange: { pickup: 0.4, mix: 0.4 }, wet: { water: 0.8, pigment: 0.45, rewet: 0.2 } },
279
283
  colour: { default: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 500, repeat: "mirror", unit: "px" } } }
280
284
  },
285
+ // Live patterns: Gray-Scott reaction-diffusion that grows inside the wet wash and freezes as it dries.
286
+ {
287
+ id: "turing-wash",
288
+ name: "Turing wash",
289
+ kind: "mark",
290
+ medium: "paint",
291
+ variety: "watercolor",
292
+ tip: { shape: "round", size: 36, hardness: 0.5, spacing: 0.12 },
293
+ dynamics: { smoothing: 0.25, pressureOpacity: true, minSize: 0.35 },
294
+ behaviour: { surface: "wet", exchange: { pickup: 0.45 }, wet: { water: 0.75, pigment: 0.55, rewet: 0.2 } },
295
+ livePatterns: { pattern: "spots", contrast: 1.2, secondColor: "#0ea5e9", relief: 0.25 }
296
+ },
297
+ {
298
+ id: "coral-ink",
299
+ name: "Coral ink",
300
+ kind: "mark",
301
+ medium: "ink",
302
+ variety: "watercolor",
303
+ tip: { shape: "round", size: 22, hardness: 0.65, spacing: 0.12 },
304
+ dynamics: { smoothing: 0.3, pressureOpacity: true, minSize: 0.3 },
305
+ behaviour: { surface: "wet", exchange: { pickup: 0.35 }, wet: { water: 0.7, pigment: 0.75, rewet: 0.15 } },
306
+ livePatterns: { pattern: "coral", scale: 1.2, contrast: 1.3 }
307
+ },
281
308
  {
282
309
  id: "watercolor-detail",
283
310
  name: "Watercolour rigger",
@@ -905,6 +932,29 @@ var BRUSH_PRESETS = [
905
932
  tip: { shape: "round", size: 6, hardness: 0.9, spacing: 0.06 },
906
933
  dynamics: { smoothing: 0.35, pressureSmoothing: 0.25, minSize: 0.25 },
907
934
  mutation: {}
935
+ },
936
+ // --- Ferrofluid paint: liquid magnetic paint that rises into lit Rosensweig spikes near magnets ---
937
+ {
938
+ id: "ferrofluid",
939
+ name: "Ferrofluid",
940
+ kind: "mark",
941
+ medium: "paint",
942
+ variety: "ferrofluid",
943
+ tip: { shape: "round", size: 28, hardness: 0.75, flow: 0.9 },
944
+ dynamics: { smoothing: 0.35 },
945
+ ferro: { amount: 0.85 },
946
+ colour: { default: "#111827", palette: ["#111827", "#1e293b", "#312e81", "#0f3d3e", "#3b0764", "#451a03"] }
947
+ },
948
+ {
949
+ id: "ferro-ink",
950
+ name: "Ferro ink",
951
+ kind: "mark",
952
+ medium: "ink",
953
+ variety: "ferrofluid",
954
+ tip: { shape: "round", size: 12, spacing: 0.08 },
955
+ dynamics: { smoothing: 0.45, minSize: 0.25, velocitySize: -0.25 },
956
+ ferro: { amount: 0.7, wavelength: 12, threshold: 0.15 },
957
+ colour: { default: "#0f172a" }
908
958
  }
909
959
  ];
910
960
 
@@ -1074,6 +1124,18 @@ function normalizeMutation(raw = {}, base = DEFAULT_MUTATION) {
1074
1124
  fade: range(raw.fade, 0.5, 10, base.fade)
1075
1125
  };
1076
1126
  }
1127
+ var DEFAULT_FERRO = { amount: 0, wavelength: 16, threshold: 0.12 };
1128
+ function normalizeFerro(raw = {}, base = DEFAULT_FERRO) {
1129
+ if (typeof raw === "number") {
1130
+ return { amount: unit2(raw, base.amount), wavelength: base.wavelength, threshold: base.threshold };
1131
+ }
1132
+ const r = _nullishCoalesce(raw, () => ( {}));
1133
+ return {
1134
+ amount: unit2(r.amount, base.amount),
1135
+ wavelength: range(r.wavelength, 4, 64, base.wavelength),
1136
+ threshold: range(r.threshold, 0.01, 0.95, base.threshold)
1137
+ };
1138
+ }
1077
1139
  function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
1078
1140
  return {
1079
1141
  tool: IMPASTO_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
@@ -1152,6 +1214,12 @@ function countAndFlip(raw, base) {
1152
1214
  return { ...n > 1 ? { count: n } : {}, ...flip !== "none" ? { flip } : {} };
1153
1215
  }
1154
1216
  var withTipGrain = (g) => g ? { tipGrain: g } : {};
1217
+ var withLivePatterns = (lp) => lp ? { livePatterns: lp } : {};
1218
+ function readLivePatterns(raw, base) {
1219
+ if (raw === null) return null;
1220
+ if (!raw || typeof raw !== "object") return base;
1221
+ return _chunkIFWLZZCWcjs.normalizeLivePatterns.call(void 0, raw, _nullishCoalesce(base, () => ( _chunkIFWLZZCWcjs.DEFAULT_LIVE_PATTERNS)));
1222
+ }
1155
1223
  function readTipGrain(raw, base) {
1156
1224
  if (raw === null) return null;
1157
1225
  if (!raw || typeof raw !== "object") return base;
@@ -1250,12 +1318,12 @@ function normalizePaintBrush(raw = {}, base) {
1250
1318
  if (rb.dirty !== void 0) mixerIn.cleanEachStroke = !rb.dirty;
1251
1319
  if (rb.mixing !== void 0) mixerIn.mixing = rb.mixing;
1252
1320
  }
1253
- const mixer = _chunkNIBFKXMDcjs.normalizeMixer.call(void 0, mixerIn);
1321
+ const mixer = _chunkIFWLZZCWcjs.normalizeMixer.call(void 0, mixerIn);
1254
1322
  let behavior = normalizeBehavior(rb, b.behavior);
1255
1323
  if (engine === "mixer") behavior = { ...behavior, pickup: mixer.wet, mix: mixer.mix, dirty: !mixer.cleanEachStroke, mixing: mixer.mixing };
1256
1324
  let paint = _nullishCoalesce(b.paint, () => ( null));
1257
1325
  if (raw.paint !== void 0) {
1258
- const p = _chunkNIBFKXMDcjs.readPaintSource.call(void 0, raw.paint);
1326
+ const p = _chunkIFWLZZCWcjs.readPaintSource.call(void 0, raw.paint);
1259
1327
  paint = p && p.type === "gradient" ? p : null;
1260
1328
  } else if (rb.multicolor !== void 0) {
1261
1329
  const mc = normalizeMultiColor(rb.multicolor, _nullishCoalesce(paintToMultiColor(paint), () => ( DEFAULT_MULTI_COLOR)));
@@ -1275,7 +1343,7 @@ function normalizePaintBrush(raw = {}, base) {
1275
1343
  name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
1276
1344
  category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
1277
1345
  engine,
1278
- tip: _chunkNIBFKXMDcjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(raw.tip, () => ( {})) }),
1346
+ tip: _chunkIFWLZZCWcjs.normalizeBrush.call(void 0, { ...b.tip, ..._nullishCoalesce(raw.tip, () => ( {})) }),
1279
1347
  grain: unit2(raw.grain, b.grain),
1280
1348
  pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
1281
1349
  rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
@@ -1283,8 +1351,8 @@ function normalizePaintBrush(raw = {}, base) {
1283
1351
  mixer,
1284
1352
  loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
1285
1353
  behavior,
1286
- blender: _chunkNIBFKXMDcjs.normalizeBlender.call(void 0, _nullishCoalesce(raw.blender, () => ( {})), b.blender),
1287
- smudge: _chunkNIBFKXMDcjs.normalizeSmudge.call(void 0, _nullishCoalesce(raw.smudge, () => ( {})), b.smudge),
1354
+ blender: _chunkIFWLZZCWcjs.normalizeBlender.call(void 0, _nullishCoalesce(raw.blender, () => ( {})), b.blender),
1355
+ smudge: _chunkIFWLZZCWcjs.normalizeSmudge.call(void 0, _nullishCoalesce(raw.smudge, () => ( {})), b.smudge),
1288
1356
  wet,
1289
1357
  impasto: normalizeImpasto(_nullishCoalesce(raw.impasto, () => ( {})), _nullishCoalesce(b.impasto, () => ( DEFAULT_IMPASTO))),
1290
1358
  particle: normalizeParticle(_nullishCoalesce(raw.particle, () => ( {})), _nullishCoalesce(b.particle, () => ( DEFAULT_PARTICLE))),
@@ -1296,9 +1364,11 @@ function normalizePaintBrush(raw = {}, base) {
1296
1364
  ...withIridescence(readIridescence(raw.iridescence, _nullishCoalesce(b.iridescence, () => ( null)))),
1297
1365
  ...ghost ? { ghost } : {},
1298
1366
  ...mutation ? { mutation } : {},
1367
+ ferro: normalizeFerro(raw.ferro, _nullishCoalesce(b.ferro, () => ( DEFAULT_FERRO))),
1299
1368
  stamp: readStamp(raw.stamp, _nullishCoalesce(b.stamp, () => ( null))),
1300
1369
  spray: readSpray(raw.spray, _nullishCoalesce(b.spray, () => ( null))),
1301
1370
  ...withTipGrain(readTipGrain(raw.tipGrain, _nullishCoalesce(b.tipGrain, () => ( null)))),
1371
+ ...withLivePatterns(readLivePatterns(raw.livePatterns, _nullishCoalesce(b.livePatterns, () => ( null)))),
1302
1372
  paint,
1303
1373
  ...raw.builtin ? { builtin: true } : {}
1304
1374
  };
@@ -1308,16 +1378,16 @@ var BASE_BRUSH = {
1308
1378
  name: "Round",
1309
1379
  category: "custom",
1310
1380
  engine: "dab",
1311
- tip: { ..._chunkNIBFKXMDcjs.DEFAULT_BRUSH },
1381
+ tip: { ..._chunkIFWLZZCWcjs.DEFAULT_BRUSH },
1312
1382
  grain: 0,
1313
1383
  pressureSmoothing: 0,
1314
1384
  rotation: "fixed",
1315
1385
  dynamics: { ...DEFAULT_DYNAMICS },
1316
- mixer: { ..._chunkNIBFKXMDcjs.DEFAULT_MIXER },
1386
+ mixer: { ..._chunkIFWLZZCWcjs.DEFAULT_MIXER },
1317
1387
  loadsColor: true,
1318
1388
  behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
1319
- blender: { ..._chunkNIBFKXMDcjs.DEFAULT_BLENDER },
1320
- smudge: { ..._chunkNIBFKXMDcjs.DEFAULT_SMUDGE },
1389
+ blender: { ..._chunkIFWLZZCWcjs.DEFAULT_BLENDER },
1390
+ smudge: { ..._chunkIFWLZZCWcjs.DEFAULT_SMUDGE },
1321
1391
  wet: { ...DEFAULT_WET_BRUSH },
1322
1392
  impasto: { ...DEFAULT_IMPASTO },
1323
1393
  particle: { ...DEFAULT_PARTICLE },
@@ -1326,6 +1396,7 @@ var BASE_BRUSH = {
1326
1396
  light: { ...DEFAULT_LIGHT },
1327
1397
  fluorescence: { ...DEFAULT_FLUORESCENCE },
1328
1398
  thermochromic: { ...DEFAULT_THERMOCHROMIC },
1399
+ ferro: { ...DEFAULT_FERRO },
1329
1400
  stamp: null,
1330
1401
  spray: null,
1331
1402
  paint: null
@@ -1352,6 +1423,23 @@ var ONE_STAMP = {
1352
1423
  toEngine: (v) => _nullishCoalesce(v[0], () => ( null)),
1353
1424
  toFacet: (v) => v ? [v] : void 0
1354
1425
  };
1426
+ var LIVE_PATTERNS_MAP = {
1427
+ toEngine: (v) => v !== null && typeof v === "object" ? JSON.parse(JSON.stringify(v)) : v,
1428
+ toFacet: (v) => {
1429
+ if (!v || typeof v !== "object") return void 0;
1430
+ const lp = v;
1431
+ const preset = _nullishCoalesce(_chunkIFWLZZCWcjs.LIVE_PATTERN_PRESETS[lp.pattern], () => ( _chunkIFWLZZCWcjs.LIVE_PATTERN_PRESETS.spots));
1432
+ return {
1433
+ pattern: lp.pattern,
1434
+ ...lp.feed !== preset.feed ? { feed: lp.feed } : {},
1435
+ ...lp.kill !== preset.kill ? { kill: lp.kill } : {},
1436
+ ...lp.scale !== _chunkIFWLZZCWcjs.DEFAULT_LIVE_PATTERNS.scale ? { scale: lp.scale } : {},
1437
+ ...lp.contrast !== _chunkIFWLZZCWcjs.DEFAULT_LIVE_PATTERNS.contrast ? { contrast: lp.contrast } : {},
1438
+ ...lp.secondColor !== null ? { secondColor: lp.secondColor } : {},
1439
+ ...lp.relief !== _chunkIFWLZZCWcjs.DEFAULT_LIVE_PATTERNS.relief ? { relief: lp.relief } : {}
1440
+ };
1441
+ }
1442
+ };
1355
1443
  var f = (facet, engine, engines = ALL, extra = {}) => ({ facet, engine, engines, ...extra });
1356
1444
  var each = (facetPrefix, enginePrefix, keys, engines) => keys.map((k) => f(`${facetPrefix}.${k}`, `${enginePrefix}.${k}`, engines));
1357
1445
  var BRUSH_FIELDS = [
@@ -1368,6 +1456,7 @@ var BRUSH_FIELDS = [
1368
1456
  ...each("fluorescence", "fluorescence", ["amount", "invisible"]),
1369
1457
  ...each("thermochromic", "thermochromic", ["hotColor", "threshold"]),
1370
1458
  ...each("iridescence", "iridescence", ["thickness", "pressureToThickness", "ior"], FILM),
1459
+ ...each("ferro", "ferro", ["amount", "wavelength", "threshold"]),
1371
1460
  // Paint exchange: the dab, wet and impasto engines' behaviour, or the Mixer Brush's settings.
1372
1461
  f("behaviour.exchange.pickup", "behavior.pickup", EXCHANGE),
1373
1462
  f("behaviour.exchange.mix", "behavior.mix", EXCHANGE),
@@ -1390,6 +1479,7 @@ var BRUSH_FIELDS = [
1390
1479
  f("behaviour.strength", "wet.strength", ["wet"]),
1391
1480
  // Surfaces.
1392
1481
  ...each("behaviour.wet", "wet", ["medium", "water", "pigment", "body", "rewet", "load", "thinner"], ["wet"]),
1482
+ f("livePatterns", "livePatterns", ["wet"], { map: LIVE_PATTERNS_MAP }),
1393
1483
  ...each("ghost", "ghost", ["linger", "smear", "trigger"], ["wet"]),
1394
1484
  ...each("mutation", "mutation", ["settle", "patience", "fade"], ["dab", "wet", "impasto", "particle", "bristle"]),
1395
1485
  f("behaviour.thick.applicator", "impasto.tool", ["impasto"]),
@@ -1486,6 +1576,7 @@ function resolveBrush(brush, options = {}) {
1486
1576
  if (brush.iridescence && FILM.includes(engine)) input.iridescence = { ..._nullishCoalesce(input.iridescence, () => ( {})) };
1487
1577
  if (engine === "wet") {
1488
1578
  setPath(input, "wet.tool", brush.kind === "tool" ? WET_TOOL_OF[brush.tool] : "paint");
1579
+ if (brush.kind === "tool") delete input.livePatterns;
1489
1580
  const medium = resolveWetMedium(brush);
1490
1581
  if (medium) setPath(input, "wet.medium", medium);
1491
1582
  if (brush.kind !== "tool" && brush.ghost) input.ghost = { ..._nullishCoalesce(input.ghost, () => ( {})) };
@@ -1574,7 +1665,7 @@ function skeleton(b) {
1574
1665
  }
1575
1666
  }
1576
1667
  }
1577
- var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "motion", "colour"];
1668
+ var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "ferro", "motion", "livePatterns", "colour"];
1578
1669
  function migrateBrushV1(raw) {
1579
1670
  const b = normalizePaintBrush(raw);
1580
1671
  const brush = skeleton(b);
@@ -1621,7 +1712,7 @@ var STAMP = {
1621
1712
  pattern: BRUSH_STAMP_PATTERN,
1622
1713
  description: `A sticker ref provider:id (find_stickers lists them; the editor's own are ${_chunkRDFHWAHLcjs.STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}), or an image source source:<id> (list_brush_sources lists them; add_brush_source makes one from an image). A bare editor stamp (star) means editor:star.`
1623
1714
  };
1624
- var TIP_GRAIN_PATTERNS = _chunkNIBFKXMDcjs.GRAIN_PATTERNS.filter((p) => p !== "smooth");
1715
+ var TIP_GRAIN_PATTERNS = _chunkIFWLZZCWcjs.GRAIN_PATTERNS.filter((p) => p !== "smooth");
1625
1716
  var BRUSH_GRAIN_SOURCE_PATTERN = `^(?:source:[a-z0-9][a-z0-9-]{0,63}|${TIP_GRAIN_PATTERNS.join("|")})$`;
1626
1717
  var TIP_GRAIN = object(
1627
1718
  "Texture inside the tip (a grain source), separate from the paper's grain: each dab is multiplied by it. Continuous and scatter marks of round and stamp tips.",
@@ -1647,7 +1738,7 @@ var TIP = object(
1647
1738
  "The tip: its shape and the dab it lays.",
1648
1739
  {
1649
1740
  shape: oneOf2(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp: dabs shaped like a stamp, a sticker or an image source (see stamps), also laid as sprays, objects or particles."),
1650
- size: num(1, _chunkNIBFKXMDcjs.MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
1741
+ size: num(1, _chunkIFWLZZCWcjs.MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
1651
1742
  hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1). Image-source stamps fade towards their edges below 1."),
1652
1743
  spacing: num(0.01, 10, "Dab spacing as a fraction of the diameter: 0.05 smooth, 1 dotted (default 0.1)."),
1653
1744
  angle: num(-360, 360, "Tip angle in degrees, counter-clockwise; added to the stroke direction or barrel with dynamics.rotation (default 0)."),
@@ -1813,11 +1904,29 @@ var MUTATION = object("Mutation paint: you paint something and pause; once an ag
1813
1904
  patience: num(5, 300, "Seconds to wait for an agent before giving up and keeping the paint as ordinary paint (default 45)."),
1814
1905
  fade: num(0.5, 10, "Seconds the paint takes to sink away once claimed by read_mutation (default 2).")
1815
1906
  });
1907
+ var FERRO = object("Liquid ferrofluid paint: rises into glossy Rosensweig spikes near magnets until cured.", {
1908
+ amount: unit3("How much liquid ferrofluid the brush lays per dab (default 0: none)."),
1909
+ wavelength: num(4, 64, "Capillary wavelength (spike-to-spike spacing) in pixels (default 16)."),
1910
+ threshold: num(0.01, 0.95, "Critical magnetic field |B| threshold where spikes begin to rise (default 0.12).")
1911
+ });
1816
1912
  var MOTION = object("Animation after painting.", {
1817
1913
  live: bool("Effect paint keeps playing on stroke layers (default false)."),
1818
1914
  fps: int(1, 60, "Suggested playback rate in frames per second."),
1819
1915
  waveSpeed: unit3("Particle waves: how fast they run by stroke time; 0 they stand still (default 0), 1 about four a second.")
1820
1916
  });
1917
+ var LIVE_PATTERNS = object("Gray-Scott reaction-diffusion patterns (spots, stripes, coral, maze, worms) that grow inside wet paint (behaviour.surface: wet) and freeze where the paint dries.", {
1918
+ pattern: oneOf2(_chunkIFWLZZCWcjs.LIVE_PATTERN_KINDS, "Named Pearson feed/kill preset: spots (default, dividing mitosis spots), stripes (labyrinthine stripes), coral (branching dendrites), maze (winding channels) or worms (vermicular tracks)."),
1919
+ feed: num(0.01, 0.1, "Gray-Scott feed rate F (0.01..0.1; default from pattern)."),
1920
+ kill: num(0.03, 0.08, "Gray-Scott kill rate k (0.03..0.08; default from pattern)."),
1921
+ scale: num(0.5, 4, "Spatial scale of the pattern (diffusion multiplier, 0.5..4, default 1)."),
1922
+ contrast: num(0, 2, "Visual contrast of the V concentration field (0..2, default 1)."),
1923
+ secondColor: {
1924
+ type: ["string", "null"],
1925
+ pattern: "^#[0-9a-fA-F]{6}$",
1926
+ description: "Optional second colour (#rrggbb) that V blends the wet paint towards; null or omitted modulates pigment density/contrast."
1927
+ },
1928
+ relief: unit3("Impasto relief height written to the layer thick map as patterns grow (0..1, default 0).")
1929
+ });
1821
1930
  var SOLID = {
1822
1931
  type: "string",
1823
1932
  pattern: "^(#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*(,\\s*(0|1|0?\\.\\d+)\\s*)?\\))$",
@@ -1847,7 +1956,7 @@ var EFFECT = object(
1847
1956
  );
1848
1957
  var PAINT = {
1849
1958
  description: "A colour a brush can carry: a solid colour, a light / dark pair, a gradient or effect paint.",
1850
- anyOf: [SOLID, DUAL, _chunkNIBFKXMDcjs.PAINT_GRADIENT_JSON_SCHEMA, EFFECT]
1959
+ anyOf: [SOLID, DUAL, _chunkIFWLZZCWcjs.PAINT_GRADIENT_JSON_SCHEMA, EFFECT]
1851
1960
  };
1852
1961
  var COLOUR = object("The brush's colours. The user's colour still wins unless own is set.", {
1853
1962
  default: {
@@ -1881,7 +1990,9 @@ var BRUSH_JSON_SCHEMA = object(
1881
1990
  iridescence: IRIDESCENCE,
1882
1991
  ghost: GHOST,
1883
1992
  mutation: MUTATION,
1993
+ ferro: FERRO,
1884
1994
  motion: MOTION,
1995
+ livePatterns: LIVE_PATTERNS,
1885
1996
  colour: COLOUR
1886
1997
  },
1887
1998
  ["id", "name", "kind", "tip"]
@@ -1898,7 +2009,9 @@ var BRUSH_FACET_SCHEMAS = {
1898
2009
  iridescence: IRIDESCENCE,
1899
2010
  ghost: GHOST,
1900
2011
  mutation: MUTATION,
2012
+ ferro: FERRO,
1901
2013
  motion: MOTION,
2014
+ livePatterns: LIVE_PATTERNS,
1902
2015
  colour: COLOUR,
1903
2016
  paint: PAINT
1904
2017
  };
@@ -2047,17 +2160,19 @@ function read(value, schema, path, problems) {
2047
2160
  problems.push(`${path} must be ${JSON.stringify(schema.const)}; dropped.`);
2048
2161
  return void 0;
2049
2162
  }
2050
- switch (schema.type) {
2163
+ if (value === null && Array.isArray(schema.type) && schema.type.includes("null")) return null;
2164
+ const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
2165
+ switch (schemaType) {
2051
2166
  case "number":
2052
2167
  case "integer": {
2053
2168
  if (typeof value !== "number" || !Number.isFinite(value)) {
2054
2169
  problems.push(`${path} must be a number; dropped.`);
2055
2170
  return void 0;
2056
2171
  }
2057
- let n = schema.type === "integer" ? Math.round(value) : value;
2172
+ let n = schemaType === "integer" ? Math.round(value) : value;
2058
2173
  if (schema.minimum !== void 0 && n < schema.minimum) n = schema.minimum;
2059
2174
  if (schema.maximum !== void 0 && n > schema.maximum) n = schema.maximum;
2060
- if (n !== value) problems.push(`${path} ${value} is out of range (${schema.minimum}..${schema.maximum}${schema.type === "integer" ? ", whole" : ""}); set to ${n}.`);
2175
+ if (n !== value) problems.push(`${path} ${value} is out of range (${schema.minimum}..${schema.maximum}${schemaType === "integer" ? ", whole" : ""}); set to ${n}.`);
2061
2176
  return n;
2062
2177
  }
2063
2178
  case "boolean":
@@ -2070,7 +2185,8 @@ function read(value, schema, path, problems) {
2070
2185
  problems.push(`${path} ${JSON.stringify(value)} is not valid${schema.enum ? `; use one of ${schema.enum.join(", ")}` : ""}; dropped.`);
2071
2186
  return void 0;
2072
2187
  }
2073
- return schema.maxLength !== void 0 ? value.slice(0, schema.maxLength) : value;
2188
+ const s = schema.pattern === "^#[0-9a-fA-F]{6}$" ? value.toLowerCase() : value;
2189
+ return schema.maxLength !== void 0 ? s.slice(0, schema.maxLength) : s;
2074
2190
  }
2075
2191
  case "array": {
2076
2192
  if (!Array.isArray(value)) {
@@ -2117,7 +2233,7 @@ function readPaint(value, path, problems) {
2117
2233
  if (typeof value === "string") {
2118
2234
  if (SOLID2.test(value.trim())) return value.trim().toLowerCase();
2119
2235
  } else if (isObject(value) && value.type === "gradient") {
2120
- const g = _chunkNIBFKXMDcjs.readPaintSource.call(void 0, value);
2236
+ const g = _chunkIFWLZZCWcjs.readPaintSource.call(void 0, value);
2121
2237
  if (g && g.type === "gradient") return g;
2122
2238
  } else if (isObject(value) && value.type === "effect") {
2123
2239
  if (!_chunkWOFXZ3XUcjs.validateJsonSchema.call(void 0, value, BRUSH_FACET_SCHEMAS.paint.anyOf[3]).length) return clone(value);
@@ -2139,14 +2255,14 @@ function normalizeBrushSpec(raw, problems = []) {
2139
2255
  else if (raw.kind === void 0) kind = tool ? "tool" : "mark";
2140
2256
  else throw new Error(`Brush "${raw.id}" kind ${JSON.stringify(raw.kind)} is not valid: use mark (lays a material) or tool (works on paint already down).`);
2141
2257
  if (kind === "tool" && !tool) throw new Error(`Brush "${raw.id}" is a tool and needs tool: one of ${BRUSH_TOOLS.join(", ")}.`);
2142
- const { colour: rawColour, marks: rawMarks, kind: _k, id: _i, name: _n, tool: _t, ...rest } = raw;
2258
+ const { colour: rawColour, marks: rawMarks, ferro: rawFerro, kind: _k, id: _i, name: _n, tool: _t, ...rest } = raw;
2143
2259
  if (typeof rest.fluorescence === "number" && Number.isFinite(rest.fluorescence)) rest.fluorescence = { amount: rest.fluorescence };
2144
2260
  const out = readObject(rest, BRUSH_JSON_SCHEMA, "brush", problems);
2145
2261
  if (_optionalChain([out, 'access', _29 => _29.thermochromic, 'optionalAccess', _30 => _30.hotColor])) out.thermochromic.hotColor = out.thermochromic.hotColor.toLowerCase();
2146
2262
  const brush = { id: raw.id, name, ...out, kind, ...tool ? { tool } : {}, tip: _nullishCoalesce(out.tip, () => ( { shape: "round" })) };
2147
2263
  const ordered = () => {
2148
- const { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, motion, colour } = brush;
2149
- const all = { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, motion, colour };
2264
+ const { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, ferro, motion, livePatterns, colour } = brush;
2265
+ const all = { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, ferro, motion, livePatterns, colour };
2150
2266
  return Object.fromEntries(Object.entries(all).filter(([, v]) => v !== void 0));
2151
2267
  };
2152
2268
  if (!out.tip) problems.push("tip is missing; a round tip is used.");
@@ -2160,6 +2276,11 @@ function normalizeBrushSpec(raw, problems = []) {
2160
2276
  if (!schema) problems.push(`marks.kind ${JSON.stringify(k)} is not valid; use continuous, scatter, particles or objects. marks dropped.`);
2161
2277
  else brush.marks = readObject(rawMarks, schema, "brush.marks", problems);
2162
2278
  }
2279
+ if (rawFerro !== void 0) {
2280
+ const ferroObj = typeof rawFerro === "number" ? { amount: rawFerro } : rawFerro;
2281
+ const f2 = readObject(ferroObj, BRUSH_FACET_SCHEMAS.ferro, "brush.ferro", problems);
2282
+ if (f2 && Object.keys(f2).length) brush.ferro = f2;
2283
+ }
2163
2284
  if (rawColour !== void 0) {
2164
2285
  const c = readObject(rawColour, { ...BRUSH_FACET_SCHEMAS.colour, properties: { own: BRUSH_FACET_SCHEMAS.colour.properties.own, default: {}, palette: {} } }, "brush.colour", problems);
2165
2286
  if (c) {
@@ -2194,6 +2315,10 @@ function normalizeBrushSpec(raw, problems = []) {
2194
2315
  drop("marks", "tools lay no marks");
2195
2316
  delete brush.marks;
2196
2317
  }
2318
+ if (brush.ferro) {
2319
+ drop("ferro", "tools lay no ferrofluid");
2320
+ delete brush.ferro;
2321
+ }
2197
2322
  if (brush.iridescence) {
2198
2323
  drop("iridescence", "tools lay no film");
2199
2324
  delete brush.iridescence;
@@ -2279,8 +2404,12 @@ function normalizeBrushSpec(raw, problems = []) {
2279
2404
  delete b.exchange;
2280
2405
  }
2281
2406
  }
2407
+ if (brush.livePatterns && (kind !== "mark" || _optionalChain([brush, 'access', _52 => _52.behaviour, 'optionalAccess', _53 => _53.surface]) !== "wet" || _optionalChain([brush, 'access', _54 => _54.marks, 'optionalAccess', _55 => _55.kind]) === "particles" || _optionalChain([brush, 'access', _56 => _56.marks, 'optionalAccess', _57 => _57.kind]) === "objects" || brush.tip.shape === "bristle")) {
2408
+ drop("livePatterns", "only wet paint brushes grow live patterns (set behaviour.surface to wet)");
2409
+ delete brush.livePatterns;
2410
+ }
2282
2411
  if (brush.ghost) {
2283
- if (kind === "tool" || _optionalChain([brush, 'access', _52 => _52.behaviour, 'optionalAccess', _53 => _53.surface]) !== "wet") {
2412
+ if (kind === "tool" || _optionalChain([brush, 'access', _58 => _58.behaviour, 'optionalAccess', _59 => _59.surface]) !== "wet") {
2284
2413
  drop("ghost", "it needs a mark brush with behaviour.surface wet");
2285
2414
  delete brush.ghost;
2286
2415
  } else {
@@ -2288,7 +2417,7 @@ function normalizeBrushSpec(raw, problems = []) {
2288
2417
  drop("light", "ghost paint uses the wet light channel as its tracer");
2289
2418
  delete brush.light;
2290
2419
  }
2291
- if (_optionalChain([brush, 'access', _54 => _54.behaviour, 'optionalAccess', _55 => _55.exchange])) {
2420
+ if (_optionalChain([brush, 'access', _60 => _60.behaviour, 'optionalAccess', _61 => _61.exchange])) {
2292
2421
  drop("behaviour.exchange", "ghost paint does not pick paint up");
2293
2422
  delete brush.behaviour.exchange;
2294
2423
  }
@@ -2318,7 +2447,9 @@ var PAINT_PROFILE = {
2318
2447
  tilt: "full",
2319
2448
  tipGrain: "full",
2320
2449
  dabCount: "full",
2321
- iridescence: "full"
2450
+ livePatterns: "full",
2451
+ iridescence: "full",
2452
+ ferro: "full"
2322
2453
  };
2323
2454
  var IMAGE_PROFILE = {
2324
2455
  id: "image",
@@ -2386,28 +2517,30 @@ function checkBrush(brush, profile) {
2386
2517
  const image = brush.tip.shape === "stamp" && (_nullishCoalesce(brush.tip.stamps, () => ( []))).some(isBrushSourceRef);
2387
2518
  add("tip.shape", "error", `${host} can't lay a ${image ? "stamp tip (image tips are stamps)" : `${brush.tip.shape} tip`}.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
2388
2519
  }
2389
- const marks = _nullishCoalesce(_optionalChain([brush, 'access', _56 => _56.marks, 'optionalAccess', _57 => _57.kind]), () => ( "continuous"));
2520
+ const marks = _nullishCoalesce(_optionalChain([brush, 'access', _62 => _62.marks, 'optionalAccess', _63 => _63.kind]), () => ( "continuous"));
2390
2521
  if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
2391
- const surface = _nullishCoalesce(_optionalChain([brush, 'access', _58 => _58.behaviour, 'optionalAccess', _59 => _59.surface]), () => ( "dry"));
2522
+ const surface = _nullishCoalesce(_optionalChain([brush, 'access', _64 => _64.behaviour, 'optionalAccess', _65 => _65.surface]), () => ( "dry"));
2392
2523
  if (brush.kind === "mark" && !profile.surfaces.includes(surface)) add("behaviour.surface", "error", `${host} has no ${surface} surface.`, `Set behaviour.surface to ${list(profile.surfaces)}.`);
2393
- const ex = _optionalChain([brush, 'access', _60 => _60.behaviour, 'optionalAccess', _61 => _61.exchange]);
2394
- feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _62 => _62.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
2395
- feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _63 => _63.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _64 => _64.pickup])) || !!_optionalChain([ex, 'optionalAccess', _65 => _65.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
2396
- feature(profile.grain, nonZero(_optionalChain([brush, 'access', _66 => _66.behaviour, 'optionalAccess', _67 => _67.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
2397
- feature(profile.light, nonZero(_optionalChain([brush, 'access', _68 => _68.light, 'optionalAccess', _69 => _69.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
2524
+ const ex = _optionalChain([brush, 'access', _66 => _66.behaviour, 'optionalAccess', _67 => _67.exchange]);
2525
+ feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _68 => _68.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
2526
+ feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _69 => _69.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _70 => _70.pickup])) || !!_optionalChain([ex, 'optionalAccess', _71 => _71.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
2527
+ feature(profile.grain, nonZero(_optionalChain([brush, 'access', _72 => _72.behaviour, 'optionalAccess', _73 => _73.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
2528
+ feature(profile.light, nonZero(_optionalChain([brush, 'access', _74 => _74.light, 'optionalAccess', _75 => _75.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
2529
+ feature(_nullishCoalesce(profile.livePatterns, () => ( "none")), Boolean(brush.livePatterns), "livePatterns", "live reaction-diffusion patterns", "Remove livePatterns.");
2398
2530
  const smartInks = profile.id === "paint" ? "full" : "none";
2399
- feature(smartInks, nonZero(_optionalChain([brush, 'access', _70 => _70.fluorescence, 'optionalAccess', _71 => _71.amount])), "fluorescence", "UV-fluorescent ink", "Remove fluorescence (or set fluorescence.amount to 0).");
2400
- feature(smartInks, Boolean(_optionalChain([brush, 'access', _72 => _72.thermochromic, 'optionalAccess', _73 => _73.hotColor])), "thermochromic", "thermochromic ink", "Remove thermochromic.");
2531
+ feature(smartInks, nonZero(_optionalChain([brush, 'access', _76 => _76.fluorescence, 'optionalAccess', _77 => _77.amount])), "fluorescence", "UV-fluorescent ink", "Remove fluorescence (or set fluorescence.amount to 0).");
2532
+ feature(smartInks, Boolean(_optionalChain([brush, 'access', _78 => _78.thermochromic, 'optionalAccess', _79 => _79.hotColor])), "thermochromic", "thermochromic ink", "Remove thermochromic.");
2401
2533
  feature(_nullishCoalesce(profile.iridescence, () => ( "none")), !!brush.iridescence, "iridescence", "thin-film iridescence", "Remove iridescence.");
2402
2534
  feature(profile.surfaces.includes("wet") && profile.light === "full" ? "full" : "none", !!brush.ghost, "ghost", "ghost paint", "Remove ghost.");
2403
2535
  if (brush.mutation) {
2404
2536
  if (profile.id !== "paint") feature("none", true, "mutation", "mutation paint", "Remove mutation.");
2405
2537
  else if (brush.kind === "tool") add("mutation", "error", "Tools work on paint already down and cannot be mutation brushes.", "Use a mark brush, or remove mutation.");
2406
- else if (_optionalChain([brush, 'access', _74 => _74.marks, 'optionalAccess', _75 => _75.kind]) === "objects") add("mutation", "error", "Sticker brushes place objects rather than paint strokes and cannot be mutation brushes.", "Remove marks (or use continuous marks), or remove mutation.");
2407
- else if (_optionalChain([ex, 'optionalAccess', _76 => _76.reservoir])) add("mutation", "error", "A Mixer Brush reads paint from the layer beneath and cannot be a mutation brush.", "Remove behaviour.exchange.reservoir, or remove mutation.");
2408
- else if (nonZero(_optionalChain([ex, 'optionalAccess', _77 => _77.pickup])) || !!_optionalChain([ex, 'optionalAccess', _78 => _78.dirty])) add("mutation", "error", "Mutation brushes must lay their own colour without picking up paint from beneath.", "Remove behaviour.exchange (or set pickup to 0 and dirty to false), or remove mutation.");
2409
- else if (surface === "wet" && nonZero(_optionalChain([brush, 'access', _79 => _79.light, 'optionalAccess', _80 => _80.amount]))) add("mutation", "error", "Wet mutation brushes use the wet light channel as their fade tracer.", "Remove light (or set light.amount to 0), or remove mutation.");
2538
+ else if (_optionalChain([brush, 'access', _80 => _80.marks, 'optionalAccess', _81 => _81.kind]) === "objects") add("mutation", "error", "Sticker brushes place objects rather than paint strokes and cannot be mutation brushes.", "Remove marks (or use continuous marks), or remove mutation.");
2539
+ else if (_optionalChain([ex, 'optionalAccess', _82 => _82.reservoir])) add("mutation", "error", "A Mixer Brush reads paint from the layer beneath and cannot be a mutation brush.", "Remove behaviour.exchange.reservoir, or remove mutation.");
2540
+ else if (nonZero(_optionalChain([ex, 'optionalAccess', _83 => _83.pickup])) || !!_optionalChain([ex, 'optionalAccess', _84 => _84.dirty])) add("mutation", "error", "Mutation brushes must lay their own colour without picking up paint from beneath.", "Remove behaviour.exchange (or set pickup to 0 and dirty to false), or remove mutation.");
2541
+ else if (surface === "wet" && nonZero(_optionalChain([brush, 'access', _85 => _85.light, 'optionalAccess', _86 => _86.amount]))) add("mutation", "error", "Wet mutation brushes use the wet light channel as their fade tracer.", "Remove light (or set light.amount to 0), or remove mutation.");
2410
2542
  }
2543
+ feature(_nullishCoalesce(profile.ferro, () => ( "none")), nonZero(_optionalChain([brush, 'access', _87 => _87.ferro, 'optionalAccess', _88 => _88.amount])), "ferro", "ferrofluid spikes and magnets", "Remove ferro (or set ferro.amount to 0).");
2411
2544
  const m = brush.motion;
2412
2545
  feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
2413
2546
  const d = _nullishCoalesce(brush.dynamics, () => ( {}));
@@ -2416,16 +2549,16 @@ function checkBrush(brush, profile) {
2416
2549
  feature(profile.velocity, nonZero(d.velocitySize) || nonZero(d.velocityOpacity), "dynamics.velocity", "velocity dynamics (its strokes carry no timing)", "Set dynamics.velocitySize and velocityOpacity to 0.");
2417
2550
  feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
2418
2551
  const colour = brush.colour;
2419
- if (_optionalChain([colour, 'optionalAccess', _81 => _81.default]) !== void 0) {
2552
+ if (_optionalChain([colour, 'optionalAccess', _89 => _89.default]) !== void 0) {
2420
2553
  const kind = brushColourKind(colour.default);
2421
2554
  if (!profile.colours.includes(kind)) add("colour.default", "error", `${host} can't paint ${kind} colours.`, `Use a ${list(profile.colours)} colour, or remove colour.default.`);
2422
2555
  }
2423
- const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _82 => _82.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
2556
+ const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _90 => _90.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
2424
2557
  if (foreign.length) add("colour.palette", "warning", `${host} skips the palette's ${list(foreign)} colours.`, `Nothing to do; or keep to ${list(profile.colours)} colours.`);
2425
2558
  return { ok: !issues.some((i) => i.severity === "error"), issues };
2426
2559
  }
2427
2560
  function withKeptIssues(check, before) {
2428
- const had = new Set((_nullishCoalesce(_optionalChain([before, 'optionalAccess', _83 => _83.issues]), () => ( []))).filter((i) => i.severity === "error").map((i) => i.facet));
2561
+ const had = new Set((_nullishCoalesce(_optionalChain([before, 'optionalAccess', _91 => _91.issues]), () => ( []))).filter((i) => i.severity === "error").map((i) => i.facet));
2429
2562
  const kept = check.issues.filter((i) => i.severity === "error" && had.has(i.facet));
2430
2563
  const issues = check.issues.filter((i) => !kept.includes(i));
2431
2564
  return { ok: !issues.some((i) => i.severity === "error"), issues, kept };
@@ -2458,8 +2591,12 @@ function degrade(brush, profile, options = {}) {
2458
2591
  delete b.behaviour.surface;
2459
2592
  delete b.behaviour.wet;
2460
2593
  delete b.behaviour.thick;
2594
+ delete b.livePatterns;
2461
2595
  delete b.ghost;
2462
2596
  break;
2597
+ case "livePatterns":
2598
+ delete b.livePatterns;
2599
+ break;
2463
2600
  case "ghost":
2464
2601
  delete b.ghost;
2465
2602
  break;
@@ -2493,6 +2630,9 @@ function degrade(brush, profile, options = {}) {
2493
2630
  case "iridescence":
2494
2631
  delete b.iridescence;
2495
2632
  break;
2633
+ case "ferro":
2634
+ delete b.ferro;
2635
+ break;
2496
2636
  case "motion":
2497
2637
  delete b.motion;
2498
2638
  break;
@@ -2517,7 +2657,7 @@ function degrade(brush, profile, options = {}) {
2517
2657
  }
2518
2658
  dropped.push(issue);
2519
2659
  }
2520
- if (_optionalChain([b, 'access', _84 => _84.behaviour, 'optionalAccess', _85 => _85.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
2660
+ if (_optionalChain([b, 'access', _92 => _92.behaviour, 'optionalAccess', _93 => _93.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
2521
2661
  if (b.colour && !Object.keys(b.colour).length) delete b.colour;
2522
2662
  const after = checkBrush(b, profile).issues.filter((i) => i.severity === "error");
2523
2663
  return { brush: b, dropped, issues: after };
@@ -2561,7 +2701,7 @@ function parseBrushLibrary2(raw, problems = []) {
2561
2701
  if (file && file.version !== void 0 && file.version !== 1 && file.version !== 2) {
2562
2702
  throw new BrushLibraryError("invalid", `Brush library version ${JSON.stringify(file.version)} is not supported; this library reads versions 1 and 2.`);
2563
2703
  }
2564
- const v1File = _optionalChain([file, 'optionalAccess', _86 => _86.version]) === 1;
2704
+ const v1File = _optionalChain([file, 'optionalAccess', _94 => _94.version]) === 1;
2565
2705
  const out = [];
2566
2706
  items.forEach((item, i) => {
2567
2707
  const at = `brushes[${i}]`;
@@ -2822,7 +2962,7 @@ function snapshotBrushLibrary(library, onChange) {
2822
2962
  (list2) => {
2823
2963
  entries = list2;
2824
2964
  user = list2.filter((e) => !e.builtin).map((e) => e.brush);
2825
- _optionalChain([onChange, 'optionalCall', _87 => _87()]);
2965
+ _optionalChain([onChange, 'optionalCall', _95 => _95()]);
2826
2966
  },
2827
2967
  () => void 0
2828
2968
  );
@@ -2964,4 +3104,6 @@ function snapshotBrushLibrary(library, onChange) {
2964
3104
 
2965
3105
 
2966
3106
 
2967
- exports.BRUSH_PRESETS = BRUSH_PRESETS; exports.MAX_STICKER_STAMPS = MAX_STICKER_STAMPS; exports.DEFAULT_STICKER = DEFAULT_STICKER; exports.normalizeStickerSettings = normalizeStickerSettings; exports.PARTICLE_KINDS = PARTICLE_KINDS; exports.PARTICLE_RENDERS = PARTICLE_RENDERS; exports.RIBBON_PAIRS = RIBBON_PAIRS; exports.DOT_TEXTURE_ORDERS = DOT_TEXTURE_ORDERS; exports.DOT_ROTATIONS = DOT_ROTATIONS; exports.MAX_DOT_TEXTURES = MAX_DOT_TEXTURES; exports.DEFAULT_PARTICLE = DEFAULT_PARTICLE; exports.normalizeParticle = normalizeParticle; exports.DEFAULT_BRISTLE = DEFAULT_BRISTLE; exports.normalizeBristle = normalizeBristle; exports.PAINT_BRUSH_ENGINES = PAINT_BRUSH_ENGINES; exports.PAINT_BRUSH_CATEGORIES = PAINT_BRUSH_CATEGORIES; exports.WET_TOOLS = WET_TOOLS; exports.WET_MEDIA = WET_MEDIA; exports.DEFAULT_WET_BRUSH = DEFAULT_WET_BRUSH; exports.normalizeWetBrush = normalizeWetBrush; exports.IMPASTO_TOOLS = IMPASTO_TOOLS; exports.DEFAULT_IMPASTO = DEFAULT_IMPASTO; exports.DEFAULT_LIGHT = DEFAULT_LIGHT; exports.normalizeLight = normalizeLight; exports.DEFAULT_FLUORESCENCE = DEFAULT_FLUORESCENCE; exports.normalizeFluorescence = normalizeFluorescence; exports.DEFAULT_THERMOCHROMIC = DEFAULT_THERMOCHROMIC; exports.normalizeThermochromic = normalizeThermochromic; exports.DEFAULT_IRIDESCENCE = DEFAULT_IRIDESCENCE; exports.GHOST_TRIGGERS = GHOST_TRIGGERS; exports.DEFAULT_GHOST = DEFAULT_GHOST; exports.normalizeGhost = normalizeGhost; exports.DEFAULT_MUTATION = DEFAULT_MUTATION; exports.normalizeMutation = normalizeMutation; exports.normalizeImpasto = normalizeImpasto; exports.MULTI_COLOR_MODES = MULTI_COLOR_MODES; exports.DEFAULT_MULTI_COLOR = DEFAULT_MULTI_COLOR; exports.DEFAULT_BEHAVIOR = DEFAULT_BEHAVIOR; exports.BRUSH_ROTATIONS = BRUSH_ROTATIONS; exports.DAB_FLIPS = DAB_FLIPS; exports.TIP_GRAIN_MODES = TIP_GRAIN_MODES; exports.DEFAULT_TIP_GRAIN = DEFAULT_TIP_GRAIN; exports.DEFAULT_DYNAMICS = DEFAULT_DYNAMICS; exports.normalizeDynamics = normalizeDynamics; exports.BRUSH_ID = BRUSH_ID; exports.normalizeMultiColor = normalizeMultiColor; exports.normalizeBehavior = normalizeBehavior; exports.DEFAULT_SPRAY = DEFAULT_SPRAY; exports.isWetCapable = isWetCapable; exports.isWetBrush = isWetBrush; exports.usesPaintExchange = usesPaintExchange; exports.brushTakesPaint = brushTakesPaint; exports.paintToMultiColor = paintToMultiColor; exports.multiColorToPaint = multiColorToPaint; exports.normalizePaintBrush = normalizePaintBrush; exports.BRUSH_FIELDS = BRUSH_FIELDS; exports.getPath = getPath; exports.setPath = setPath; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium; exports.resolveCategory = resolveCategory; exports.resolveBrush = resolveBrush; exports.BUILTIN_BRUSHES = BUILTIN_BRUSHES; exports.DEFAULT_BRUSH_ID = DEFAULT_BRUSH_ID; exports.brushToJSON = brushToJSON; exports.parseBrushLibrary = parseBrushLibrary; exports.migrateBrushV1 = migrateBrushV1; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.BRUSH_GRAIN_MODES = BRUSH_GRAIN_MODES; exports.BRUSH_FLIPS = BRUSH_FLIPS; exports.GHOST_TRIGGERS2 = GHOST_TRIGGERS2; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.BRUSH_WORD = BRUSH_WORD; exports.TIP_GRAIN_PATTERNS = TIP_GRAIN_PATTERNS; exports.BRUSH_GRAIN_SOURCE_PATTERN = BRUSH_GRAIN_SOURCE_PATTERN; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.BRUSH_SOURCE_PROVIDER = BRUSH_SOURCE_PROVIDER; exports.MAX_BRUSH_SOURCE_SIZE = MAX_BRUSH_SOURCE_SIZE; exports.MAX_BRUSH_SOURCE_INPUT_PIXELS = MAX_BRUSH_SOURCE_INPUT_PIXELS; exports.MAX_BRUSH_SOURCES = MAX_BRUSH_SOURCES; exports.BRUSH_SOURCE_CHANNELS = BRUSH_SOURCE_CHANNELS; exports.BrushSourceError = BrushSourceError; exports.brushSourceRef = brushSourceRef; exports.isBrushSourceRef = isBrushSourceRef; exports.brushSourceIdOf = brushSourceIdOf; exports.brushSourceIds = brushSourceIds; exports.summarizeBrushSource = summarizeBrushSource; exports.brushSourceIdFromName = brushSourceIdFromName; exports.brushSourceFromMask = brushSourceFromMask; exports.makeBrushSource = makeBrushSource; exports.readBrushSource = readBrushSource; exports.decodeBrushSource = decodeBrushSource; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.normalizeBrushSpec = normalizeBrushSpec; exports.PAINT_PROFILE = PAINT_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.withKeptIssues = withKeptIssues; exports.checkBrushEdit = checkBrushEdit; exports.listPresets = listPresets; exports.degrade = degrade; exports.BrushLibraryError = BrushLibraryError; exports.isBrushLibrary = isBrushLibrary; exports.parseBrushLibrarySources = parseBrushLibrarySources; exports.parseBrushLibrary2 = parseBrushLibrary2; exports.createBrushLibrary = createBrushLibrary; exports.closest = closest; exports.snapshotBrushLibrary = snapshotBrushLibrary;
3107
+
3108
+
3109
+ exports.BRUSH_PRESETS = BRUSH_PRESETS; exports.MAX_STICKER_STAMPS = MAX_STICKER_STAMPS; exports.DEFAULT_STICKER = DEFAULT_STICKER; exports.normalizeStickerSettings = normalizeStickerSettings; exports.PARTICLE_KINDS = PARTICLE_KINDS; exports.PARTICLE_RENDERS = PARTICLE_RENDERS; exports.RIBBON_PAIRS = RIBBON_PAIRS; exports.DOT_TEXTURE_ORDERS = DOT_TEXTURE_ORDERS; exports.DOT_ROTATIONS = DOT_ROTATIONS; exports.MAX_DOT_TEXTURES = MAX_DOT_TEXTURES; exports.DEFAULT_PARTICLE = DEFAULT_PARTICLE; exports.normalizeParticle = normalizeParticle; exports.DEFAULT_BRISTLE = DEFAULT_BRISTLE; exports.normalizeBristle = normalizeBristle; exports.PAINT_BRUSH_ENGINES = PAINT_BRUSH_ENGINES; exports.PAINT_BRUSH_CATEGORIES = PAINT_BRUSH_CATEGORIES; exports.WET_TOOLS = WET_TOOLS; exports.WET_MEDIA = WET_MEDIA; exports.DEFAULT_WET_BRUSH = DEFAULT_WET_BRUSH; exports.normalizeWetBrush = normalizeWetBrush; exports.IMPASTO_TOOLS = IMPASTO_TOOLS; exports.DEFAULT_IMPASTO = DEFAULT_IMPASTO; exports.DEFAULT_LIGHT = DEFAULT_LIGHT; exports.normalizeLight = normalizeLight; exports.DEFAULT_FLUORESCENCE = DEFAULT_FLUORESCENCE; exports.normalizeFluorescence = normalizeFluorescence; exports.DEFAULT_THERMOCHROMIC = DEFAULT_THERMOCHROMIC; exports.normalizeThermochromic = normalizeThermochromic; exports.DEFAULT_IRIDESCENCE = DEFAULT_IRIDESCENCE; exports.GHOST_TRIGGERS = GHOST_TRIGGERS; exports.DEFAULT_GHOST = DEFAULT_GHOST; exports.normalizeGhost = normalizeGhost; exports.DEFAULT_MUTATION = DEFAULT_MUTATION; exports.normalizeMutation = normalizeMutation; exports.DEFAULT_FERRO = DEFAULT_FERRO; exports.normalizeFerro = normalizeFerro; exports.normalizeImpasto = normalizeImpasto; exports.MULTI_COLOR_MODES = MULTI_COLOR_MODES; exports.DEFAULT_MULTI_COLOR = DEFAULT_MULTI_COLOR; exports.DEFAULT_BEHAVIOR = DEFAULT_BEHAVIOR; exports.BRUSH_ROTATIONS = BRUSH_ROTATIONS; exports.DAB_FLIPS = DAB_FLIPS; exports.TIP_GRAIN_MODES = TIP_GRAIN_MODES; exports.DEFAULT_TIP_GRAIN = DEFAULT_TIP_GRAIN; exports.DEFAULT_DYNAMICS = DEFAULT_DYNAMICS; exports.normalizeDynamics = normalizeDynamics; exports.BRUSH_ID = BRUSH_ID; exports.normalizeMultiColor = normalizeMultiColor; exports.normalizeBehavior = normalizeBehavior; exports.DEFAULT_SPRAY = DEFAULT_SPRAY; exports.isWetCapable = isWetCapable; exports.isWetBrush = isWetBrush; exports.usesPaintExchange = usesPaintExchange; exports.brushTakesPaint = brushTakesPaint; exports.paintToMultiColor = paintToMultiColor; exports.multiColorToPaint = multiColorToPaint; exports.normalizePaintBrush = normalizePaintBrush; exports.BRUSH_FIELDS = BRUSH_FIELDS; exports.getPath = getPath; exports.setPath = setPath; exports.resolveEngine = resolveEngine; exports.resolveWetMedium = resolveWetMedium; exports.resolveCategory = resolveCategory; exports.resolveBrush = resolveBrush; exports.BUILTIN_BRUSHES = BUILTIN_BRUSHES; exports.DEFAULT_BRUSH_ID = DEFAULT_BRUSH_ID; exports.brushToJSON = brushToJSON; exports.parseBrushLibrary = parseBrushLibrary; exports.migrateBrushV1 = migrateBrushV1; exports.BRUSH_KINDS = BRUSH_KINDS; exports.BRUSH_MEDIA = BRUSH_MEDIA; exports.BRUSH_TOOLS = BRUSH_TOOLS; exports.BRUSH_TIP_SHAPES = BRUSH_TIP_SHAPES; exports.BRUSH_MARKS_KINDS = BRUSH_MARKS_KINDS; exports.BRUSH_SURFACES = BRUSH_SURFACES; exports.BRUSH_GRAIN_MODES = BRUSH_GRAIN_MODES; exports.BRUSH_FLIPS = BRUSH_FLIPS; exports.GHOST_TRIGGERS2 = GHOST_TRIGGERS2; exports.MAX_BRUSH_PALETTE = MAX_BRUSH_PALETTE; exports.BRUSH_WORD = BRUSH_WORD; exports.TIP_GRAIN_PATTERNS = TIP_GRAIN_PATTERNS; exports.BRUSH_GRAIN_SOURCE_PATTERN = BRUSH_GRAIN_SOURCE_PATTERN; exports.BRUSH_JSON_SCHEMA = BRUSH_JSON_SCHEMA; exports.BRUSH_FACET_SCHEMAS = BRUSH_FACET_SCHEMAS; exports.BRUSH_SOURCE_PROVIDER = BRUSH_SOURCE_PROVIDER; exports.MAX_BRUSH_SOURCE_SIZE = MAX_BRUSH_SOURCE_SIZE; exports.MAX_BRUSH_SOURCE_INPUT_PIXELS = MAX_BRUSH_SOURCE_INPUT_PIXELS; exports.MAX_BRUSH_SOURCES = MAX_BRUSH_SOURCES; exports.BRUSH_SOURCE_CHANNELS = BRUSH_SOURCE_CHANNELS; exports.BrushSourceError = BrushSourceError; exports.brushSourceRef = brushSourceRef; exports.isBrushSourceRef = isBrushSourceRef; exports.brushSourceIdOf = brushSourceIdOf; exports.brushSourceIds = brushSourceIds; exports.summarizeBrushSource = summarizeBrushSource; exports.brushSourceIdFromName = brushSourceIdFromName; exports.brushSourceFromMask = brushSourceFromMask; exports.makeBrushSource = makeBrushSource; exports.readBrushSource = readBrushSource; exports.decodeBrushSource = decodeBrushSource; exports.WET_BRUSH_TOOLS = WET_BRUSH_TOOLS; exports.normalizeBrushSpec = normalizeBrushSpec; exports.PAINT_PROFILE = PAINT_PROFILE; exports.IMAGE_PROFILE = IMAGE_PROFILE; exports.CANVAS_PROFILE = CANVAS_PROFILE; exports.VECTOR_PROFILE = VECTOR_PROFILE; exports.BRUSH_HOST_PROFILES = BRUSH_HOST_PROFILES; exports.brushColourKind = brushColourKind; exports.checkBrush = checkBrush; exports.withKeptIssues = withKeptIssues; exports.checkBrushEdit = checkBrushEdit; exports.listPresets = listPresets; exports.degrade = degrade; exports.BrushLibraryError = BrushLibraryError; exports.isBrushLibrary = isBrushLibrary; exports.parseBrushLibrarySources = parseBrushLibrarySources; exports.parseBrushLibrary2 = parseBrushLibrary2; exports.createBrushLibrary = createBrushLibrary; exports.closest = closest; exports.snapshotBrushLibrary = snapshotBrushLibrary;