@pixerate/editor 0.256.0 → 0.258.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 (124) hide show
  1. package/dist/agent/index.cjs +40 -38
  2. package/dist/agent/index.js +9 -7
  3. package/dist/animation/index.cjs +4 -2
  4. package/dist/animation/index.js +4 -2
  5. package/dist/animation-editor/index.cjs +63 -61
  6. package/dist/animation-editor/index.js +7 -5
  7. package/dist/brushes/colour.d.cts +11 -0
  8. package/dist/brushes/colour.d.ts +11 -0
  9. package/dist/brushes/index.cjs +51 -30
  10. package/dist/brushes/index.d.cts +1 -0
  11. package/dist/brushes/index.d.ts +1 -0
  12. package/dist/brushes/index.js +25 -4
  13. package/dist/brushes/resolve.d.cts +2 -0
  14. package/dist/brushes/resolve.d.ts +2 -0
  15. package/dist/brushes/schema.d.cts +1 -0
  16. package/dist/brushes/schema.d.ts +1 -0
  17. package/dist/brushes/tools.d.cts +7 -0
  18. package/dist/brushes/tools.d.ts +7 -0
  19. package/dist/brushes/types.d.cts +10 -2
  20. package/dist/brushes/types.d.ts +10 -2
  21. package/dist/brushes/ui.d.cts +14 -3
  22. package/dist/brushes/ui.d.ts +14 -3
  23. package/dist/{brushstrokeRender-HCOQUSWW.js → brushstrokeRender-JI4W6F2G.js} +2 -2
  24. package/dist/{brushstrokeRender-ZZOXI5O5.cjs → brushstrokeRender-MQ6PSDM5.cjs} +11 -11
  25. package/dist/canvas/index.cjs +20 -18
  26. package/dist/canvas/index.js +7 -5
  27. package/dist/chart/index.cjs +159 -157
  28. package/dist/chart/index.js +9 -7
  29. package/dist/{chunk-7272D3K2.js → chunk-2EUW2XZX.js} +27 -276
  30. package/dist/{chunk-L6Y7TSQ4.js → chunk-2VJ6OVJV.js} +1 -1
  31. package/dist/{chunk-6ZZKRH7R.js → chunk-47V3WXDG.js} +2 -2
  32. package/dist/{chunk-H7ET2ISK.cjs → chunk-4IMD2TLY.cjs} +52 -52
  33. package/dist/{chunk-VUKUTT2W.js → chunk-4M4E3N6X.js} +1 -1
  34. package/dist/{chunk-TBAVCLEQ.cjs → chunk-5Q5MLXAD.cjs} +23 -23
  35. package/dist/{chunk-MONXNVZK.js → chunk-6I23ZCGK.js} +5 -3
  36. package/dist/{chunk-GOBOCZZI.js → chunk-6NQPKNUZ.js} +1 -1
  37. package/dist/{chunk-WYGSR3IJ.js → chunk-AORZ5M5C.js} +1 -1
  38. package/dist/chunk-AVTPZO4Z.cjs +142 -0
  39. package/dist/{chunk-WKMUBATU.js → chunk-C2WEIEUB.js} +1 -1
  40. package/dist/{chunk-P3K2JCJA.js → chunk-D5BXSOSU.js} +3 -3
  41. package/dist/chunk-E2V5DA3W.js +36 -0
  42. package/dist/{chunk-WZOGDKCQ.js → chunk-F7CPSRLN.js} +0 -133
  43. package/dist/chunk-FJBQTZS4.js +17 -0
  44. package/dist/{chunk-SOSBOS5T.cjs → chunk-G23BTQA5.cjs} +22 -22
  45. package/dist/{chunk-EUIDTZNQ.cjs → chunk-I3Y2Q3PM.cjs} +59 -308
  46. package/dist/{chunk-YHUMGBZD.js → chunk-ICTPLVSR.js} +22 -4
  47. package/dist/{chunk-LRNUZKWV.cjs → chunk-IPUTIUGQ.cjs} +1 -1
  48. package/dist/{chunk-6GRUVGKU.js → chunk-ITURMC2M.js} +3 -3
  49. package/dist/{chunk-HYWRU5VX.js → chunk-IXQZHCOF.js} +1 -1
  50. package/dist/chunk-J5UQHWVL.js +142 -0
  51. package/dist/{chunk-YBAPT7NL.js → chunk-JC2SHA4A.js} +1 -23
  52. package/dist/chunk-LRTR4ASZ.cjs +17 -0
  53. package/dist/{chunk-HUAISCUX.cjs → chunk-M2HG5DJ2.cjs} +18 -18
  54. package/dist/{chunk-N76AMOKE.cjs → chunk-MDLEGVEN.cjs} +42 -42
  55. package/dist/chunk-MEGSQUQO.cjs +36 -0
  56. package/dist/chunk-MLLTWAXH.cjs +276 -0
  57. package/dist/{chunk-RHPSE2YR.js → chunk-MQ6MGIQM.js} +2 -2
  58. package/dist/{chunk-PGPFWC35.cjs → chunk-MQGQTKVW.cjs} +51 -33
  59. package/dist/{chunk-5YWWQ36H.cjs → chunk-O3ZVZSNJ.cjs} +9 -9
  60. package/dist/{chunk-GCTUOTCW.cjs → chunk-P3PZ63M3.cjs} +35 -35
  61. package/dist/{chunk-WRZ6C4TU.cjs → chunk-P6WYDBTH.cjs} +4 -4
  62. package/dist/{chunk-CJ6TBL2N.cjs → chunk-RHEMMGCN.cjs} +16 -16
  63. package/dist/{chunk-HIMGBZAM.cjs → chunk-RSIKDWOP.cjs} +201 -38
  64. package/dist/{chunk-SABJMH6M.cjs → chunk-RVGP2XLF.cjs} +2 -135
  65. package/dist/{chunk-JVRLGV3C.js → chunk-SMTUQWGY.js} +184 -21
  66. package/dist/{chunk-DMTEQ6SH.js → chunk-SSFMWJDK.js} +4 -4
  67. package/dist/{chunk-TFF2GTGN.cjs → chunk-SXQHWXEL.cjs} +2 -24
  68. package/dist/{chunk-IWEVV63O.cjs → chunk-UEYLANO3.cjs} +18 -16
  69. package/dist/chunk-VR6OGHU5.js +276 -0
  70. package/dist/{chunk-AF23QK5F.js → chunk-YQ4PQTBB.js} +1 -1
  71. package/dist/{chunk-B7V34GIH.cjs → chunk-YSGW7VV4.cjs} +88 -88
  72. package/dist/{chunk-3NS5C7FB.cjs → chunk-YYBW6DF2.cjs} +76 -76
  73. package/dist/{chunk-UFEDHACQ.js → chunk-Z7BX4TWO.js} +2 -2
  74. package/dist/{chunk-RUONPPMC.cjs → chunk-ZSE25IZA.cjs} +8 -8
  75. package/dist/color/index.cjs +7 -3
  76. package/dist/color/index.js +18 -14
  77. package/dist/export/index.cjs +8 -6
  78. package/dist/export/index.js +7 -5
  79. package/dist/extensions/index.cjs +11 -9
  80. package/dist/extensions/index.js +10 -8
  81. package/dist/gradient/index.cjs +33 -31
  82. package/dist/gradient/index.js +5 -3
  83. package/dist/image-editor/brush-presets.d.cts +4 -1
  84. package/dist/image-editor/brush-presets.d.ts +4 -1
  85. package/dist/image-editor/controller.d.cts +10 -5
  86. package/dist/image-editor/controller.d.ts +10 -5
  87. package/dist/image-editor/index.cjs +1090 -1073
  88. package/dist/image-editor/index.js +37 -20
  89. package/dist/index.cjs +17 -15
  90. package/dist/index.js +11 -9
  91. package/dist/paint-editor/brushes.d.cts +18 -1
  92. package/dist/paint-editor/brushes.d.ts +18 -1
  93. package/dist/paint-editor/collaboration.d.cts +38 -0
  94. package/dist/paint-editor/collaboration.d.ts +38 -0
  95. package/dist/paint-editor/controller.d.cts +60 -0
  96. package/dist/paint-editor/controller.d.ts +60 -0
  97. package/dist/paint-editor/engine.d.cts +7 -0
  98. package/dist/paint-editor/engine.d.ts +7 -0
  99. package/dist/paint-editor/index.cjs +1564 -1324
  100. package/dist/paint-editor/index.js +1198 -958
  101. package/dist/paint-editor/mark-settings.d.cts +1 -1
  102. package/dist/paint-editor/mark-settings.d.ts +1 -1
  103. package/dist/paint-editor/procedural.d.cts +4 -2
  104. package/dist/paint-editor/procedural.d.ts +4 -2
  105. package/dist/paint-editor/stamp-tip.d.cts +45 -0
  106. package/dist/paint-editor/stamp-tip.d.ts +45 -0
  107. package/dist/paint-editor/stroke.d.cts +4 -0
  108. package/dist/paint-editor/stroke.d.ts +4 -0
  109. package/dist/stickers/index.cjs +4 -2
  110. package/dist/stickers/index.js +6 -4
  111. package/dist/typography/index.cjs +3 -2
  112. package/dist/typography/index.js +2 -1
  113. package/dist/vector/index.cjs +971 -969
  114. package/dist/vector/index.js +8 -6
  115. package/dist/video-editor/index.cjs +4 -2
  116. package/dist/video-editor/index.js +4 -2
  117. package/package.json +1 -1
  118. package/skills/pixerate-brushes/SKILL.md +21 -11
  119. package/skills/pixerate-brushes-design/SKILL.md +6 -2
  120. package/skills/pixerate-brushes-design/evals/canvas-chalk.json +8 -8
  121. package/skills/pixerate-canvas/SKILL.md +3 -2
  122. package/skills/pixerate-image-editor/SKILL.md +1 -1
  123. package/skills/pixerate-paint-editor/SKILL.md +5 -1
  124. package/skills/pixerate-vector/SKILL.md +1 -1
