@pixerate/editor 0.274.0 → 0.275.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 (79) hide show
  1. package/dist/animation-editor/index.cjs +60 -60
  2. package/dist/animation-editor/index.js +4 -4
  3. package/dist/brushes/index.cjs +102 -74
  4. package/dist/brushes/index.js +34 -6
  5. package/dist/brushes/patch.d.cts +1 -1
  6. package/dist/brushes/patch.d.ts +1 -1
  7. package/dist/brushes/preview.d.cts +3 -0
  8. package/dist/brushes/preview.d.ts +3 -0
  9. package/dist/brushes/profiles.d.cts +2 -0
  10. package/dist/brushes/profiles.d.ts +2 -0
  11. package/dist/brushes/schema.d.cts +4 -0
  12. package/dist/brushes/schema.d.ts +4 -0
  13. package/dist/brushes/types.d.cts +47 -0
  14. package/dist/brushes/types.d.ts +47 -0
  15. package/dist/{brushstrokeRender-5YBCLFDQ.cjs → brushstrokeRender-2RQL46WB.cjs} +15 -15
  16. package/dist/{brushstrokeRender-DZGFU3J4.js → brushstrokeRender-WGY4DZHF.js} +3 -3
  17. package/dist/canvas/index.cjs +13 -13
  18. package/dist/canvas/index.js +1 -1
  19. package/dist/{chunk-2XDNF73E.js → chunk-43XJC3PP.js} +1 -1
  20. package/dist/{chunk-GHGBIAWQ.cjs → chunk-6QNDGMK7.cjs} +263 -45
  21. package/dist/{chunk-2F3YDXCN.js → chunk-75SYKUOT.js} +1 -1
  22. package/dist/{chunk-M3RZOF3G.cjs → chunk-C6VB7CPY.cjs} +1 -1
  23. package/dist/{chunk-N5IMDAHM.cjs → chunk-DHC4E2DU.cjs} +89 -37
  24. package/dist/{chunk-NAHJV3O6.js → chunk-G5TCMKX5.js} +28 -11
  25. package/dist/{chunk-DZVRH63Y.js → chunk-IEEY5RJV.js} +1 -1
  26. package/dist/{chunk-7W2EQQBP.cjs → chunk-JDXWLCXF.cjs} +6 -6
  27. package/dist/{chunk-N3SKPQEH.js → chunk-LMG57L5A.js} +62 -10
  28. package/dist/{chunk-5HP2VSAS.js → chunk-MSWLJI4G.js} +1 -1
  29. package/dist/{chunk-SNAFFCAX.cjs → chunk-OLZXXAER.cjs} +221 -196
  30. package/dist/{chunk-YJANF2BL.cjs → chunk-RAPNRK4R.cjs} +86 -86
  31. package/dist/{chunk-CLB2I2FU.js → chunk-RJGW3RQS.js} +2 -2
  32. package/dist/{chunk-FXVCB6YG.cjs → chunk-T6KNJVBP.cjs} +35 -35
  33. package/dist/{chunk-ZEI7ZEA4.js → chunk-Y2ISSVPH.js} +34 -9
  34. package/dist/{chunk-3JKCREEU.cjs → chunk-Y354ETAE.cjs} +76 -59
  35. package/dist/{chunk-TNLKFW7D.cjs → chunk-YYZC2EQO.cjs} +23 -23
  36. package/dist/{chunk-UXYCY3HP.js → chunk-ZM7IKQ4X.js} +239 -21
  37. package/dist/image-editor/index.cjs +789 -789
  38. package/dist/image-editor/index.js +5 -5
  39. package/dist/paint/index.cjs +2 -2
  40. package/dist/paint/index.js +1 -1
  41. package/dist/paint/wet/world.d.cts +15 -2
  42. package/dist/paint/wet/world.d.ts +15 -2
  43. package/dist/paint-editor/brush-controls.d.cts +7 -0
  44. package/dist/paint-editor/brush-controls.d.ts +7 -0
  45. package/dist/paint-editor/brush-model.d.cts +59 -1
  46. package/dist/paint-editor/brush-model.d.ts +59 -1
  47. package/dist/paint-editor/controller.d.cts +168 -1
  48. package/dist/paint-editor/controller.d.ts +168 -1
  49. package/dist/paint-editor/dab-stroke.d.cts +3 -0
  50. package/dist/paint-editor/dab-stroke.d.ts +3 -0
  51. package/dist/paint-editor/document.d.cts +13 -3
  52. package/dist/paint-editor/document.d.ts +13 -3
  53. package/dist/paint-editor/engine.d.cts +3 -0
  54. package/dist/paint-editor/engine.d.ts +3 -0
  55. package/dist/paint-editor/heat.d.cts +156 -0
  56. package/dist/paint-editor/heat.d.ts +156 -0
  57. package/dist/paint-editor/index.cjs +3386 -855
  58. package/dist/paint-editor/index.d.cts +3 -0
  59. package/dist/paint-editor/index.d.ts +3 -0
  60. package/dist/paint-editor/index.js +2731 -200
  61. package/dist/paint-editor/mutation-tools.d.cts +13 -0
  62. package/dist/paint-editor/mutation-tools.d.ts +13 -0
  63. package/dist/paint-editor/mutation.d.cts +67 -0
  64. package/dist/paint-editor/mutation.d.ts +67 -0
  65. package/dist/paint-editor/presence.d.cts +7 -1
  66. package/dist/paint-editor/presence.d.ts +7 -1
  67. package/dist/paint-editor/procedural.d.cts +3 -0
  68. package/dist/paint-editor/procedural.d.ts +3 -0
  69. package/dist/paint-editor/stroke.d.cts +32 -2
  70. package/dist/paint-editor/stroke.d.ts +32 -2
  71. package/dist/paint-editor/thick.d.cts +48 -7
  72. package/dist/paint-editor/thick.d.ts +48 -7
  73. package/dist/paint-editor/ui.d.cts +34 -1
  74. package/dist/paint-editor/ui.d.ts +34 -1
  75. package/dist/paint-editor/wet.d.cts +8 -1
  76. package/dist/paint-editor/wet.d.ts +8 -1
  77. package/dist/vector/index.cjs +999 -999
  78. package/dist/vector/index.js +5 -5
  79. package/package.json +1 -1
@@ -776,6 +776,69 @@ var BRUSH_PRESETS = [
776
776
  dynamics: { smoothing: 0.2, pressureSize: false, pressureOpacity: true },
777
777
  colour: { palette: ["#ef4444", "#f97316", "#facc15", "#22c55e", "#3b82f6", "#a855f7", "#111827", "#f8fafc"] }
778
778
  },
779
+ // --- Smart inks: UV-fluorescent and thermochromic ------------------------------------
780
+ // Invisible UV marker: invisible in normal light, glows brightly under the UV blacklight.
781
+ {
782
+ id: "uv-marker",
783
+ name: "UV marker",
784
+ kind: "mark",
785
+ medium: "ink",
786
+ variety: "uv-ink",
787
+ tip: { shape: "round", size: 18, hardness: 0.75, spacing: 0.06, opacity: 0.9 },
788
+ dynamics: { smoothing: 0.3, minSize: 0.5 },
789
+ fluorescence: { amount: 0.9, invisible: true },
790
+ colour: { default: "#22d3ee", palette: ["#22d3ee", "#a855f7", "#4ade80", "#f472b6"] }
791
+ },
792
+ // Thermochromic ink: shifts smoothly toward its hot colour where the canvas heat field warms it.
793
+ {
794
+ id: "thermo-ink",
795
+ name: "Thermochromic ink",
796
+ kind: "mark",
797
+ medium: "ink",
798
+ variety: "thermochromic",
799
+ tip: { shape: "round", size: 12, hardness: 0.85, spacing: 0.05 },
800
+ dynamics: { smoothing: 0.3, minSize: 0.4 },
801
+ thermochromic: { hotColor: "#ff7a18", threshold: 0.45 },
802
+ colour: { default: "#1e3a8a", palette: ["#1e3a8a", "#312e81", "#065f46", "#7f1d1d"] }
803
+ },
804
+ // --- Structural colour (thin-film iridescence) ---------------------------------------
805
+ // Scarab lacquer: thick metallic impasto under a high-index chitin-like thin film.
806
+ {
807
+ id: "scarab-lacquer",
808
+ name: "Scarab lacquer",
809
+ kind: "mark",
810
+ medium: "paint",
811
+ variety: "iridescent",
812
+ tip: { shape: "round", size: 22, hardness: 0.75, spacing: 0.06 },
813
+ dynamics: { smoothing: 0.25, minSize: 0.3 },
814
+ behaviour: { surface: "thick", thick: { thickness: 0.7, gloss: 0.85, metallic: 0.5 } },
815
+ iridescence: { thickness: 480, pressureToThickness: 0.35, ior: 1.65 },
816
+ colour: { default: "#065f46", palette: ["#065f46", "#1e3a8a", "#581c87", "#7c2d12", "#111827"] }
817
+ },
818
+ // Soap film: a soft glaze whose thin watery film shifts across the spectrum with pressure.
819
+ {
820
+ id: "soap-film",
821
+ name: "Soap film",
822
+ kind: "mark",
823
+ medium: "paint",
824
+ variety: "iridescent",
825
+ tip: { shape: "round", size: 32, hardness: 0.4, spacing: 0.06, opacity: 0.85, flow: 0.75 },
826
+ dynamics: { smoothing: 0.35, pressureOpacity: true, minSize: 0.35 },
827
+ iridescence: { thickness: 340, pressureToThickness: 0.8, ior: 1.33 },
828
+ colour: { default: { light: "#0284c7", dark: "#bae6fd" }, palette: [{ light: "#0284c7", dark: "#bae6fd" }, "#f472b6", "#a78bfa", "#34d399"] }
829
+ },
830
+ // Pearl glaze: a flat nacre glaze with gentle interference colour shifts.
831
+ {
832
+ id: "pearl-glaze",
833
+ name: "Pearl glaze",
834
+ kind: "mark",
835
+ medium: "paint",
836
+ variety: "iridescent",
837
+ tip: { shape: "flat", size: 28, hardness: 0.5, spacing: 0.06, angle: 25, roundness: 0.4, opacity: 0.9, flow: 0.85 },
838
+ dynamics: { smoothing: 0.3, rotation: "stroke", minSize: 0.4 },
839
+ iridescence: { thickness: 560, pressureToThickness: 0.25, ior: 1.55 },
840
+ colour: { default: { light: "#475569", dark: "#f1f5f9" }, palette: [{ light: "#475569", dark: "#f1f5f9" }, "#fbcfe8", "#ddd6fe", "#bae6fd", "#fef3c7"] }
841
+ },
779
842
  // --- Ghost paint: wet paint whose colour vanishes, leaving only what it smeared ---
780
843
  {
781
844
  id: "ghost-wash",
@@ -820,6 +883,28 @@ var BRUSH_PRESETS = [
820
883
  dynamics: { smoothing: 0.3, minSize: 0.4 },
821
884
  behaviour: { surface: "wet", wet: { water: 0.45, pigment: 0, rewet: 0.35 } },
822
885
  ghost: { smear: 0.65 }
886
+ },
887
+ // --- Mutation paint: strokes that sink away once claimed, and an agent answers ---
888
+ {
889
+ id: "mutation-wash",
890
+ name: "Mutation wash",
891
+ kind: "mark",
892
+ medium: "paint",
893
+ variety: "watercolor",
894
+ tip: { shape: "round", size: 28, hardness: 0.6, spacing: 0.12 },
895
+ dynamics: { smoothing: 0.25, pressureOpacity: true, minSize: 0.35 },
896
+ behaviour: { surface: "wet", wet: { water: 0.6, rewet: 0.2 } },
897
+ mutation: {}
898
+ },
899
+ {
900
+ id: "mutation-ink",
901
+ name: "Mutation ink",
902
+ kind: "mark",
903
+ medium: "ink",
904
+ variety: "pen",
905
+ tip: { shape: "round", size: 6, hardness: 0.9, spacing: 0.06 },
906
+ dynamics: { smoothing: 0.35, pressureSmoothing: 0.25, minSize: 0.25 },
907
+ mutation: {}
823
908
  }
824
909
  ];
825
910
 
@@ -942,11 +1027,51 @@ var DEFAULT_LIGHT = { amount: 0, pressure: false };
942
1027
  function normalizeLight(raw = {}, base = DEFAULT_LIGHT) {
943
1028
  return { amount: unit2(raw.amount, base.amount), pressure: typeof raw.pressure === "boolean" ? raw.pressure : base.pressure };
944
1029
  }