@@ -75,7 +75,13 @@ var DEFAULT_PARTICLE = {
75
75
  dotRotation: "random"
76
76
  };
77
77
  function normalizeParticle(raw = {}, base = DEFAULT_PARTICLE) {
78
- const textures = Array.isArray(raw.dotTextures) ? raw.dotTextures.filter((t) => STAMP_KINDS.includes(t)).slice(0, MAX_DOT_TEXTURES) : [...base.dotTextures ?? []];
78
+ const texture = (t) => {
79
+ if (typeof t !== "string") return null;
80
+ const bare = t.startsWith("editor:") ? t.slice(7) : t;
81
+ if (STAMP_KINDS.includes(bare)) return bare;
82
+ return /^[^:\s]+:\S+$/.test(t) && t.length <= 200 ? t : null;
83
+ };
84
+ const textures = Array.isArray(raw.dotTextures) ? raw.dotTextures.map(texture).filter((t) => t !== null).slice(0, MAX_DOT_TEXTURES) : [...base.dotTextures ?? []];
79
85
  return {
80
86
  kind: oneOf(raw.kind, PARTICLE_KINDS, base.kind),
81
87
  render: oneOf(raw.render, PARTICLE_RENDERS, base.render),
@@ -204,6 +210,20 @@ function normalizeBehavior(raw = {}, base = DEFAULT_BEHAVIOR) {
204
210
  multicolor: normalizeMultiColor(raw.multicolor ?? {}, base.multicolor)
205
211
  };
206
212
  }
213
+ var DEFAULT_SPRAY = { density: 0.5, size: 0.12 };
214
+ function readStamp(raw, base) {
215
+ if (raw === null) return null;
216
+ if (typeof raw !== "string") return base;
217
+ if (STAMP_KINDS.includes(raw)) return `editor:${raw}`;
218
+ return /^[^:\s]+:\S+$/.test(raw) && raw.length <= 200 ? raw : base;
219
+ }
220
+ function readSpray(raw, base) {
221
+ if (raw === null) return null;
222
+ if (!raw || typeof raw !== "object") return base;
223
+ const r = raw;
224
+ const b = base ?? DEFAULT_SPRAY;
225
+ return { density: unit2(r.density, b.density), size: range(r.size, 0.02, 1, b.size) };
226
+ }
207
227
  function isWetCapable(brush) {
208
228
  return brush.engine === "dab" || brush.engine === "mixer" || brush.engine === "wet" || brush.engine === "impasto";
209
229
  }
@@ -305,6 +325,8 @@ function normalizePaintBrush(raw = {}, base) {
305
325
  bristle: normalizeBristle(raw.bristle ?? {}, b.bristle ?? DEFAULT_BRISTLE),
306
326
  sticker: normalizeStickerSettings(raw.sticker ?? {}, b.sticker ?? DEFAULT_STICKER),
307
327
  light: normalizeLight(raw.light ?? {}, b.light ?? DEFAULT_LIGHT),
328
+ stamp: readStamp(raw.stamp, b.stamp ?? null),
329
+ spray: readSpray(raw.spray, b.spray ?? null),
308
330
  paint,
309
331
  ...raw.builtin ? { builtin: true } : {}
310
332
  };
@@ -330,6 +352,8 @@ var BASE_BRUSH = {
330
352
  bristle: { ...DEFAULT_BRISTLE },
331
353
  sticker: { ...DEFAULT_STICKER, stamps: [...DEFAULT_STICKER.stamps] },
332
354
  light: { ...DEFAULT_LIGHT },
355
+ stamp: null,
356
+ spray: null,
333
357
  paint: null
334
358
  };
335
359
  var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
@@ -959,6 +983,53 @@ var BUILTIN_BRUSHES = [
959
983
  engine: "wet",
960
984
  tip: { size: 40, hardness: 0.5, spacing: 0.12, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
961
985
  wet: { tool: "blot", strength: 0.7 }
986
+ }),
987
+ // Pens, crayon, chalk and the spray can (their facets: `@pixerate/editor/brushes` presets).
988
+ builtin({
989
+ id: "ballpoint",
990
+ name: "Ballpoint",
991
+ category: "ink",
992
+ tip: { size: 3, hardness: 0.95, spacing: 0.05, opacity: 0.9, smoothing: 0.35, pressureOpacity: true },
993
+ grain: 0.15,
994
+ pressureSmoothing: 0.3,
995
+ dynamics: { minSize: 0.7, opacityJitter: 0.08 }
996
+ }),
997
+ builtin({
998
+ id: "gel-pen",
999
+ name: "Gel pen",
1000
+ category: "ink",
1001
+ tip: { size: 4, spacing: 0.04, smoothing: 0.3 },
1002
+ pressureSmoothing: 0.3,
1003
+ dynamics: { minSize: 0.8 }
1004
+ }),
1005
+ builtin({
1006
+ id: "fineliner",
1007
+ name: "Fineliner",
1008
+ category: "ink",
1009
+ tip: { size: 2, spacing: 0.05, smoothing: 0.4, pressureSize: false }
1010
+ }),
1011
+ builtin({
1012
+ id: "crayon",
1013
+ name: "Crayon",
1014
+ category: "pastel",
1015
+ tip: { size: 14, hardness: 0.7, spacing: 0.06, opacity: 0.9, flow: 0.85, smoothing: 0.2 },
1016
+ grain: 0.9,
1017
+ dynamics: { minSize: 0.6, tiltSize: 0.6, sizeJitter: 0.1, opacityJitter: 0.1 }
1018
+ }),
1019
+ builtin({
1020
+ id: "chalk",
1021
+ name: "Chalk",
1022
+ category: "pastel",
1023
+ tip: { size: 18, hardness: 0.6, spacing: 0.05, opacity: 0.95, flow: 0.8, smoothing: 0.2 },
1024
+ grain: 1,
1025
+ dynamics: { minSize: 0.5, tiltSize: 0.8, sizeJitter: 0.12, scatter: 0.05 }
1026
+ }),
1027
+ builtin({
1028
+ id: "spray-can",
1029
+ name: "Spray can",
1030
+ category: "airbrush",
1031
+ tip: { size: 60, hardness: 0.9, spacing: 0.08, opacity: 0.9, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
1032
+ spray: { density: 0.7, size: 0.04 }
962
1033
  })
963
1034
  ];
964
1035
  var DEFAULT_BRUSH_ID = "pencil-6b";
@@ -998,12 +1069,16 @@ var EDITOR_STAMPS = {
998
1069
  toEngine: (v) => v.flatMap((ref) => {
999
1070
  try {
1000
1071
  const stamp = editorStampOf(ref);
1001
- return stamp ? [stamp] : [];
1072
+ return stamp ? [stamp] : ref.includes(":") ? [ref] : [];
1002
1073
  } catch {
1003
1074
  return [];
1004
1075
  }
1005
1076
  }),
1006
- toFacet: (v) => v.map((stamp) => `editor:${stamp}`)
1077
+ toFacet: (v) => v.map((stamp) => stamp.includes(":") ? stamp : `editor:${stamp}`)
1078
+ };
1079
+ var ONE_STAMP = {
1080
+ toEngine: (v) => v[0] ?? null,
1081
+ toFacet: (v) => v ? [v] : void 0
1007
1082
  };
1008
1083
  var f = (facet, engine, engines = ALL, extra = {}) => ({ facet, engine, engines, ...extra });
1009
1084
  var each = (facetPrefix, enginePrefix, keys, engines) => keys.map((k) => f(`${facetPrefix}.${k}`, `${enginePrefix}.${k}`, engines));
@@ -1047,6 +1122,9 @@ var BRUSH_FIELDS = [
1047
1122
  f("tip.stamps", "particle.dotTextures", ["particle"], { shapes: ["stamp"], map: EDITOR_STAMPS }),
1048
1123
  f("motion.waveSpeed", "particle.waveSpeed", ["particle"]),
1049
1124
  f("tip.stamps", "sticker.stamps", ["sticker"], { shapes: ["stamp"] }),
1125
+ f("tip.stamps", "stamp", ["dab"], { shapes: ["stamp"], map: ONE_STAMP }),
1126
+ f("marks.density", "spray.density", ["dab"], { marks: ["scatter"] }),
1127
+ f("marks.size", "spray.size", ["dab"], { marks: ["scatter"] }),
1050
1128
  ...each("marks", "sticker", ["density", "sizeJitter", "rotationJitter", "scatter"], ["sticker"]),
1051
1129
  f("marks.colourJitter", "sticker.colorJitter", ["sticker"]),
1052
1130
  ...each("tip.bristle", "bristle", ["count", "stiffness", "splay", "thickness", "load", "jitter"], ["bristle"])
@@ -1086,6 +1164,8 @@ var VARIETY_CATEGORY = {
1086
1164
  pastel: "pastel",
1087
1165
  "soft-pastel": "pastel",
1088
1166
  "oil-pastel": "pastel",
1167
+ crayon: "pastel",
1168
+ chalk: "pastel",
1089
1169
  charcoal: "charcoal",
1090
1170
  airbrush: "airbrush",
1091
1171
  marker: "marker"
@@ -1118,10 +1198,11 @@ function resolveBrush(brush, options = {}) {
1118
1198
  const engine = resolveEngine(brush);
1119
1199
  const input = { id: brush.id, name: brush.name, category: resolveCategory(brush, engine), engine };
1120
1200
  for (const field of BRUSH_FIELDS) {
1121
- if (!field.engines.includes(engine) || field.shapes && !field.shapes.includes(brush.tip.shape)) continue;
1201
+ if (!field.engines.includes(engine) || field.shapes && !field.shapes.includes(brush.tip.shape) || field.marks && !field.marks.includes(brush.marks?.kind ?? "continuous")) continue;
1122
1202
  const v = getPath(brush, field.facet);
1123
1203
  if (v !== void 0) setPath(input, field.engine, field.map ? field.map.toEngine(v) : copy(v));
1124
1204
  }
1205
+ if (engine === "dab" && brush.marks?.kind === "scatter") input.spray = { ...input.spray ?? {} };
1125
1206
  if (engine === "wet") {
1126
1207
  setPath(input, "wet.tool", brush.kind === "tool" ? WET_TOOL_OF[brush.tool] : "paint");
1127
1208
  const medium = resolveWetMedium(brush);
@@ -1147,7 +1228,7 @@ var CATEGORY_MEDIUM = {
1147
1228
  };
1148
1229
  function tipShape(b) {
1149
1230
  if (b.engine === "bristle") return "bristle";
1150
- if (b.engine === "sticker" || b.engine === "particle" && b.particle.dotTextures.length > 0) return "stamp";
1231
+ if (b.engine === "sticker" || b.engine === "particle" && b.particle.dotTextures.length > 0 || b.engine === "dab" && b.stamp) return "stamp";
1151
1232
  if (b.tip.roundness >= 1) return "round";
1152
1233
  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";
1153
1234
  }
@@ -1173,7 +1254,7 @@ function skeleton(b) {
1173
1254
  return { ...brush, marks: { kind: "objects" } };
1174
1255
  default: {
1175
1256
  const [medium, variety] = CATEGORY_MEDIUM[b.category] ?? [];
1176
- return { ...brush, ...medium ? { medium } : {}, ...variety ? { variety } : {} };
1257
+ return { ...brush, ...medium ? { medium } : {}, ...variety ? { variety } : {}, ...b.engine === "dab" && b.spray ? { marks: { kind: "scatter" } } : {} };
1177
1258
  }
1178
1259
  }
1179
1260
  }
@@ -1183,7 +1264,7 @@ function migrateBrushV1(raw) {
1183
1264
  const brush = skeleton(b);
1184
1265
  const defaults = resolveBrush(brush);
1185
1266
  for (const field of BRUSH_FIELDS) {
1186
- if (!field.engines.includes(b.engine) || field.shapes && !field.shapes.includes(brush.tip.shape)) continue;
1267
+ if (!field.engines.includes(b.engine) || field.shapes && !field.shapes.includes(brush.tip.shape) || field.marks && !field.marks.includes(brush.marks?.kind ?? "continuous")) continue;
1187
1268
  const v = getPath(b, field.engine);
1188
1269
  if (!equal(v, getPath(defaults, field.engine))) setPath(brush, field.facet, field.map ? field.map.toFacet(v) : copy2(v));
1189
1270
  }
@@ -1196,7 +1277,7 @@ var BRUSH_KINDS = ["mark", "tool"];
1196
1277
  var BRUSH_MEDIA = ["paint", "ink", "dry", "spray"];
1197
1278
  var BRUSH_TOOLS = ["eraser", "blender", "smudge", "water", "dryer", "blow", "blot", "wet-blend", "wet-drag"];
1198
1279
  var BRUSH_TIP_SHAPES = ["round", "flat", "chisel", "bristle", "stamp"];
1199
- var BRUSH_MARKS_KINDS = ["continuous", "particles", "objects"];
1280
+ var BRUSH_MARKS_KINDS = ["continuous", "scatter", "particles", "objects"];
1200
1281
  var BRUSH_SURFACES = ["dry", "wet", "thick"];
1201
1282
 
1202
1283
  // src/brushes/schema.ts
@@ -1284,6 +1365,15 @@ var PARTICLES = object(
1284
1365
  },
1285
1366
  ["kind"]
1286
1367
  );
1368
+ var SCATTER = object(
1369
+ "A spray (a spray can): each dab lays a seeded scatter of droplets across the tip; jitter and size come from dynamics and the tip. Missing fields take their defaults.",
1370
+ {
1371
+ kind: { type: "string", const: "scatter", description: 'Always "scatter".' },
1372
+ density: unit3("Droplets per dab: 1 + density \xD7 31 (default 0.5)."),
1373
+ size: num(0.02, 1, "Droplet size as a fraction of the tip size (default 0.12).")
1374
+ },
1375
+ ["kind"]
1376
+ );
1287
1377
  var OBJECTS = object(
1288
1378
  "Each mark stays an editable object (a sticker) on a sticker layer; needs a stamp tip. Missing fields take their defaults.",
1289
1379
  {
@@ -1423,7 +1513,7 @@ var BRUSH_JSON_SCHEMA = object(
1423
1513
  "Tools: eraser removes paint; blender mixes the colours under the tip; smudge drags paint along; water lays clear water (lifts dried paint with behaviour.wet.rewet); dryer dries wet paint; blow pushes water along; blot takes wet paint up; wet-blend evens out wet pigment; wet-drag drags wet paint."
1424
1514
  ),
1425
1515
  tip: TIP,
1426
- marks: { description: "How the tip is laid along the path (default continuous).", oneOf: [CONTINUOUS, PARTICLES, OBJECTS] },
1516
+ marks: { description: "How the tip is laid along the path (default continuous): continuous dabs, a scatter of spray droplets, particles, or objects (editable stickers).", oneOf: [CONTINUOUS, SCATTER, PARTICLES, OBJECTS] },
1427
1517
  dynamics: DYNAMICS,
1428
1518
  behaviour: BEHAVIOUR,
1429
1519
  light: LIGHT,
@@ -1434,7 +1524,7 @@ var BRUSH_JSON_SCHEMA = object(
1434
1524
  );
1435
1525
  var BRUSH_FACET_SCHEMAS = {
1436
1526
  tip: TIP,
1437
- marks: { continuous: CONTINUOUS, particles: PARTICLES, objects: OBJECTS },
1527
+ marks: { continuous: CONTINUOUS, scatter: SCATTER, particles: PARTICLES, objects: OBJECTS },
1438
1528
  dynamics: DYNAMICS,
1439
1529
  behaviour: BEHAVIOUR,
1440
1530
  light: LIGHT,
@@ -1562,7 +1652,7 @@ function normalizeBrushSpec(raw, problems = []) {
1562
1652
  if (rawMarks !== void 0) {
1563
1653
  const k = isObject(rawMarks) ? rawMarks.kind : void 0;
1564
1654
  const schema = BRUSH_FACET_SCHEMAS.marks[k];
1565
- if (!schema) problems.push(`marks.kind ${JSON.stringify(k)} is not valid; use continuous, particles or objects. marks dropped.`);
1655
+ if (!schema) problems.push(`marks.kind ${JSON.stringify(k)} is not valid; use continuous, scatter, particles or objects. marks dropped.`);
1566
1656
  else brush.marks = readObject(rawMarks, schema, "brush.marks", problems);
1567
1657
  }
1568
1658
  if (rawColour !== void 0) {
@@ -1600,6 +1690,14 @@ function normalizeBrushSpec(raw, problems = []) {
1600
1690
  delete brush.marks;
1601
1691
  }
1602
1692
  }
1693
+ if (brush.marks?.kind === "scatter") {
1694
+ const surface = brush.behaviour?.surface;
1695
+ const why = brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : surface === "wet" || surface === "thick" ? `${surface} paint is not sprayed` : brush.behaviour?.exchange ? "a spray does not pick paint up" : null;
1696
+ if (why) {
1697
+ drop("marks", why);
1698
+ delete brush.marks;
1699
+ }
1700
+ }
1603
1701
  if (brush.marks?.kind === "objects" && brush.tip.shape !== "stamp") {
1604
1702
  problems.push("marks objects lay stamps: tip.shape set to stamp.");
1605
1703
  brush.tip.shape = "stamp";
@@ -1613,15 +1711,7 @@ function normalizeBrushSpec(raw, problems = []) {
1613
1711
  delete brush.tip.stamps;
1614
1712
  }
1615
1713
  if (brush.tip.stamps) {
1616
- const particles = brush.marks?.kind === "particles";
1617
- const stamps = brush.tip.stamps.flatMap((s) => {
1618
- const ref = normalizeStickerRef(s);
1619
- if (particles && !editorStampOf(ref)) {
1620
- problems.push(`brush.tip.stamps "${ref}" dropped: particle marks lay only the editor's stamps (editor:star, editor:dot\u2026).`);
1621
- return [];
1622
- }
1623
- return [ref];
1624
- });
1714
+ const stamps = brush.tip.stamps.map((s) => normalizeStickerRef(s));
1625
1715
  if (stamps.length) brush.tip.stamps = stamps;
1626
1716
  else {
1627
1717
  drop("tip.stamps", "none of the stamps can be laid");
@@ -2123,7 +2213,8 @@ var BRUSH_PRESETS = [
2123
2213
  variety: "neon",
2124
2214
  tip: { shape: "round", size: 10, hardness: 0.9, spacing: 0.04 },
2125
2215
  dynamics: { smoothing: 0.45, pressureSize: false },
2126
- light: { amount: 1 }
2216
+ light: { amount: 1 },
2217
+ colour: { palette: ["#f0abfc", "#22d3ee", "#a3e635", "#fb7185", "#fde047"] }
2127
2218
  },
2128
2219
  {
2129
2220
  id: "light-airbrush",
@@ -2340,6 +2431,77 @@ var BRUSH_PRESETS = [
2340
2431
  tip: { shape: "round", size: 40, hardness: 0.5, spacing: 0.12 },
2341
2432
  dynamics: { smoothing: 0.2, pressureSize: false, pressureOpacity: true },
2342
2433
  behaviour: { strength: 0.7 }
2434
+ },
2435
+ // --- Pens, crayon, chalk and the spray can -------------------------------------------
2436
+ // A ballpoint: a fine, hard, even line that lightens with a light touch.
2437
+ {
2438
+ id: "ballpoint",
2439
+ name: "Ballpoint",
2440
+ kind: "mark",
2441
+ medium: "ink",
2442
+ variety: "ballpoint",
2443
+ tip: { shape: "round", size: 3, hardness: 0.95, spacing: 0.05, opacity: 0.9 },
2444
+ dynamics: { smoothing: 0.35, pressureSmoothing: 0.3, pressureOpacity: true, minSize: 0.7, opacityJitter: 0.08 },
2445
+ behaviour: { grain: 0.15 },
2446
+ colour: { default: "#1e3a8a", palette: ["#1e3a8a", "#111827", "#b91c1c", "#166534"] }
2447
+ },
2448
+ // A gel pen: an even, opaque, saturated line.
2449
+ {
2450
+ id: "gel-pen",
2451
+ name: "Gel pen",
2452
+ kind: "mark",
2453
+ medium: "ink",
2454
+ variety: "gel",
2455
+ tip: { shape: "round", size: 4, spacing: 0.04 },
2456
+ dynamics: { smoothing: 0.3, pressureSmoothing: 0.3, minSize: 0.8 },
2457
+ colour: { default: "#111827", palette: ["#111827", "#7c3aed", "#db2777", "#0891b2", "#16a34a", "#f59e0b"] }
2458
+ },
2459
+ // A fineliner: one width, whatever the pressure.
2460
+ {
2461
+ id: "fineliner",
2462
+ name: "Fineliner",
2463
+ kind: "mark",
2464
+ medium: "ink",
2465
+ variety: "fineliner",
2466
+ tip: { shape: "round", size: 2, spacing: 0.05 },
2467
+ dynamics: { smoothing: 0.4, pressureSize: false },
2468
+ colour: { default: "#111827" }
2469
+ },
2470
+ // A wax crayon: waxy, catching the paper's peaks, a little uneven.
2471
+ {
2472
+ id: "crayon",
2473
+ name: "Crayon",
2474
+ kind: "mark",
2475
+ medium: "dry",
2476
+ variety: "crayon",
2477
+ tip: { shape: "round", size: 14, hardness: 0.7, spacing: 0.06, opacity: 0.9, flow: 0.85 },
2478
+ dynamics: { smoothing: 0.2, minSize: 0.6, tiltSize: 0.6, sizeJitter: 0.1, opacityJitter: 0.1 },
2479
+ behaviour: { grain: 0.9 },
2480
+ colour: { default: "#dc2626", palette: ["#dc2626", "#f59e0b", "#facc15", "#16a34a", "#2563eb", "#7c3aed"] }
2481
+ },
2482
+ // Chalk: broad, powdery and grainy. Its default colour is light on dark paper and dark on light.
2483
+ {
2484
+ id: "chalk",
2485
+ name: "Chalk",
2486
+ kind: "mark",
2487
+ medium: "dry",
2488
+ variety: "chalk",
2489
+ tip: { shape: "round", size: 18, hardness: 0.6, spacing: 0.05, opacity: 0.95, flow: 0.8 },
2490
+ dynamics: { smoothing: 0.2, minSize: 0.5, tiltSize: 0.8, sizeJitter: 0.12, scatter: 0.05 },
2491
+ behaviour: { grain: 1 },
2492
+ colour: { default: { light: "#475569", dark: "#f8fafc" }, palette: [{ light: "#475569", dark: "#f8fafc" }, "#fca5a5", "#fde68a", "#93c5fd"] }
2493
+ },
2494
+ // A spray can: a mist of hard droplets, denser the slower it passes.
2495
+ {
2496
+ id: "spray-can",
2497
+ name: "Spray can",
2498
+ kind: "mark",
2499
+ medium: "spray",
2500
+ variety: "spraycan",
2501
+ tip: { shape: "round", size: 60, hardness: 0.9, spacing: 0.08, opacity: 0.9 },
2502
+ marks: { kind: "scatter", density: 0.7, size: 0.04 },
2503
+ dynamics: { smoothing: 0.2, pressureSize: false, pressureOpacity: true },
2504
+ colour: { palette: ["#ef4444", "#f97316", "#facc15", "#22c55e", "#3b82f6", "#a855f7", "#111827", "#f8fafc"] }
2343
2505
  }
2344
2506
  ];
2345
2507
 
@@ -2771,6 +2933,7 @@ export {
2771
2933
  BRUSH_ID,
2772
2934
  normalizeMultiColor,
2773
2935
  normalizeBehavior,
2936
+ DEFAULT_SPRAY,
2774
2937
  isWetCapable,
2775
2938
  isWetBrush,
2776
2939
  usesPaintExchange,
@@ -1,6 +1,3 @@
1
- import {
2
- parseCssColor
3
- } from "./chunk-3CI7FRMK.js";
4
1
  import {
5
2
  boxMean,
6
3
  createSelectionMask,
@@ -11,6 +8,9 @@ import {
11
8
  hashMaskContent,
12
9
  resizeSelection
13
10
  } from "./chunk-ZNA3IQZF.js";
11
+ import {
12
+ parseCssColor
13
+ } from "./chunk-3CI7FRMK.js";
14
14
  import {
15
15
  DEFAULT_GLOBAL_LIGHT,
16
16
  DeepCompositor,
@@ -106,7 +106,7 @@ import {
106
106
  transformTextRect,
107
107
  translateMatrix,
108
108
  warpPresetMesh
109
- } from "./chunk-AF23QK5F.js";
109
+ } from "./chunk-YQ4PQTBB.js";
110
110
  import {
111
111
  bezierPathBounds,
112
112
  outlineStroke,
@@ -1,6 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
-
3
-
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
4
2
 
5
3
 
6
4
 
@@ -123,26 +121,6 @@ function normalizeColor(input) {
123
121
  return parsed ? formatHex(parsed) : null;
124
122
  }
125
123
 
126
- // src/color/suggest.ts
127
- function suggestAccessibleForeground(foreground, background, options = {}) {
128
- const large = _nullishCoalesce(options.large, () => ( false));
129
- const lch = _chunk5UEFBM2Mcjs.oklabToOklch.call(void 0, _chunk5UEFBM2Mcjs.rgbToOklab.call(void 0, foreground));
130
- const asHex = (l) => _chunk5UEFBM2Mcjs.parseColor.call(void 0, formatHex(toSrgbGamut(_chunk5UEFBM2Mcjs.oklabToRgb.call(void 0, _chunk5UEFBM2Mcjs.oklchToOklab.call(void 0, { ...lch, l })))));
131
- for (let d = 0.01; d <= 1; d += 0.01) {
132
- const downCand = asHex(Math.max(0, lch.l - d));
133
- const upCand = asHex(Math.min(1, lch.l + d));
134
- const downRatio = _chunk5UEFBM2Mcjs.contrastRatio.call(void 0, downCand, background);
135
- const upRatio = _chunk5UEFBM2Mcjs.contrastRatio.call(void 0, upCand, background);
136
- const downPass = _chunk5UEFBM2Mcjs.contrastLevel.call(void 0, downRatio, { large }) !== "fail";
137
- const upPass = _chunk5UEFBM2Mcjs.contrastLevel.call(void 0, upRatio, { large }) !== "fail";
138
- if (downPass && upPass) return formatHex(downRatio >= upRatio ? downCand : upCand);
139
- if (downPass) return formatHex(downCand);
140
- if (upPass) return formatHex(upCand);
141
- }
142
- return void 0;
143
- }
144
-
145
-
146
124
 
147
125
 
148
126
 
@@ -150,4 +128,4 @@ function suggestAccessibleForeground(foreground, background, options = {}) {
150
128
 
151
129
 
152
130
 
153
- exports.inSrgbGamut = inSrgbGamut; exports.deltaEOK = deltaEOK; exports.toSrgbGamut = toSrgbGamut; exports.formatHex = formatHex; exports.formatColor = formatColor; exports.normalizeColor = normalizeColor; exports.suggestAccessibleForeground = suggestAccessibleForeground;
131
+ exports.inSrgbGamut = inSrgbGamut; exports.deltaEOK = deltaEOK; exports.toSrgbGamut = toSrgbGamut; exports.formatHex = formatHex; exports.formatColor = formatColor; exports.normalizeColor = normalizeColor;
@@ -1,15 +1,17 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
+ var _chunkI3Y2Q3PMcjs = require('./chunk-I3Y2Q3PM.cjs');
3
4
 
4
5
 
5
6
 
6
7
 
7
- var _chunkEUIDTZNQcjs = require('./chunk-EUIDTZNQ.cjs');
8
8
 
9
+ var _chunkMLLTWAXHcjs = require('./chunk-MLLTWAXH.cjs');
9
10
 
10
11
 
11
12
 
12
- var _chunkTFF2GTGNcjs = require('./chunk-TFF2GTGN.cjs');
13
+
14
+ var _chunkSXQHWXELcjs = require('./chunk-SXQHWXEL.cjs');
13
15
 
14
16
 
15
17
 
@@ -70,9 +72,9 @@ function normalizeGradient(input) {
70
72
  const rawStop = s;
71
73
  let normColor = null;
72
74
  if (typeof rawStop.color === "string" || rawStop.color && typeof rawStop.color === "object") {
73
- const dual = _chunkEUIDTZNQcjs.normalizeDualColor.call(void 0, rawStop.color);
75
+ const dual = _chunkMLLTWAXHcjs.normalizeDualColor.call(void 0, rawStop.color);
74
76
  if (dual) {
75
- normColor = _chunkEUIDTZNQcjs.formatDualColor.call(void 0, dual);
77
+ normColor = _chunkMLLTWAXHcjs.formatDualColor.call(void 0, dual);
76
78
  }
77
79
  }
78
80
  if (!normColor) {
@@ -142,7 +144,7 @@ function createGradient(colorsOrOptions, options) {
142
144
  const stops = resolvedColors.map((color, i) => ({
143
145
  id: `s${i + 1}`,
144
146
  offset: i / (resolvedColors.length - 1),
145
- color: _chunkEUIDTZNQcjs.formatDualColor.call(void 0, _nullishCoalesce(_chunkEUIDTZNQcjs.normalizeDualColor.call(void 0, color), () => ( { light: "#000000", dark: "#000000" })))
147
+ color: _chunkMLLTWAXHcjs.formatDualColor.call(void 0, _nullishCoalesce(_chunkMLLTWAXHcjs.normalizeDualColor.call(void 0, color), () => ( { light: "#000000", dark: "#000000" })))
146
148
  }));
147
149
  const res2 = normalizeGradient({
148
150
  ...options,
@@ -194,7 +196,7 @@ function isDualColorValue(val) {
194
196
  if (typeof val === "string") {
195
197
  return val.trim().toLowerCase().startsWith("light-dark(");
196
198
  }
197
- if (_chunkEUIDTZNQcjs.isDualColor.call(void 0, val)) {
199
+ if (_chunkMLLTWAXHcjs.isDualColor.call(void 0, val)) {
198
200
  return val.light !== val.dark;
199
201
  }
200
202
  return false;
@@ -208,7 +210,7 @@ function resolveGradient(g, scheme) {
208
210
  center: g.center ? { ...g.center } : void 0,
209
211
  stops: g.stops.map((s) => ({
210
212
  ...s,
211
- color: _nullishCoalesce(_chunkEUIDTZNQcjs.resolveColor.call(void 0, s.color, scheme), () => ( s.color))
213
+ color: _nullishCoalesce(_chunkMLLTWAXHcjs.resolveColor.call(void 0, s.color, scheme), () => ( s.color))
212
214
  }))
213
215
  };
214
216
  }
@@ -236,7 +238,7 @@ function sampleGradient(g, t, scheme = "light") {
236
238
  if (s0.midpoint !== void 0) {
237
239
  u = gradientMidpointRemap(u, s0.midpoint);
238
240
  }
239
- const mixed = _chunkEUIDTZNQcjs.mixColors.call(void 0, s0.color, s1.color, u, { space: _nullishCoalesce(g.interpolation, () => ( "oklab")) });
241
+ const mixed = _chunkI3Y2Q3PMcjs.mixColors.call(void 0, s0.color, s1.color, u, { space: _nullishCoalesce(g.interpolation, () => ( "oklab")) });
240
242
  return _nullishCoalesce(mixed, () => ( s0.color));
241
243
  }
242
244
  function insertGradientStop(g, offset, color) {
@@ -575,12 +577,12 @@ ${stopsXml.join("\n")}
575
577
  </linearGradient>`;
576
578
  }
577
579
  function checkGradient(g, options = {}) {
578
- const dual = isDualGradient(g) || _chunkEUIDTZNQcjs.isDualColor.call(void 0, options.surface) || _chunkEUIDTZNQcjs.isDualColor.call(void 0, options.text);
580
+ const dual = isDualGradient(g) || _chunkMLLTWAXHcjs.isDualColor.call(void 0, options.surface) || _chunkMLLTWAXHcjs.isDualColor.call(void 0, options.text);
579
581
  if (dual) {
580
582
  const lightG = resolveGradient(g, "light");
581
583
  const darkG = resolveGradient(g, "dark");
582
- const lightText = options.text ? _nullishCoalesce(_chunkEUIDTZNQcjs.resolveColor.call(void 0, options.text, "light"), () => ( void 0)) : void 0;
583
- const darkText = options.text ? _nullishCoalesce(_chunkEUIDTZNQcjs.resolveColor.call(void 0, options.text, "dark"), () => ( void 0)) : void 0;
584
+ const lightText = options.text ? _nullishCoalesce(_chunkMLLTWAXHcjs.resolveColor.call(void 0, options.text, "light"), () => ( void 0)) : void 0;
585
+ const darkText = options.text ? _nullishCoalesce(_chunkMLLTWAXHcjs.resolveColor.call(void 0, options.text, "dark"), () => ( void 0)) : void 0;
584
586
  const checkScheme = (grad, txt, schemeName) => {
585
587
  const issues2 = [];
586
588
  for (let i = 0; i < grad.stops.length - 1; i++) {
@@ -591,7 +593,7 @@ function checkGradient(g, options = {}) {
591
593
  const c1 = _chunk5UEFBM2Mcjs.parseColor.call(void 0, grad.stops[i].color);
592
594
  const c2 = _chunk5UEFBM2Mcjs.parseColor.call(void 0, grad.stops[i + 1].color);
593
595
  if (c1 && c2 && gap < 0.05) {
594
- const dE = _chunkTFF2GTGNcjs.deltaEOK.call(void 0, c1, c2);
596
+ const dE = _chunkSXQHWXELcjs.deltaEOK.call(void 0, c1, c2);
595
597
  if (dE > 0.5) {
596
598
  issues2.push(
597
599
  `[${schemeName}] Banding risk between stops "${grad.stops[i].id}" and "${grad.stops[i + 1].id}" (\u0394EOK = ${dE.toFixed(2)} across gap ${gap.toFixed(3)}).`
@@ -601,7 +603,7 @@ function checkGradient(g, options = {}) {
601
603
  }
602
604
  for (const s of grad.stops) {
603
605
  const c = _chunk5UEFBM2Mcjs.parseColor.call(void 0, s.color);
604
- if (c && !_chunkTFF2GTGNcjs.inSrgbGamut.call(void 0, c)) {
606
+ if (c && !_chunkSXQHWXELcjs.inSrgbGamut.call(void 0, c)) {
605
607
  issues2.push(`[${schemeName}] Stop "${s.id}" colour ${s.color} is outside standard sRGB gamut.`);
606
608
  }
607
609
  }
@@ -638,7 +640,7 @@ function checkGradient(g, options = {}) {
638
640
  const c1 = _chunk5UEFBM2Mcjs.parseColor.call(void 0, g.stops[i].color);
639
641
  const c2 = _chunk5UEFBM2Mcjs.parseColor.call(void 0, g.stops[i + 1].color);
640
642
  if (c1 && c2 && gap < 0.05) {
641
- const dE = _chunkTFF2GTGNcjs.deltaEOK.call(void 0, c1, c2);
643
+ const dE = _chunkSXQHWXELcjs.deltaEOK.call(void 0, c1, c2);
642
644
  if (dE > 0.5) {
643
645
  issues.push(
644
646
  `Banding risk between stops "${g.stops[i].id}" and "${g.stops[i + 1].id}" (\u0394EOK = ${dE.toFixed(2)} across gap ${gap.toFixed(3)}).`
@@ -648,7 +650,7 @@ function checkGradient(g, options = {}) {
648
650
  }
649
651
  for (const s of g.stops) {
650
652
  const c = _chunk5UEFBM2Mcjs.parseColor.call(void 0, s.color);
651
- if (c && !_chunkTFF2GTGNcjs.inSrgbGamut.call(void 0, c)) {
653
+ if (c && !_chunkSXQHWXELcjs.inSrgbGamut.call(void 0, c)) {
652
654
  issues.push(`Stop "${s.id}" colour ${s.color} is outside standard sRGB gamut.`);
653
655
  }
654
656
  }
@@ -875,7 +877,7 @@ function fitGradientStops(samples, options = {}) {
875
877
  }
876
878
  const stops = at.map((i, k) => {
877
879
  const [l, a, b, alpha] = lab[i].v;
878
- const color = _chunkTFF2GTGNcjs.formatHex.call(void 0, _chunk5UEFBM2Mcjs.oklabToRgb.call(void 0, { l, a, b, alpha: Math.min(1, Math.max(0, alpha)) }));
880
+ const color = _chunkSXQHWXELcjs.formatHex.call(void 0, _chunk5UEFBM2Mcjs.oklabToRgb.call(void 0, { l, a, b, alpha: Math.min(1, Math.max(0, alpha)) }));
879
881
  return { id: `s${k + 1}`, offset: k === 0 ? 0 : k === at.length - 1 ? 1 : Math.round(lab[i].t * 1e3) / 1e3, color };
880
882
  });
881
883
  return { stops, maxError: Math.round(maxErrorOf(lab, at) * 1e4) / 1e4 };