1030
+ var DEFAULT_FLUORESCENCE = { amount: 0, invisible: false };
1031
+ function normalizeFluorescence(raw = {}, base = DEFAULT_FLUORESCENCE) {
1032
+ if (typeof raw === "number") return { amount: unit2(raw, base.amount), invisible: base.invisible };
1033
+ return {
1034
+ amount: unit2(raw.amount, base.amount),
1035
+ invisible: typeof raw.invisible === "boolean" ? raw.invisible : base.invisible
1036
+ };
1037
+ }
1038
+ var DEFAULT_THERMOCHROMIC = { hotColor: null, threshold: 0.5 };
1039
+ function normalizeThermochromic(raw = {}, base = DEFAULT_THERMOCHROMIC) {
1040
+ const hotColor = raw.hotColor === null ? null : typeof raw.hotColor === "string" && HEX.test(raw.hotColor.trim()) ? raw.hotColor.trim().toLowerCase() : base.hotColor;
1041
+ return {
1042
+ hotColor,
1043
+ threshold: range(raw.threshold, 0.05, 0.95, base.threshold)
1044
+ };
1045
+ }
1046
+ var DEFAULT_IRIDESCENCE = { thickness: 400, pressureToThickness: 0, ior: 1.5 };
1047
+ var withIridescence = (ir) => ir ? { iridescence: ir } : {};
1048
+ function readIridescence(raw, base) {
1049
+ if (raw === null) return null;
1050
+ if (!raw || typeof raw !== "object") return base;
1051
+ const r = raw;
1052
+ const b = _nullishCoalesce(base, () => ( DEFAULT_IRIDESCENCE));
1053
+ return {
1054
+ thickness: range(r.thickness, 100, 1e3, b.thickness),
1055
+ pressureToThickness: unit2(r.pressureToThickness, b.pressureToThickness),
1056
+ ior: range(r.ior, 1.2, 2, b.ior)
1057
+ };
1058
+ }
1059
+ var GHOST_TRIGGERS = ["immediate", "agent"];
945
1060
  var DEFAULT_GHOST = { linger: 4, smear: 0.3 };
946
1061
  function normalizeGhost(raw = {}, base = DEFAULT_GHOST) {
1062
+ const trigger = raw.trigger === "agent" || raw.trigger === "immediate" ? raw.trigger : base.trigger;
947
1063
  return {
948
1064
  linger: range(raw.linger, 0.5, 60, base.linger),
949
- smear: unit2(raw.smear, base.smear)
1065
+ smear: unit2(raw.smear, base.smear),
1066
+ ...trigger === "agent" ? { trigger: "agent" } : {}
1067
+ };
1068
+ }
1069
+ var DEFAULT_MUTATION = { settle: 1.5, patience: 45, fade: 2 };
1070
+ function normalizeMutation(raw = {}, base = DEFAULT_MUTATION) {
1071
+ return {
1072
+ settle: range(raw.settle, 0.5, 10, base.settle),
1073
+ patience: range(raw.patience, 5, 300, base.patience),
1074
+ fade: range(raw.fade, 0.5, 10, base.fade)
950
1075
  };
951
1076
  }
952
1077
  function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
@@ -1139,8 +1264,12 @@ function normalizePaintBrush(raw = {}, base) {
1139
1264
  behavior = { ...behavior, multicolor: _nullishCoalesce(paintToMultiColor(paint), () => ( { ...DEFAULT_MULTI_COLOR, colors: [] })) };
1140
1265
  const wet = normalizeWetBrush(_nullishCoalesce(raw.wet, () => ( {})), _nullishCoalesce(b.wet, () => ( DEFAULT_WET_BRUSH)));
1141
1266
  const wantsLight = typeof _optionalChain([raw, 'access', _3 => _3.light, 'optionalAccess', _4 => _4.amount]) === "number" && raw.light.amount > 0;
1142
- const ghost = engine === "wet" && wet.tool === "paint" && (raw.ghost !== void 0 ? raw.ghost !== null : !wantsLight && Boolean(b.ghost)) ? normalizeGhost(_nullishCoalesce(raw.ghost, () => ( {})), _nullishCoalesce(b.ghost, () => ( DEFAULT_GHOST))) : void 0;
1143
- if (ghost) behavior = { ...behavior, pickup: 0, dirty: false };
1267
+ const canMutate = engine === "dab" || engine === "wet" && wet.tool === "paint" || engine === "impasto" || engine === "particle" || engine === "bristle";
1268
+ const mutation = canMutate && (raw.mutation !== void 0 ? raw.mutation !== null : Boolean(b.mutation)) ? normalizeMutation(_nullishCoalesce(raw.mutation, () => ( {})), _nullishCoalesce(b.mutation, () => ( DEFAULT_MUTATION))) : void 0;
1269
+ const hadAutoGhost = Boolean(b.mutation && raw.mutation === null && raw.ghost === void 0);
1270
+ const ghostBase = mutation ? { ..._nullishCoalesce(b.ghost, () => ( DEFAULT_GHOST)), linger: mutation.fade, trigger: "agent" } : _nullishCoalesce(b.ghost, () => ( DEFAULT_GHOST));
1271
+ const ghost = engine === "wet" && wet.tool === "paint" && !hadAutoGhost && (raw.ghost !== void 0 ? raw.ghost !== null : Boolean(mutation) || !wantsLight && Boolean(b.ghost)) ? normalizeGhost(_nullishCoalesce(raw.ghost, () => ( {})), ghostBase) : void 0;
1272
+ if (ghost || mutation) behavior = { ...behavior, pickup: 0, dirty: false };
1144
1273
  return {
1145
1274
  id,
1146
1275
  name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
@@ -1162,7 +1291,11 @@ function normalizePaintBrush(raw = {}, base) {
1162
1291
  bristle: normalizeBristle(_nullishCoalesce(raw.bristle, () => ( {})), _nullishCoalesce(b.bristle, () => ( DEFAULT_BRISTLE))),
1163
1292
  sticker: normalizeStickerSettings(_nullishCoalesce(raw.sticker, () => ( {})), _nullishCoalesce(b.sticker, () => ( DEFAULT_STICKER))),
1164
1293
  light: ghost && !wantsLight ? { ...DEFAULT_LIGHT } : normalizeLight(_nullishCoalesce(raw.light, () => ( {})), _nullishCoalesce(b.light, () => ( DEFAULT_LIGHT))),
1294
+ fluorescence: normalizeFluorescence(_nullishCoalesce(raw.fluorescence, () => ( {})), _nullishCoalesce(b.fluorescence, () => ( DEFAULT_FLUORESCENCE))),
1295
+ thermochromic: normalizeThermochromic(_nullishCoalesce(raw.thermochromic, () => ( {})), _nullishCoalesce(b.thermochromic, () => ( DEFAULT_THERMOCHROMIC))),
1296
+ ...withIridescence(readIridescence(raw.iridescence, _nullishCoalesce(b.iridescence, () => ( null)))),
1165
1297
  ...ghost ? { ghost } : {},
1298
+ ...mutation ? { mutation } : {},
1166
1299
  stamp: readStamp(raw.stamp, _nullishCoalesce(b.stamp, () => ( null))),
1167
1300
  spray: readSpray(raw.spray, _nullishCoalesce(b.spray, () => ( null))),
1168
1301
  ...withTipGrain(readTipGrain(raw.tipGrain, _nullishCoalesce(b.tipGrain, () => ( null)))),
@@ -1191,6 +1324,8 @@ var BASE_BRUSH = {
1191
1324
  bristle: { ...DEFAULT_BRISTLE },
1192
1325
  sticker: { ...DEFAULT_STICKER, stamps: [...DEFAULT_STICKER.stamps] },
1193
1326
  light: { ...DEFAULT_LIGHT },
1327
+ fluorescence: { ...DEFAULT_FLUORESCENCE },
1328
+ thermochromic: { ...DEFAULT_THERMOCHROMIC },
1194
1329
  stamp: null,
1195
1330
  spray: null,
1196
1331
  paint: null
@@ -1200,6 +1335,7 @@ var BASE_BRUSH = {
1200
1335
  var ALL = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
1201
1336
  var EXCHANGE = ["dab", "wet", "impasto"];
1202
1337
  var DABS = ["dab", "eraser", "mixer", "blender", "smudge"];
1338
+ var FILM = ["dab", "mixer", "wet", "impasto", "particle", "bristle"];
1203
1339
  var NOT = { toEngine: (v) => !v, toFacet: (v) => !v };
1204
1340
  var EDITOR_STAMPS = {
1205
1341
  toEngine: (v) => v.flatMap((ref) => {
@@ -1229,6 +1365,9 @@ var BRUSH_FIELDS = [
1229
1365
  f("tip.grain", "tipGrain", DABS),
1230
1366
  f("behaviour.grain", "grain"),
1231
1367
  ...each("light", "light", ["amount", "pressure"]),
1368
+ ...each("fluorescence", "fluorescence", ["amount", "invisible"]),
1369
+ ...each("thermochromic", "thermochromic", ["hotColor", "threshold"]),
1370
+ ...each("iridescence", "iridescence", ["thickness", "pressureToThickness", "ior"], FILM),
1232
1371
  // Paint exchange: the dab, wet and impasto engines' behaviour, or the Mixer Brush's settings.
1233
1372
  f("behaviour.exchange.pickup", "behavior.pickup", EXCHANGE),
1234
1373
  f("behaviour.exchange.mix", "behavior.mix", EXCHANGE),
@@ -1251,7 +1390,8 @@ var BRUSH_FIELDS = [
1251
1390
  f("behaviour.strength", "wet.strength", ["wet"]),
1252
1391
  // Surfaces.
1253
1392
  ...each("behaviour.wet", "wet", ["medium", "water", "pigment", "body", "rewet", "load", "thinner"], ["wet"]),
1254
- ...each("ghost", "ghost", ["linger", "smear"], ["wet"]),
1393
+ ...each("ghost", "ghost", ["linger", "smear", "trigger"], ["wet"]),
1394
+ ...each("mutation", "mutation", ["settle", "patience", "fade"], ["dab", "wet", "impasto", "particle", "bristle"]),
1255
1395
  f("behaviour.thick.applicator", "impasto.tool", ["impasto"]),
1256
1396
  ...each("behaviour.thick", "impasto", ["thickness", "gloss", "metallic", "load", "thinner", "drag", "rim", "clarity"], ["impasto"]),
1257
1397
  // Marks.
@@ -1343,12 +1483,14 @@ function resolveBrush(brush, options = {}) {
1343
1483
  if (v !== void 0) setPath(input, field.engine, field.map ? field.map.toEngine(v) : copy(v));
1344
1484
  }
1345
1485
  if (engine === "dab" && _optionalChain([brush, 'access', _21 => _21.marks, 'optionalAccess', _22 => _22.kind]) === "scatter") input.spray = { ..._nullishCoalesce(input.spray, () => ( {})) };
1486
+ if (brush.iridescence && FILM.includes(engine)) input.iridescence = { ..._nullishCoalesce(input.iridescence, () => ( {})) };
1346
1487
  if (engine === "wet") {
1347
1488
  setPath(input, "wet.tool", brush.kind === "tool" ? WET_TOOL_OF[brush.tool] : "paint");
1348
1489
  const medium = resolveWetMedium(brush);
1349
1490
  if (medium) setPath(input, "wet.medium", medium);
1350
1491
  if (brush.kind !== "tool" && brush.ghost) input.ghost = { ..._nullishCoalesce(input.ghost, () => ( {})) };
1351
1492
  }
1493
+ if (brush.kind !== "tool" && brush.mutation) input.mutation = { ..._nullishCoalesce(input.mutation, () => ( {})) };
1352
1494
  if (engine === "blender" || engine === "smudge") input.loadsColor = false;
1353
1495
  const paint = _optionalChain([brush, 'access', _23 => _23.colour, 'optionalAccess', _24 => _24.default]);
1354
1496
  if (paint && typeof paint === "object" && "type" in paint && paint.type === "gradient") input.paint = copy(paint);
@@ -1405,32 +1547,34 @@ function tipShape(b) {
1405
1547
  return Math.abs(Math.abs(b.tip.angle % 180) - 45) < 1e-9 || Math.abs(Math.abs(b.tip.angle % 180) - 135) < 1e-9 ? "chisel" : "flat";
1406
1548
  }
1407
1549
  function skeleton(b) {
1408
- const brush = { id: b.id, name: b.name, kind: "mark", tip: { shape: tipShape(b) } };
1409
- if (b.engine === "eraser" || b.engine === "blender" || b.engine === "smudge") return { ...brush, kind: "tool", tool: b.engine };
1550
+ const irid = b.iridescence ? { iridescence: {} } : {};
1551
+ const brush = { id: b.id, name: b.name, kind: "mark", tip: { shape: tipShape(b) }, ...irid };
1552
+ if (b.engine === "eraser" || b.engine === "blender" || b.engine === "smudge") return { id: b.id, name: b.name, kind: "tool", tool: b.engine, tip: { shape: tipShape(b) } };
1410
1553
  if (b.engine === "wet" && b.wet.tool !== "paint") {
1411
1554
  const tool = Object.keys(WET_TOOL_OF).find((t) => WET_TOOL_OF[t] === b.wet.tool);
1412
- return { ...brush, kind: "tool", tool };
1555
+ return { id: b.id, name: b.name, kind: "tool", tool, tip: { shape: tipShape(b) } };
1413
1556
  }
1557
+ const mut = b.mutation ? { mutation: {} } : {};
1414
1558
  switch (b.engine) {
1415
1559
  case "wet":
1416
- return { ...brush, medium: "paint", variety: b.wet.medium, behaviour: { surface: "wet" }, ...b.ghost ? { ghost: {} } : {} };
1560
+ return { ...brush, medium: "paint", variety: b.wet.medium, behaviour: { surface: "wet" }, ...b.ghost && !b.mutation ? { ghost: {} } : {}, ...mut };
1417
1561
  case "impasto":
1418
- return { ...brush, medium: "paint", behaviour: { surface: "thick" } };
1562
+ return { ...brush, medium: "paint", behaviour: { surface: "thick" }, ...mut };
1419
1563
  case "mixer":
1420
- return { ...brush, medium: "paint", variety: "mixer", behaviour: { exchange: { reservoir: {} } } };
1564
+ return { ...brush, medium: "paint", variety: "mixer", behaviour: { exchange: { reservoir: {} } }, ...mut };
1421
1565
  case "bristle":
1422
- return { ...brush, medium: "paint" };
1566
+ return { ...brush, medium: "paint", ...mut };
1423
1567
  case "particle":
1424
- return { ...brush, marks: { kind: "particles" } };
1568
+ return { ...brush, marks: { kind: "particles" }, ...mut };
1425
1569
  case "sticker":
1426
- return { ...brush, marks: { kind: "objects" } };
1570
+ return { id: b.id, name: b.name, kind: "mark", tip: { shape: tipShape(b) }, marks: { kind: "objects" }, ...mut };
1427
1571
  default: {
1428
1572
  const [medium, variety] = _nullishCoalesce(CATEGORY_MEDIUM[b.category], () => ( []));
1429
- return { ...brush, ...medium ? { medium } : {}, ...variety ? { variety } : {}, ...b.engine === "dab" && b.spray ? { marks: { kind: "scatter" } } : {} };
1573
+ return { ...brush, ...medium ? { medium } : {}, ...variety ? { variety } : {}, ...b.engine === "dab" && b.spray ? { marks: { kind: "scatter" } } : {}, ...mut };
1430
1574
  }
1431
1575
  }
1432
1576
  }
1433
- var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "ghost", "motion", "colour"];
1577
+ var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "motion", "colour"];
1434
1578
  function migrateBrushV1(raw) {
1435
1579
  const b = normalizePaintBrush(raw);
1436
1580
  const brush = skeleton(b);
@@ -1453,6 +1597,7 @@ var BRUSH_MARKS_KINDS = ["continuous", "scatter", "particles", "objects"];
1453
1597
  var BRUSH_SURFACES = ["dry", "wet", "thick"];
1454
1598
  var BRUSH_GRAIN_MODES = ["moving", "texturised"];
1455
1599
  var BRUSH_FLIPS = ["none", "x", "y", "xy"];
1600
+ var GHOST_TRIGGERS2 = ["immediate", "agent"];
1456
1601
 
1457
1602
  // src/brushes/schema.ts
1458
1603
  var MAX_BRUSH_PALETTE = 16;
@@ -1645,9 +1790,28 @@ var LIGHT = object("Light the brush gives off with its colour (glow and spill).
1645
1790
  }),
1646
1791
  glow: bool("Suggest a glow layer effect for the layer (glow stays a layer effect).")
1647
1792
  });
1793
+ var FLUORESCENCE = object("UV-fluorescent ink: glows in its colour under the virtual UV blacklight (set_uv_light); invisible in normal light when invisible is true.", {
1794
+ amount: unit3("Fluorescence intensity under UV blacklight, 0 (none) .. 1."),
1795
+ invisible: bool("Invisible ink in normal light: lays no daylight paint and glows only under UV (default false).")
1796
+ });
1797
+ var THERMOCHROMIC = object("Thermochromic ink: shifts smoothly toward hotColor where the canvas heat field warms it past threshold.", {
1798
+ hotColor: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "Colour (#rrggbb) the ink turns when heated." },
1799
+ threshold: num(0.05, 0.95, "Temperature 0.05..0.95 at which the colour shift is halfway (default 0.5).")
1800
+ });
1801
+ var IRIDESCENCE = object("Structural colour: a thin dielectric film over the paint whose interference hue shifts with the light and view tilt. Missing fields take their defaults.", {
1802
+ thickness: num(100, 1e3, "Film thickness in nanometres, 100..1000 (default 400)."),
1803
+ pressureToThickness: unit3("How much pen pressure shifts the film thickness per dab, 0..1 (default 0: uniform thickness)."),
1804
+ ior: num(1.2, 2, "Refractive index of the film, 1.2..2.0 (default 1.5; water/soap ~1.33, pearl ~1.55, beetle lacquer ~1.65).")
1805
+ });
1648
1806
  var GHOST = object('Ghost paint: wet paint that flows and mixes visibly while wet, then fades away without leaving its colour, keeping only the smear it made in the paint it touched (needs behaviour.surface "wet").', {
1649
1807
  linger: num(0.5, 60, "Seconds of simulated time before the ghost paint has fully faded (default 4)."),
1650
- smear: unit3("How much the ghost paint drags and lifts the paint it passes through: 0 none .. 1 full (default 0.3).")
1808
+ smear: unit3("How much the ghost paint drags and lifts the paint it passes through: 0 none .. 1 full (default 0.3)."),
1809
+ trigger: oneOf2(GHOST_TRIGGERS2, "When the fade starts: immediate (default) counts down as soon as the paint is laid; agent holds the fade until read_mutation claims the stroke.")
1810
+ });
1811
+ var MUTATION = object("Mutation paint: you paint something and pause; once an agent claims it with read_mutation, your paint sinks away and the agent paints its own answer (any wet or dry paint brush that lays its own colour without reading paint beneath).", {
1812
+ settle: num(0.5, 10, "Seconds after the last stroke before it counts as finished and raises a mutation request (default 1.5)."),
1813
+ patience: num(5, 300, "Seconds to wait for an agent before giving up and keeping the paint as ordinary paint (default 45)."),
1814
+ fade: num(0.5, 10, "Seconds the paint takes to sink away once claimed by read_mutation (default 2).")
1651
1815
  });
1652
1816
  var MOTION = object("Animation after painting.", {
1653
1817
  live: bool("Effect paint keeps playing on stroke layers (default false)."),
@@ -1712,7 +1876,11 @@ var BRUSH_JSON_SCHEMA = object(
1712
1876
  dynamics: DYNAMICS,
1713
1877
  behaviour: BEHAVIOUR,
1714
1878
  light: LIGHT,
1879
+ fluorescence: FLUORESCENCE,
1880
+ thermochromic: THERMOCHROMIC,
1881
+ iridescence: IRIDESCENCE,
1715
1882
  ghost: GHOST,
1883
+ mutation: MUTATION,
1716
1884
  motion: MOTION,
1717
1885
  colour: COLOUR
1718
1886
  },
@@ -1725,7 +1893,11 @@ var BRUSH_FACET_SCHEMAS = {
1725
1893
  dynamics: DYNAMICS,
1726
1894
  behaviour: BEHAVIOUR,
1727
1895
  light: LIGHT,
1896
+ fluorescence: FLUORESCENCE,
1897
+ thermochromic: THERMOCHROMIC,
1898
+ iridescence: IRIDESCENCE,
1728
1899
  ghost: GHOST,
1900
+ mutation: MUTATION,
1729
1901
  motion: MOTION,
1730
1902
  colour: COLOUR,
1731
1903
  paint: PAINT
@@ -1968,11 +2140,13 @@ function normalizeBrushSpec(raw, problems = []) {
1968
2140
  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).`);
1969
2141
  if (kind === "tool" && !tool) throw new Error(`Brush "${raw.id}" is a tool and needs tool: one of ${BRUSH_TOOLS.join(", ")}.`);
1970
2142
  const { colour: rawColour, marks: rawMarks, kind: _k, id: _i, name: _n, tool: _t, ...rest } = raw;
2143
+ if (typeof rest.fluorescence === "number" && Number.isFinite(rest.fluorescence)) rest.fluorescence = { amount: rest.fluorescence };
1971
2144
  const out = readObject(rest, BRUSH_JSON_SCHEMA, "brush", problems);
2145
+ if (_optionalChain([out, 'access', _29 => _29.thermochromic, 'optionalAccess', _30 => _30.hotColor])) out.thermochromic.hotColor = out.thermochromic.hotColor.toLowerCase();
1972
2146
  const brush = { id: raw.id, name, ...out, kind, ...tool ? { tool } : {}, tip: _nullishCoalesce(out.tip, () => ( { shape: "round" })) };
1973
2147
  const ordered = () => {
1974
- const { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, ghost, motion, colour } = brush;
1975
- const all = { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, ghost, motion, colour };
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 };
1976
2150
  return Object.fromEntries(Object.entries(all).filter(([, v]) => v !== void 0));
1977
2151
  };
1978
2152
  if (!out.tip) problems.push("tip is missing; a round tip is used.");
@@ -2020,16 +2194,24 @@ function normalizeBrushSpec(raw, problems = []) {
2020
2194
  drop("marks", "tools lay no marks");
2021
2195
  delete brush.marks;
2022
2196
  }
2197
+ if (brush.iridescence) {
2198
+ drop("iridescence", "tools lay no film");
2199
+ delete brush.iridescence;
2200
+ }
2201
+ }
2202
+ if (brush.iridescence && _optionalChain([brush, 'access', _31 => _31.marks, 'optionalAccess', _32 => _32.kind]) === "objects") {
2203
+ drop("iridescence", "object marks are stickers, not film");
2204
+ delete brush.iridescence;
2023
2205
  }
2024
- if (_optionalChain([brush, 'access', _29 => _29.marks, 'optionalAccess', _30 => _30.kind]) === "scatter") {
2025
- const surface = _optionalChain([brush, 'access', _31 => _31.behaviour, 'optionalAccess', _32 => _32.surface]);
2026
- const why = brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : surface === "wet" || surface === "thick" ? `${surface} paint is not sprayed` : _optionalChain([brush, 'access', _33 => _33.behaviour, 'optionalAccess', _34 => _34.exchange, 'optionalAccess', _35 => _35.reservoir]) ? "the Mixer Brush is not sprayed" : null;
2206
+ if (_optionalChain([brush, 'access', _33 => _33.marks, 'optionalAccess', _34 => _34.kind]) === "scatter") {
2207
+ const surface = _optionalChain([brush, 'access', _35 => _35.behaviour, 'optionalAccess', _36 => _36.surface]);
2208
+ const why = brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : surface === "wet" || surface === "thick" ? `${surface} paint is not sprayed` : _optionalChain([brush, 'access', _37 => _37.behaviour, 'optionalAccess', _38 => _38.exchange, 'optionalAccess', _39 => _39.reservoir]) ? "the Mixer Brush is not sprayed" : null;
2027
2209
  if (why) {
2028
2210
  drop("marks", why);
2029
2211
  delete brush.marks;
2030
2212
  }
2031
2213
  }
2032
- if (_optionalChain([brush, 'access', _36 => _36.marks, 'optionalAccess', _37 => _37.kind]) === "objects" && brush.tip.shape !== "stamp") {
2214
+ if (_optionalChain([brush, 'access', _40 => _40.marks, 'optionalAccess', _41 => _41.kind]) === "objects" && brush.tip.shape !== "stamp") {
2033
2215
  problems.push("marks objects lay stamps: tip.shape set to stamp.");
2034
2216
  brush.tip.shape = "stamp";
2035
2217
  }
@@ -2043,7 +2225,7 @@ function normalizeBrushSpec(raw, problems = []) {
2043
2225
  }
2044
2226
  if (brush.tip.stamps) {
2045
2227
  let stamps = brush.tip.stamps.map((s) => _chunkRDFHWAHLcjs.normalizeStickerRef.call(void 0, s));
2046
- if (_optionalChain([brush, 'access', _38 => _38.marks, 'optionalAccess', _39 => _39.kind]) === "objects" && stamps.some(isBrushSourceRef)) {
2228
+ if (_optionalChain([brush, 'access', _42 => _42.marks, 'optionalAccess', _43 => _43.kind]) === "objects" && stamps.some(isBrushSourceRef)) {
2047
2229
  problems.push("tip.stamps: image sources (source:\u2026) dropped: objects marks lay stickers; use continuous, scatter or particles marks for an image tip.");
2048
2230
  stamps = stamps.filter((s) => !isBrushSourceRef(s));
2049
2231
  }
@@ -2054,8 +2236,8 @@ function normalizeBrushSpec(raw, problems = []) {
2054
2236
  }
2055
2237
  }
2056
2238
  if (brush.tip.grain) {
2057
- const surface = _optionalChain([brush, 'access', _40 => _40.behaviour, 'optionalAccess', _41 => _41.surface]);
2058
- const marks = _optionalChain([brush, 'access', _42 => _42.marks, 'optionalAccess', _43 => _43.kind]);
2239
+ const surface = _optionalChain([brush, 'access', _44 => _44.behaviour, 'optionalAccess', _45 => _45.surface]);
2240
+ const marks = _optionalChain([brush, 'access', _46 => _46.marks, 'optionalAccess', _47 => _47.kind]);
2059
2241
  const why = !brush.tip.grain.source ? "it needs a source (a grain pattern or source:<id>)" : brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : marks === "particles" || marks === "objects" ? `${marks} marks are not dabs` : surface === "wet" || surface === "thick" ? `${surface} paint lays its own texture` : kind === "tool" && WET_BRUSH_TOOLS.includes(brush.tool) ? "the wet tools work on the wet layer" : null;
2060
2242
  if (why) {
2061
2243
  drop("tip.grain", why);
@@ -2092,13 +2274,13 @@ function normalizeBrushSpec(raw, problems = []) {
2092
2274
  if (b.exchange && kind === "tool" && !wetTool) {
2093
2275
  drop("behaviour.exchange", `the ${brush.tool} does not pick paint up`);
2094
2276
  delete b.exchange;
2095
- } else if (b.exchange && (_optionalChain([brush, 'access', _44 => _44.marks, 'optionalAccess', _45 => _45.kind]) === "particles" || _optionalChain([brush, 'access', _46 => _46.marks, 'optionalAccess', _47 => _47.kind]) === "objects" || brush.tip.shape === "bristle")) {
2277
+ } else if (b.exchange && (_optionalChain([brush, 'access', _48 => _48.marks, 'optionalAccess', _49 => _49.kind]) === "particles" || _optionalChain([brush, 'access', _50 => _50.marks, 'optionalAccess', _51 => _51.kind]) === "objects" || brush.tip.shape === "bristle")) {
2096
2278
  drop("behaviour.exchange", "particle, object and bristle marks do not pick paint up");
2097
2279
  delete b.exchange;
2098
2280
  }
2099
2281
  }
2100
2282
  if (brush.ghost) {
2101
- if (kind === "tool" || _optionalChain([brush, 'access', _48 => _48.behaviour, 'optionalAccess', _49 => _49.surface]) !== "wet") {
2283
+ if (kind === "tool" || _optionalChain([brush, 'access', _52 => _52.behaviour, 'optionalAccess', _53 => _53.surface]) !== "wet") {
2102
2284
  drop("ghost", "it needs a mark brush with behaviour.surface wet");
2103
2285
  delete brush.ghost;
2104
2286
  } else {
@@ -2106,7 +2288,7 @@ function normalizeBrushSpec(raw, problems = []) {
2106
2288
  drop("light", "ghost paint uses the wet light channel as its tracer");
2107
2289
  delete brush.light;
2108
2290
  }
2109
- if (_optionalChain([brush, 'access', _50 => _50.behaviour, 'optionalAccess', _51 => _51.exchange])) {
2291
+ if (_optionalChain([brush, 'access', _54 => _54.behaviour, 'optionalAccess', _55 => _55.exchange])) {
2110
2292
  drop("behaviour.exchange", "ghost paint does not pick paint up");
2111
2293
  delete brush.behaviour.exchange;
2112
2294
  }
@@ -2135,7 +2317,8 @@ var PAINT_PROFILE = {
2135
2317
  velocity: "full",
2136
2318
  tilt: "full",
2137
2319
  tipGrain: "full",
2138
- dabCount: "full"
2320
+ dabCount: "full",
2321
+ iridescence: "full"
2139
2322
  };
2140
2323
  var IMAGE_PROFILE = {
2141
2324
  id: "image",
@@ -2154,7 +2337,8 @@ var IMAGE_PROFILE = {
2154
2337
  velocity: "ignored",
2155
2338
  tilt: "ignored",
2156
2339
  tipGrain: "ignored",
2157
- dabCount: "ignored"
2340
+ dabCount: "ignored",
2341
+ iridescence: "none"
2158
2342
  };
2159
2343
  var CANVAS_PROFILE = {
2160
2344
  id: "canvas",
@@ -2173,7 +2357,8 @@ var CANVAS_PROFILE = {
2173
2357
  velocity: "ignored",
2174
2358
  tilt: "full",
2175
2359
  tipGrain: "ignored",
2176
- dabCount: "ignored"
2360
+ dabCount: "ignored",
2361
+ iridescence: "none"
2177
2362
  };
2178
2363
  var VECTOR_PROFILE = { ...CANVAS_PROFILE, id: "vector", name: "Vector editor", kinds: ["mark", "tool"], tools: ["blender", "smudge"], surfaces: ["dry", "wet"], reservoir: "full", light: "ignored" };
2179
2364
  var BRUSH_HOST_PROFILES = { paint: PAINT_PROFILE, image: IMAGE_PROFILE, canvas: CANVAS_PROFILE, vector: VECTOR_PROFILE };
@@ -2201,16 +2386,28 @@ function checkBrush(brush, profile) {
2201
2386
  const image = brush.tip.shape === "stamp" && (_nullishCoalesce(brush.tip.stamps, () => ( []))).some(isBrushSourceRef);
2202
2387
  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)}.`);
2203
2388
  }
2204
- const marks = _nullishCoalesce(_optionalChain([brush, 'access', _52 => _52.marks, 'optionalAccess', _53 => _53.kind]), () => ( "continuous"));
2389
+ const marks = _nullishCoalesce(_optionalChain([brush, 'access', _56 => _56.marks, 'optionalAccess', _57 => _57.kind]), () => ( "continuous"));
2205
2390
  if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
2206
- const surface = _nullishCoalesce(_optionalChain([brush, 'access', _54 => _54.behaviour, 'optionalAccess', _55 => _55.surface]), () => ( "dry"));
2391
+ const surface = _nullishCoalesce(_optionalChain([brush, 'access', _58 => _58.behaviour, 'optionalAccess', _59 => _59.surface]), () => ( "dry"));
2207
2392
  if (brush.kind === "mark" && !profile.surfaces.includes(surface)) add("behaviour.surface", "error", `${host} has no ${surface} surface.`, `Set behaviour.surface to ${list(profile.surfaces)}.`);
2208
- const ex = _optionalChain([brush, 'access', _56 => _56.behaviour, 'optionalAccess', _57 => _57.exchange]);
2209
- feature(profile.reservoir, !!_optionalChain([ex, 'optionalAccess', _58 => _58.reservoir]), "behaviour.exchange.reservoir", "a Mixer Brush reservoir", "Remove behaviour.exchange.reservoir.");
2210
- feature(profile.exchange, !_optionalChain([ex, 'optionalAccess', _59 => _59.reservoir]) && (nonZero(_optionalChain([ex, 'optionalAccess', _60 => _60.pickup])) || !!_optionalChain([ex, 'optionalAccess', _61 => _61.dirty])), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
2211
- feature(profile.grain, nonZero(_optionalChain([brush, 'access', _62 => _62.behaviour, 'optionalAccess', _63 => _63.grain])), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
2212
- feature(profile.light, nonZero(_optionalChain([brush, 'access', _64 => _64.light, 'optionalAccess', _65 => _65.amount])), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
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).");
2398
+ 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.");
2401
+ feature(_nullishCoalesce(profile.iridescence, () => ( "none")), !!brush.iridescence, "iridescence", "thin-film iridescence", "Remove iridescence.");
2213
2402
  feature(profile.surfaces.includes("wet") && profile.light === "full" ? "full" : "none", !!brush.ghost, "ghost", "ghost paint", "Remove ghost.");
2403
+ if (brush.mutation) {
2404
+ if (profile.id !== "paint") feature("none", true, "mutation", "mutation paint", "Remove mutation.");
2405
+ 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.");
2410
+ }
2214
2411
  const m = brush.motion;
2215
2412
  feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
2216
2413
  const d = _nullishCoalesce(brush.dynamics, () => ( {}));
@@ -2219,16 +2416,16 @@ function checkBrush(brush, profile) {
2219
2416
  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.");
2220
2417
  feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
2221
2418
  const colour = brush.colour;
2222
- if (_optionalChain([colour, 'optionalAccess', _66 => _66.default]) !== void 0) {
2419
+ if (_optionalChain([colour, 'optionalAccess', _81 => _81.default]) !== void 0) {
2223
2420
  const kind = brushColourKind(colour.default);
2224
2421
  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.`);
2225
2422
  }
2226
- const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _67 => _67.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
2423
+ const foreign = [...new Set((_nullishCoalesce(_optionalChain([colour, 'optionalAccess', _82 => _82.palette]), () => ( []))).map(brushColourKind).filter((k) => !profile.colours.includes(k)))];
2227
2424
  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.`);
2228
2425
  return { ok: !issues.some((i) => i.severity === "error"), issues };
2229
2426
  }
2230
2427
  function withKeptIssues(check, before) {
2231
- const had = new Set((_nullishCoalesce(_optionalChain([before, 'optionalAccess', _68 => _68.issues]), () => ( []))).filter((i) => i.severity === "error").map((i) => i.facet));
2428
+ const had = new Set((_nullishCoalesce(_optionalChain([before, 'optionalAccess', _83 => _83.issues]), () => ( []))).filter((i) => i.severity === "error").map((i) => i.facet));
2232
2429
  const kept = check.issues.filter((i) => i.severity === "error" && had.has(i.facet));
2233
2430
  const issues = check.issues.filter((i) => !kept.includes(i));
2234
2431
  return { ok: !issues.some((i) => i.severity === "error"), issues, kept };
@@ -2266,6 +2463,9 @@ function degrade(brush, profile, options = {}) {
2266
2463
  case "ghost":
2267
2464
  delete b.ghost;
2268
2465
  break;
2466
+ case "mutation":
2467
+ delete b.mutation;
2468
+ break;
2269
2469
  case "behaviour.exchange.reservoir":
2270
2470
  delete b.behaviour.exchange.reservoir;
2271
2471
  break;
@@ -2284,6 +2484,15 @@ function degrade(brush, profile, options = {}) {
2284
2484
  case "light":
2285
2485
  delete b.light;
2286
2486
  break;
2487
+ case "fluorescence":
2488
+ delete b.fluorescence;
2489
+ break;
2490
+ case "thermochromic":
2491
+ delete b.thermochromic;
2492
+ break;
2493
+ case "iridescence":
2494
+ delete b.iridescence;
2495
+ break;
2287
2496
  case "motion":
2288
2497
  delete b.motion;
2289
2498
  break;
@@ -2308,7 +2517,7 @@ function degrade(brush, profile, options = {}) {
2308
2517
  }
2309
2518
  dropped.push(issue);
2310
2519
  }
2311
- if (_optionalChain([b, 'access', _69 => _69.behaviour, 'optionalAccess', _70 => _70.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
2520
+ if (_optionalChain([b, 'access', _84 => _84.behaviour, 'optionalAccess', _85 => _85.exchange]) && !Object.keys(b.behaviour.exchange).length) delete b.behaviour.exchange;
2312
2521
  if (b.colour && !Object.keys(b.colour).length) delete b.colour;
2313
2522
  const after = checkBrush(b, profile).issues.filter((i) => i.severity === "error");
2314
2523
  return { brush: b, dropped, issues: after };
@@ -2352,7 +2561,7 @@ function parseBrushLibrary2(raw, problems = []) {
2352
2561
  if (file && file.version !== void 0 && file.version !== 1 && file.version !== 2) {
2353
2562
  throw new BrushLibraryError("invalid", `Brush library version ${JSON.stringify(file.version)} is not supported; this library reads versions 1 and 2.`);
2354
2563
  }
2355
- const v1File = _optionalChain([file, 'optionalAccess', _71 => _71.version]) === 1;
2564
+ const v1File = _optionalChain([file, 'optionalAccess', _86 => _86.version]) === 1;
2356
2565
  const out = [];
2357
2566
  items.forEach((item, i) => {
2358
2567
  const at = `brushes[${i}]`;
@@ -2613,7 +2822,7 @@ function snapshotBrushLibrary(library, onChange) {
2613
2822
  (list2) => {
2614
2823
  entries = list2;
2615
2824
  user = list2.filter((e) => !e.builtin).map((e) => e.brush);
2616
- _optionalChain([onChange, 'optionalCall', _72 => _72()]);
2825
+ _optionalChain([onChange, 'optionalCall', _87 => _87()]);
2617
2826
  },
2618
2827
  () => void 0
2619
2828
  );
@@ -2746,4 +2955,13 @@ function snapshotBrushLibrary(library, onChange) {
2746
2955
 
2747
2956
 
2748
2957
 
2749
- 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_GHOST = DEFAULT_GHOST; exports.normalizeGhost = normalizeGhost; 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.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;
2958
+
2959
+
2960
+
2961
+
2962
+
2963
+
2964
+
2965
+
2966
+
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;
@@ -5,7 +5,7 @@ import {
5
5
  moveAnchorHandle,
6
6
  subpathSegment,
7
7
  subpathSegmentCount
8
- } from "./chunk-ZEI7ZEA4.js";
8
+ } from "./chunk-Y2ISSVPH.js";
9
9
  import {
10
10
  segmentPoint
11
11
  } from "./chunk-WEIK3UYB.js";
@@ -74,7 +74,7 @@ function profileAlongPolyline(points, values, count = 32) {
74
74
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
75
75
  var loading = null;
76
76
  function loadCanvasBrushstrokeRenderer() {
77
- loading ??= Promise.resolve().then(() => _interopRequireWildcard(require("./brushstrokeRender-5YBCLFDQ.cjs"))).catch((error) => {
77
+ loading ??= Promise.resolve().then(() => _interopRequireWildcard(require("./brushstrokeRender-2RQL46WB.cjs"))).catch((error) => {
78
78
  loading = null;
79
79
  throw error;
80
80
  });