@pixerate/editor 0.254.1 → 0.256.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.js +1 -1
  3. package/dist/animation-editor/index.cjs +59 -59
  4. package/dist/animation-editor/index.js +3 -3
  5. package/dist/brushes/index.cjs +31 -29
  6. package/dist/brushes/index.d.cts +1 -1
  7. package/dist/brushes/index.d.ts +1 -1
  8. package/dist/brushes/index.js +27 -25
  9. package/dist/brushes/library.d.cts +17 -0
  10. package/dist/brushes/library.d.ts +17 -0
  11. package/dist/{brushstrokeRender-5PYSKZIJ.js → brushstrokeRender-HCOQUSWW.js} +53 -18
  12. package/dist/{brushstrokeRender-7FLLSQEC.cjs → brushstrokeRender-ZZOXI5O5.cjs} +55 -20
  13. package/dist/canvas/brushTypes.d.cts +22 -3
  14. package/dist/canvas/brushTypes.d.ts +22 -3
  15. package/dist/canvas/brushstroke.d.cts +34 -12
  16. package/dist/canvas/brushstroke.d.ts +34 -12
  17. package/dist/canvas/brushstrokeRender.d.cts +14 -8
  18. package/dist/canvas/brushstrokeRender.d.ts +14 -8
  19. package/dist/canvas/drawController.d.cts +11 -3
  20. package/dist/canvas/drawController.d.ts +11 -3
  21. package/dist/canvas/freehand.d.cts +6 -1
  22. package/dist/canvas/freehand.d.ts +6 -1
  23. package/dist/canvas/index.cjs +114 -102
  24. package/dist/canvas/index.js +16 -4
  25. package/dist/{chunk-XVIK73LS.js → chunk-5P4JEOIV.js} +4 -4
  26. package/dist/{chunk-JPEHXIB5.cjs → chunk-5YWWQ36H.cjs} +66 -21
  27. package/dist/{chunk-CHAWTXBE.cjs → chunk-6WCQ35GI.cjs} +4 -4
  28. package/dist/{chunk-GYMMNCFT.js → chunk-AF23QK5F.js} +56 -11
  29. package/dist/{chunk-JFHBXNR2.cjs → chunk-B7V34GIH.cjs} +86 -86
  30. package/dist/{chunk-QC3OXRE4.js → chunk-DMTEQ6SH.js} +1 -1
  31. package/dist/{chunk-TLOGFIIV.cjs → chunk-GCTUOTCW.cjs} +35 -35
  32. package/dist/{chunk-34KLWUH4.cjs → chunk-HIMGBZAM.cjs} +1133 -597
  33. package/dist/{chunk-7P6VF3FG.js → chunk-HYWRU5VX.js} +1 -1
  34. package/dist/{chunk-QPHNVZHO.js → chunk-JVRLGV3C.js} +1118 -582
  35. package/dist/{chunk-LFMKWGFL.cjs → chunk-LRNUZKWV.cjs} +22 -8
  36. package/dist/chunk-PGPFWC35.cjs +517 -0
  37. package/dist/{chunk-QLSGTCYT.cjs → chunk-TBAVCLEQ.cjs} +23 -23
  38. package/dist/{chunk-2ZJ4DT3M.js → chunk-VUKUTT2W.js} +1 -1
  39. package/dist/{chunk-5JNA6LLY.js → chunk-WYGSR3IJ.js} +22 -8
  40. package/dist/chunk-YHUMGBZD.js +517 -0
  41. package/dist/commands/builtins.d.cts +1 -1
  42. package/dist/commands/builtins.d.ts +1 -1
  43. package/dist/commands/index.cjs +2 -2
  44. package/dist/commands/index.js +1 -1
  45. package/dist/image-editor/brush-presets.d.cts +32 -0
  46. package/dist/image-editor/brush-presets.d.ts +32 -0
  47. package/dist/image-editor/controller.d.cts +15 -0
  48. package/dist/image-editor/controller.d.ts +15 -0
  49. package/dist/image-editor/index.cjs +848 -775
  50. package/dist/image-editor/index.d.cts +1 -0
  51. package/dist/image-editor/index.d.ts +1 -0
  52. package/dist/image-editor/index.js +81 -8
  53. package/dist/image-editor/types.d.cts +7 -0
  54. package/dist/image-editor/types.d.ts +7 -0
  55. package/dist/paint-editor/collaboration-tools.d.cts +19 -0
  56. package/dist/paint-editor/collaboration-tools.d.ts +19 -0
  57. package/dist/paint-editor/collaboration.d.cts +180 -0
  58. package/dist/paint-editor/collaboration.d.ts +180 -0
  59. package/dist/paint-editor/controller.d.cts +54 -0
  60. package/dist/paint-editor/controller.d.ts +54 -0
  61. package/dist/paint-editor/index.cjs +816 -283
  62. package/dist/paint-editor/index.d.cts +2 -0
  63. package/dist/paint-editor/index.d.ts +2 -0
  64. package/dist/paint-editor/index.js +567 -34
  65. package/dist/vector/brushstroke.d.cts +21 -5
  66. package/dist/vector/brushstroke.d.ts +21 -5
  67. package/dist/vector/controller.d.cts +14 -1
  68. package/dist/vector/controller.d.ts +14 -1
  69. package/dist/vector/index.cjs +1220 -1199
  70. package/dist/vector/index.js +34 -13
  71. package/dist/vector/types.d.cts +7 -0
  72. package/dist/vector/types.d.ts +7 -0
  73. package/package.json +1 -1
  74. package/skills/pixerate-brushes/SKILL.md +9 -3
  75. package/skills/pixerate-canvas/SKILL.md +10 -5
  76. package/skills/pixerate-image-editor/SKILL.md +2 -1
  77. package/skills/pixerate-paint-editor/SKILL.md +31 -46
  78. package/skills/pixerate-paint-editor/references/painting-alongside.md +19 -0
  79. package/skills/pixerate-vector/SKILL.md +1 -1
  80. package/dist/chunk-HS6QDWDF.cjs +0 -1029
  81. package/dist/chunk-MTKY6K3Q.js +0 -1029
@@ -26,7 +26,7 @@ import {
26
26
  traceField,
27
27
  traceVectorImage,
28
28
  vectorTraceContours
29
- } from "../chunk-2ZJ4DT3M.js";
29
+ } from "../chunk-VUKUTT2W.js";
30
30
  import {
31
31
  applyHomography,
32
32
  homographyFromPoints,
@@ -53,7 +53,7 @@ import {
53
53
  reverseMorphContour,
54
54
  subpathToMorphContour,
55
55
  turnMorphContour
56
- } from "../chunk-7P6VF3FG.js";
56
+ } from "../chunk-HYWRU5VX.js";
57
57
  import {
58
58
  DEFAULT_VECTOR_FONT,
59
59
  IDENTITY_MATRIX,
@@ -376,6 +376,8 @@ import {
376
376
  vectorBoundsInside,
377
377
  vectorBoxMatrix,
378
378
  vectorBrushstrokeBrush,
379
+ vectorBrushstrokeBrushError,
380
+ vectorBrushstrokeBrushOf,
379
381
  vectorBrushstrokeBrushes,
380
382
  vectorBrushstrokeCharacterError,
381
383
  vectorBrushstrokeImages,
@@ -526,8 +528,10 @@ import {
526
528
  withVectorStrokes,
527
529
  withVectorTargetPaint,
528
530
  zigzagPaths
529
- } from "../chunk-GYMMNCFT.js";
530
- import "../chunk-MTKY6K3Q.js";
531
+ } from "../chunk-AF23QK5F.js";
532
+ import {
533
+ snapshotBrushLibrary
534
+ } from "../chunk-JVRLGV3C.js";
531
535
  import "../chunk-APX63ILU.js";
532
536
  import {
533
537
  createProvenance
@@ -3689,7 +3693,7 @@ function getVectorAppearancePanel(state) {
3689
3693
  ...kind === "stroke" ? { strokeWidth: l.strokeWidth ?? 1, strokeAlign: l.strokeAlign ?? "center" } : {},
3690
3694
  ...isVectorBrushstrokeLayer(l) ? (() => {
3691
3695
  const b = l.brush;
3692
- const preset = vectorBrushstrokeBrush(b.brushId);
3696
+ const preset = vectorBrushstrokeBrushOf(b);
3693
3697
  return { brushstroke: { brushId: b.brushId, name: preset.name, size: b.size ?? preset.tip.size, ...b.seed !== void 0 ? { seed: b.seed } : {}, grain: b.seed !== void 0 && b.paper !== null } };
3694
3698
  })() : (l.kind ?? "paint") !== "paint" ? { unknownKind: String(l.kind) } : {},
3695
3699
  editing: edit.target === kind && (edit.layer === null ? index === count - 1 : edit.layer === index)
@@ -6818,6 +6822,7 @@ var VectorEditorController = class _VectorEditorController {
6818
6822
  revision = 0;
6819
6823
  state;
6820
6824
  constructor(options = {}) {
6825
+ this.brushes = options.brushLibrary ? snapshotBrushLibrary(options.brushLibrary, () => this.notify()) : null;
6821
6826
  this.history = options.history ?? new HistoryManager();
6822
6827
  this.doc = options.document ? loadedDocument(options.document) : { width: 800, height: 600, nodes: [] };
6823
6828
  this.doc = this.refreshed(this.doc);
@@ -6853,6 +6858,16 @@ var VectorEditorController = class _VectorEditorController {
6853
6858
  * Stops listening to the history and drops all listeners. A later `subscribe` reconnects, so a
6854
6859
  * controller torn down by React StrictMode's double effect keeps working when it is reused.
6855
6860
  */
6861
+ /** The saved brushes of the brush library as last read (empty without one). */
6862
+ brushes;
6863
+ /** The brushes a brushstroke can use: the presets, then the brush library's saved brushes the vector profile admits. */
6864
+ listBrushstrokeBrushes() {
6865
+ return vectorBrushstrokeBrushes(this.brushes?.user ?? []);
6866
+ }
6867
+ /** Resolves once the brush library has been read (its saved brushes can be used from then on). */
6868
+ whenBrushesLoaded() {
6869
+ return this.brushes?.ready ?? Promise.resolve();
6870
+ }
6856
6871
  destroy() {
6857
6872
  if (this.dwellTimer) clearTimeout(this.dwellTimer);
6858
6873
  this.dwellTimer = null;
@@ -8163,15 +8178,17 @@ var VectorEditorController = class _VectorEditorController {
8163
8178
  }
8164
8179
  /**
8165
8180
  * Adds a brushstroke to the selected (or given) paths: a stroke layer that paints the path with
8166
- * brush `brushId` from the paint editor's library (`vectorBrushstrokeBrushes`), `size` document
8181
+ * brush `brushId` (a preset, or a saved brush of the brush library: `listBrushstrokeBrushes`), `size` document
8167
8182
  * units across (default the brush's own), an optional `pressure` profile along the length, in
8168
8183
  * `color` (default the top stroke's paint, else black) at `opacity`. At the top of the stroke
8169
8184
  * stack or at `index`. A `seed` gives it the brush's character (jitter and the grain of `paper`,
8170
8185
  * `null` for none); without one it renders plain. One undo step.
8171
8186
  */
8172
8187
  addBrushstroke(brush, options = {}) {
8173
- const clean = sanitizeVectorBrushstroke(brush);
8174
- if (!clean) return { ok: false, message: `Unknown brush "${brush?.brushId}": use one of ${vectorBrushstrokeBrushes().map((b) => b.id).join(", ")}.` };
8188
+ const user = this.brushes?.user ?? [];
8189
+ const brushError = typeof brush?.brushId === "string" ? vectorBrushstrokeBrushError(brush.brushId, user) : "brushId must be a brush id (vector.listBrushes lists them).";
8190
+ const clean = brushError ? null : sanitizeVectorBrushstroke(brush, user);
8191
+ if (!clean) return { ok: false, message: brushError ?? `Brush "${brush.brushId}" can't paint a brushstroke (vector.listBrushes lists the ones that can).` };
8175
8192
  if (brush.size !== void 0 && !(brush.size > 0 && brush.size <= 500)) return { ok: false, message: "size must be above 0 and at most 500 (document units)." };
8176
8193
  const characterError = vectorBrushstrokeCharacterError(brush);
8177
8194
  if (characterError) return { ok: false, message: characterError };
@@ -8193,7 +8210,9 @@ var VectorEditorController = class _VectorEditorController {
8193
8210
  * brushstroke. With `merge`, consecutive edits fold into one undo step.
8194
8211
  */
8195
8212
  setBrushstroke(index, patch, options = {}) {
8196
- if (patch.brushId !== void 0 && !vectorBrushstrokeBrush(patch.brushId)) return { ok: false, message: `Unknown brush "${patch.brushId}": use one of ${vectorBrushstrokeBrushes().map((b) => b.id).join(", ")}.` };
8213
+ const user = this.brushes?.user ?? [];
8214
+ const brushError = patch.brushId !== void 0 ? vectorBrushstrokeBrushError(patch.brushId, user) : null;
8215
+ if (brushError) return { ok: false, message: brushError };
8197
8216
  if (patch.size !== void 0 && patch.size !== null && !(patch.size > 0 && patch.size <= 500)) return { ok: false, message: "size must be above 0 and at most 500 (document units)." };
8198
8217
  if (patch.color !== void 0 && !parseVectorColor(patch.color)) return { ok: false, message: `"${patch.color}" is not a colour.` };
8199
8218
  if (patch.opacity !== void 0 && !(patch.opacity >= 0 && patch.opacity <= 1)) return { ok: false, message: "opacity must be from 0 to 1." };
@@ -8207,7 +8226,7 @@ var VectorEditorController = class _VectorEditorController {
8207
8226
  (stack, path) => {
8208
8227
  const layer = stack[index];
8209
8228
  if (!layer || layer.kind !== "brushstroke" || !layer.brush) return `Path "${path.id}" has no brushstroke at stroke ${index} (vector.getAppearance lists its strokes; 0 is the bottom).`;
8210
- const brush = { ...layer.brush, ...patch.brushId ? { brushId: patch.brushId } : {} };
8229
+ const brush = patch.brushId && patch.brushId !== layer.brush.brushId ? { ...layer.brush, brushId: patch.brushId, spec: void 0 } : { ...layer.brush };
8211
8230
  if (patch.size === null) delete brush.size;
8212
8231
  else if (patch.size !== void 0) brush.size = patch.size;
8213
8232
  if (patch.pressure === null) delete brush.pressure;
@@ -8215,7 +8234,7 @@ var VectorEditorController = class _VectorEditorController {
8215
8234
  if (patch.seed === null) delete brush.seed;
8216
8235
  else if (patch.seed !== void 0) brush.seed = patch.seed;
8217
8236
  if (patch.paper !== void 0) brush.paper = patch.paper;
8218
- const next = { ...layer, brush: sanitizeVectorBrushstroke(brush) ?? layer.brush, ...patch.color ? { paint: patch.color } : {}, ...patch.opacity !== void 0 ? { opacity: patch.opacity } : {} };
8237
+ const next = { ...layer, brush: sanitizeVectorBrushstroke(brush, user) ?? layer.brush, ...patch.color ? { paint: patch.color } : {}, ...patch.opacity !== void 0 ? { opacity: patch.opacity } : {} };
8219
8238
  return stack.map((l, i) => i === index ? next : l);
8220
8239
  },
8221
8240
  merge
@@ -15894,9 +15913,9 @@ function createVectorCommands(controller, options = {}) {
15894
15913
  label: "Brushstroke Brushes",
15895
15914
  readOnly: true,
15896
15915
  keywords: ["brush", "brushstroke", "pencil", "pastel", "charcoal", "ink", "marker", "natural media"],
15897
- description: "Lists the brushes a brushstroke can use (the paint editor's dry brushes): { brushes: [{ id, name, category, size }] } (size: the brush's own diameter in document units).",
15916
+ description: "Lists the brushes a brushstroke can use (the dry presets, then saved brushes from the brush library the vector profile admits): { brushes: [{ id, name, category, size, builtin }] } (size: the brush's own diameter in document units).",
15898
15917
  params: { type: "object", properties: {}, additionalProperties: false },
15899
- run: () => ({ brushes: vectorBrushstrokeBrushes().map((b) => ({ id: b.id, name: b.name, category: b.category, size: b.tip.size })) })
15918
+ run: () => ({ brushes: controller.listBrushstrokeBrushes().map((b) => ({ id: b.id, name: b.name, category: b.category, size: b.tip.size, builtin: vectorBrushstrokeBrush(b.id) !== null })) })
15900
15919
  },
15901
15920
  {
15902
15921
  ...base,
@@ -18848,6 +18867,8 @@ export {
18848
18867
  vectorBoxCorners,
18849
18868
  vectorBoxMatrix,
18850
18869
  vectorBrushstrokeBrush,
18870
+ vectorBrushstrokeBrushError,
18871
+ vectorBrushstrokeBrushOf,
18851
18872
  vectorBrushstrokeBrushes,
18852
18873
  vectorBrushstrokeCharacterError,
18853
18874
  vectorBrushstrokeImages,
@@ -2,6 +2,7 @@ import type { GenerationProvenance } from '../generation/provenance.cjs';
2
2
  import type { CanvasGradientDef } from '../canvas/gradient.cjs';
3
3
  import type { WarpMesh, WarpStyle } from '../geom/types.cjs';
4
4
  import type { GrainPattern } from '../paint/grain.cjs';
5
+ import type { Brush } from '../brushes/types.cjs';
5
6
  /** An affine matrix `[a, b, c, d, e, f]`: `x' = a·x + c·y + e`, `y' = b·x + d·y + f` (SVG order). */
6
7
  export type VectorMatrix = readonly [number, number, number, number, number, number];
7
8
  /** A point in document coordinates. */
@@ -268,7 +269,13 @@ export interface VectorBrushstrokePaper {
268
269
  * renders plain dabs, as before, and `paper` is ignored.
269
270
  */
270
271
  export interface VectorBrushstroke {
272
+ /** A preset id, or a saved brush's id from the brush library. */
271
273
  brushId: string;
274
+ /**
275
+ * The saved brush `brushId` names (a facet brush of `@pixerate/editor/brushes`), embedded when it
276
+ * is not a preset, so the document renders without the library.
277
+ */
278
+ spec?: Brush;
272
279
  size?: number;
273
280
  pressure?: number[];
274
281
  /** Jitter seed, an integer 0–2147483647. Turns the brush's character on. */
@@ -2,6 +2,7 @@ import type { GenerationProvenance } from '../generation/provenance.js';
2
2
  import type { CanvasGradientDef } from '../canvas/gradient.js';
3
3
  import type { WarpMesh, WarpStyle } from '../geom/types.js';
4
4
  import type { GrainPattern } from '../paint/grain.js';
5
+ import type { Brush } from '../brushes/types.js';
5
6
  /** An affine matrix `[a, b, c, d, e, f]`: `x' = a·x + c·y + e`, `y' = b·x + d·y + f` (SVG order). */
6
7
  export type VectorMatrix = readonly [number, number, number, number, number, number];
7
8
  /** A point in document coordinates. */
@@ -268,7 +269,13 @@ export interface VectorBrushstrokePaper {
268
269
  * renders plain dabs, as before, and `paper` is ignored.
269
270
  */
270
271
  export interface VectorBrushstroke {
272
+ /** A preset id, or a saved brush's id from the brush library. */
271
273
  brushId: string;
274
+ /**
275
+ * The saved brush `brushId` names (a facet brush of `@pixerate/editor/brushes`), embedded when it
276
+ * is not a preset, so the document renders without the library.
277
+ */
278
+ spec?: Brush;
272
279
  size?: number;
273
280
  pressure?: number[];
274
281
  /** Jitter seed, an integer 0–2147483647. Turns the brush's character on. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.254.1",
3
+ "version": "0.256.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -38,9 +38,15 @@ paints without (warnings), each with a fix; lists and writes use the current hos
38
38
  | Host | Renders |
39
39
  | --- | --- |
40
40
  | Paint editor (`paint`) | Everything. |
41
- | Image editor (`image`) | Dry dabs on round, flat and chisel tips, the Mixer Brush and the eraser; no wet or thick surface, particles, objects, light or motion. |
42
- | Canvas (`canvas`) | Dry dab brushes along a path (no tools, wet, thick, particles, objects or light); velocity is ignored. |
43
- | Vector strokes (`vector`) | As canvas, but light brushes paint as plain colour. |
41
+ | Image editor (`image`) | Dry dabs on round, flat and chisel tips, the Mixer Brush and the eraser; no wet or thick surface, particles, objects, light or motion. Pick one with `set_brush_preset`. |
42
+ | Canvas (`canvas`) | Dry dab brushes along a path (no tools, wet, thick, particles, objects or light); velocity is ignored. Draw with one as `canvas.drawFreehand` `brush`. |
43
+ | Vector strokes (`vector`) | As canvas, but light brushes paint as plain colour. Draw with one as `vector.addBrushstroke` `brushId`. |
44
+
45
+ Every host lists the presets its profile admits and the library's saved brushes it admits, and
46
+ each registers these tools with its own profile, so `list_brush_presets` shows that host's brushes.
47
+ A saved brush used in canvas or vector strokes is copied into the stroke, so the document renders
48
+ where the library is not; changing the brush in the library later doesn't change strokes already
49
+ drawn.
44
50
 
45
51
  ## Working method
46
52
 
@@ -250,8 +250,8 @@ character: seeded jitter and the paper grain they catch) and, when a width was c
250
250
 
251
251
  | Command | Use it to |
252
252
  | --- | --- |
253
- | `canvas.drawFreehand` | Draw a pen or highlighter stroke through `points` (`[x, y]` pairs in canvas units, in drawing order; one undo step). `tool` picks the defaults (`pen`: blue, width 3; `highlighter`: yellow, width 24, opacity 0.35); `color`, `width`, `opacity` override them (with a `brush`, `width` is the brush diameter and defaults to the preset's own size). Pass `brush` (a preset id from `canvas.listBrushes`, or tip settings) to paint it with a dry brush, plus `pressure` / `tiltX` / `tiltY` profiles spread along its length (`[0.2, 1, 0.3]` swells in the middle). Add `paint` (only with `brush`) for gradient paint that changes colour along the stroke: `{ "type": "gradient", "stops": [...], "map": { "by": "distance", "length": 200 } }` (length in canvas units), or `by` `pressure`, `tilt`, `direction`, `across-tip`, `random`. Returns the node `id`, its `bounds` and the stored `brush` (with `paint` normalised: defaults filled in, colours lowercased). |
254
- | `canvas.listBrushes` | Read-only: the dry paint presets `canvas.drawFreehand` and the gestures accept as `brush` (`id`, `name`, `category`, the preset's own `size`, which a stroke uses unless you pass `width`). Call it before the first brushed stroke instead of guessing ids. |
253
+ | `canvas.drawFreehand` | Draw a pen or highlighter stroke through `points` (`[x, y]` pairs in canvas units, in drawing order; one undo step). `tool` picks the defaults (`pen`: blue, width 3; `highlighter`: yellow, width 24, opacity 0.35); `color`, `width`, `opacity` override them (with a `brush`, `width` is the brush diameter and defaults to the preset's own size). Pass `brush` (a preset or saved brush id from `canvas.listBrushes`, or tip settings) to paint it with a dry brush, plus `pressure` / `tiltX` / `tiltY` profiles spread along its length (`[0.2, 1, 0.3]` swells in the middle). Add `paint` (only with `brush`) for gradient paint that changes colour along the stroke: `{ "type": "gradient", "stops": [...], "map": { "by": "distance", "length": 200 } }` (length in canvas units), or `by` `pressure`, `tilt`, `direction`, `across-tip`, `random`. Returns the node `id`, its `bounds` and the stored `brush` (with `paint` normalised: defaults filled in, colours lowercased). |
254
+ | `canvas.listBrushes` | Read-only: the brushes `canvas.drawFreehand` and the gestures accept as `brush`: the dry presets, then saved brushes from the brush library the canvas can paint (`id`, `name`, `category`, the brush's own `size`, which a stroke uses unless you pass `width`, and `builtin`). Call it before the first brushed stroke instead of guessing ids. |
255
255
 
256
256
  Gestures draw one hand-drawn stroke node each (one undo step) through the same path as
257
257
  `canvas.drawFreehand`, and the agent's cursor traces them. Targets are boxes
@@ -348,9 +348,14 @@ effects provider (for example Flair); without one the read tools say so and the
348
348
  - **A shape node holds one effect.** `apply_effect` replaces the effect that is there. Effects draw on shape nodes only (nodes with `data.shape`).
349
349
  - **Effects belong to the provider they came from.** Each saved effect keeps its provider id. After the provider changes (another effects library), check `list_applied_effects` for `available: false` and re-apply those effects with ids from the new provider's `search_effects`; the old effect ids mean nothing to it.
350
350
 
351
- - **Brushes are dry only.** Wet presets (watercolour, gouache, wet oils), mixer, blender, smudge and
352
- eraser presets are refused: wet paint needs an area to flow in, and the others need paint under
353
- them. `pressure` and tilt without a `brush` are refused too.
351
+ - **Brushes are dry only.** The canvas profile (`CANVAS_PROFILE`, see `pixerate-brushes`) refuses
352
+ wet and thick brushes (watercolour, gouache, wet oils, impasto), particle and sticker brushes,
353
+ tools (mixer, blender, smudge, eraser) and light brushes, saying why and which ids to use:
354
+ wet paint needs an area to flow in, tools need paint under them, light needs the paint editor's
355
+ light channel. Saved brushes from the brush library work when they are dry (`check_brush` with
356
+ `host: "canvas"` says, and gives a `degraded` brush to save instead); a stroke keeps a copy of
357
+ the saved brush, so it renders without the library. `pressure` and tilt without a `brush` are
358
+ refused too.
354
359
  - **Gradient paint needs a brush, and no timing.** `paint` without `brush` is refused; use
355
360
  `color` for a plain stroke. Canvas strokes record no timing, so `velocity` and `time` mappings
356
361
  are refused: use `distance` for colour that changes along the stroke. The stroke `color` still
@@ -313,7 +313,8 @@ in the source frame at the document's bit depth, colour mode and profile. Read
313
313
  | `set_paint` | Set what the brush paints: one colour (it becomes the foreground) or gradient paint (`stops` sorted by offset, `space` pigment / oklab / srgb, `map`, `softness`). Errors say what is wrong. Returns `paint`, `foreground`, `background`, `brush`. |
314
314
  | `sample_color` | The Eyedropper: read the composite colour at a document point (`size` averages a square; default 1 px) and, unless `setForeground` is false, make it the paint foreground. Use it to match a colour already in the image before painting or retouching. Fails outside the canvas or on transparent pixels. Returns `color` and `foreground`. |
315
315
  | `sample_gradient` | Read the composite along a line (`start` → `end`, document pixels) and fit the fewest gradient stops to it (`tolerance` OKLab ΔE, `maxStops`, `sampleSize` averaging, `snapStep`). Changes nothing. Returns a linear `gradient` (angle of the line, oklab), its `css` and `pointsRead`. Use it to match a sky, a backdrop or a light falloff before painting a gradient or styling one. |
316
- | `get_paint` | Read-only. What the brush paints (`paint`), the foreground and background colours and the brush settings. |
316
+ | `get_paint` | Read-only. What the brush paints (`paint`), the foreground and background colours, the brush settings, and `brushPreset` (the preset they came from, null once changed by hand). |
317
+ | `set_brush_preset` | Paint with a brush from the brush library: `id` from `list_brush_presets` (the image host lists only what it can paint). Sets the paint tool it needs (`brush`, `mixer` for a Mixer Brush, `eraser`), its brush and mixer settings and a brush's own gradient. Refuses wet, thick, particle, sticker and light brushes with the reason. Returns `tool`, `brush`, `mixer`, `paint`, `warnings` (e.g. paper grain, which the image editor paints without). Then `paint_stroke` with that tool. |
317
318
  | `get_mixer_brush` | Read-only. The Mixer Brush settings and what it carries (`reservoir` #rrggbb or null, `amount` left 0..1, `pickup` #rrggbb or null). Call it before a mixer stroke with `loadEachStroke: false`. |
318
319
  | `load_mixer_brush` | Fill the Mixer Brush reservoir with `color` (default the foreground), amount 1; keeps the pickup. View state, not an undo step. Use before strokes with `loadEachStroke: false`. Returns the load. |
319
320
  | `clean_mixer_brush` | Empty the Mixer Brush pickup well (`emptyReservoir: true` also drops the loaded paint, so it only smears). View state, not an undo step. Returns the load. |
@@ -101,58 +101,32 @@ the pitfalls, and uses the tools below.
101
101
 
102
102
  ## Best practices
103
103
 
104
- - **Big to small, dark to light (or light to dark), one decision per layer.** Name layers by
105
- purpose ("Underpainting", "Shadows", "Line"). Lower a layer's `opacity` instead of repainting.
106
- - **Pressure is your value control** on grainy media: points with `pressure` 0.2..0.4 give
107
- texture-catching, broken marks; 0.8..1 fills the grain. Vary pressure along a stroke (swell in
108
- the middle, lighten at the ends) for natural marks.
109
- - **Give points a time `t`** (milliseconds, about 8..16 ms apart for natural speed) when using an
110
- ink or brush pen: velocity dynamics thin fast strokes. Without `t` there is no velocity effect.
111
- - **Tilt** (`altitude` near 0.3..0.6 radians with an `azimuth`) turns a pencil into broad side
112
- shading; upright points draw a fine line.
113
- - **Space points** about a third of the brush size apart or closer; the brush fills in dabs
114
- between them. Long smooth curves need more points than straight lines.
115
- - **Use `rotation: "stroke"`** for flat tips (pastel, charcoal, chisel marker) so the mark turns
116
- with the path; `fixed` for calligraphic nibs.
104
+ - **Big to small, dark to light (or light to dark), one decision per layer.** Name layers by purpose ("Underpainting", "Shadows", "Line"). Lower a layer's `opacity` instead of repainting.
105
+ - **Pressure is your value control** on grainy media: points with `pressure` 0.2..0.4 give texture-catching, broken marks; 0.8..1 fills the grain. Vary pressure along a stroke (swell in the middle, lighten at the ends) for natural marks.
106
+ - **Give points a time `t`** (milliseconds, about 8..16 ms apart for natural speed) when using an ink or brush pen: velocity dynamics thin fast strokes. Without `t` there is no velocity effect.
107
+ - **Tilt** (`altitude` near 0.3..0.6 radians with an `azimuth`) turns a pencil into broad side shading; upright points draw a fine line.
108
+ - **Space points** about a third of the brush size apart or closer; the brush fills in dabs between them. Long smooth curves need more points than straight lines.
109
+ - **Use `rotation: "stroke"`** for flat tips (pastel, charcoal, chisel marker) so the mark turns with the path; `fixed` for calligraphic nibs.
117
110
  - **Blending tools**:
118
- - `blender`: `strength` 0.3..0.6 for soft transitions, `distance` 0.2..0.5 (fraction of the
119
- tip) for how far colours travel. Keep `mixing: "pigment"` for painterly blends; `rgb`
120
- blends like a soft blur. Work along an edge in short strokes rather than over everything.
121
- - `smudge`: `length` near 0.9 drags paint far (long streaks), 0.5 short pushes; `strength`
122
- 0.5..0.8. `mixing: "pigment"` (the `wet-smudge` preset) smears colours into each other like
123
- wet paint.
111
+ - `blender`: `strength` 0.3..0.6 for soft transitions, `distance` 0.2..0.5 (fraction of the tip) for how far colours travel. Keep `mixing: "pigment"` for painterly blends; `rgb` blends like a soft blur. Work along an edge in short strokes rather than over everything.
112
+ - `smudge`: `length` near 0.9 drags paint far (long streaks), 0.5 short pushes; `strength` 0.5..0.8. `mixing: "pigment"` (the `wet-smudge` preset) smears colours into each other like wet paint.
124
113
  - Mixer Brush: blend with flow 0.3..0.6; at full flow it paints only its own colour.
125
- - **Pick-up and mix ratios**: `pickup` 0.3..0.7 with `mix` 0.4..0.7 gives a brush that drags
126
- the colours it crosses into its own; `dirty: true` keeps them for the next stroke (wipe with
114
+ - **Pick-up and mix ratios**: `pickup` 0.3..0.7 with `mix` 0.4..0.7 gives a brush that drags the colours it crosses into its own; `dirty: true` keeps them for the next stroke (wipe with
127
115
  `clean_mixer_brush`). Keep `pickup` 0 for clean, predictable colour.
128
- - **Multi-colour**: give 2..8 `colors` (or none for the paint and secondary colours). `tip` mode
129
- with a flat tip and `rotation: "stroke"` paints ribbons; `softness` 0.3..0.7 keeps the stripes
130
- readable. `dab` mode with `order: "sequence"` and `length` 5..20 walks through a palette;
116
+ - **Multi-colour**: give 2..8 `colors` (or none for the paint and secondary colours). `tip` mode with a flat tip and `rotation: "stroke"` paints ribbons; `softness` 0.3..0.7 keeps the stripes readable. `dab` mode with `order: "sequence"` and `length` 5..20 walks through a palette;
131
117
  `random` for foliage and confetti.
132
- - **Pigment for paint, multiply for markers**: tip `blend: "pigment"` mixes subtractively on the
133
- canvas; `multiply` darkens where marker strokes overlap. Pigment layers are written as Normal
134
- to PSD (the export warns).
118
+ - **Pigment for paint, multiply for markers**: tip `blend: "pigment"` mixes subtractively on the canvas; `multiply` darkens where marker strokes overlap. Pigment layers are written as Normal to PSD (the export warns).
135
119
  - **Painterly layer modes do the glazing for you** (`update_layer` `blend`, or a brush tip):
136
- - `watercolor`: the layer is a transparent wash. Washes multiply wavelength by wavelength, so
137
- each glaze darkens and blue over yellow makes green. White is clear: paint glazes on a
138
- layer over the underpainting, at opacity 0.4..0.8.
139
- - `tint`: recolours what is below (its hue and chroma) and keeps its lightness, gently near
140
- white and black. Use it to warm or cool a whole passage.
141
- - `highlight`: adds coloured light that rolls off towards white instead of clipping. Use it
142
- for glints and glow; black does nothing.
143
- - `shadow`: shades as if light were blocked, coloured by the layer (paint shadows in a cool
144
- blue-violet), never crushing to black. White does nothing.
145
-
146
- PSD has none of them: `export_psd` writes the nearest standard mode (watercolor and shadow
147
- as Multiply, tint as Color, highlight as Screen) and warns; the merged image stays exact.
148
- - **Grow the canvas instead of starting big** with `extend_canvas` (pixels per side; nothing
149
- painted moves relative to anything else). Adding `left` or `top` shifts every coordinate you
150
- hold by (+left, +top): re-read `list_layers` or use the returned `rect`. Dry wet paint first.
120
+ - `watercolor`: the layer is a transparent wash. Washes multiply wavelength by wavelength, so each glaze darkens and blue over yellow makes green. White is clear: paint glazes on a layer over the underpainting, at opacity 0.4..0.8.
121
+ - `tint`: recolours what is below (its hue and chroma) and keeps its lightness, gently near white and black. Use it to warm or cool a whole passage.
122
+ - `highlight`: adds coloured light that rolls off towards white instead of clipping. Use it for glints and glow; black does nothing.
123
+ - `shadow`: shades as if light were blocked, coloured by the layer (paint shadows in a cool blue-violet), never crushing to black. White does nothing.
124
+
125
+ PSD has none of them: `export_psd` writes the nearest standard mode (watercolor and shadow as Multiply, tint as Color, highlight as Screen) and warns; the merged image stays exact.
126
+ - **Grow the canvas instead of starting big** with `extend_canvas` (pixels per side; nothing painted moves relative to anything else). Adding `left` or `top` shifts every coordinate you hold by (+left, +top): re-read `list_layers` or use the returned `rect`. Dry wet paint first.
151
127
  `set_canvas` only crops or grows at the bottom-right.
152
- - **Save brushes you build** in the brush library: `create_brush` with `basedOn` and the facets
153
- you changed (or `get_brush` then `create_brush` with its `brush`), and share them with
154
- `export_brushes` / `import_brushes`. A saved brush shows in every editor that shares the
155
- library; `set_brush` its id to paint with it.
128
+ - **Save brushes you build** in the brush library: `create_brush` with `basedOn` and the facets you changed (or `get_brush` then `create_brush` with its `brush`), and share them with
129
+ `export_brushes` / `import_brushes`. A saved brush shows in every editor that shares the library; `set_brush` its id to paint with it.
156
130
 
157
131
  Which facet sets which engine setting: [references/brush-facets.md](references/brush-facets.md).
158
132
 
@@ -272,6 +246,17 @@ is a canvas coordinate: read [references/canvas-hosted.md](references/canvas-hos
272
246
  | `get_paper` | Read every setting of the current paper (pattern, seed, scale, tiling, depth, shading, dry and wet tuning, and `metallic`, `backlight`, `backlightColor` when set). |
273
247
  | `set_paper` | Change the paper: `preset`, then any of `color`, `depth` (0..1), `scale` (0.25..8), `seed` (another sheet), `pattern`, `tiling`, `shading`, `dry` / `wet` tuning (`contrast` 0..3, `invert`), `metallic`, `backlight` (0..1, light from behind: glazes glow, opaque paint blocks; preset `stained-glass`) and `backlightColor`. Affects new strokes' grain and how the paper shows; strokes already painted keep their pixels. Returns the paper. |
274
248
 
249
+ ### Suggesting paint
250
+
251
+ Contributing while the user paints: suggest, don't paint. Read [references/painting-alongside.md](references/painting-alongside.md).
252
+
253
+ | Tool | Use |
254
+ | --- | --- |
255
+ | `propose_paint` | Suggest paint as a ghost the user accepts or dismisses: `title`, `rationale`, `ops` (`stroke` with points and a dry brush, `wash` over its layer, `echo` of stroke ids moved / turned / scaled / repeated) and where its new layer goes (`place` top, bottom or above `layerId`). Returns the proposal id, the ghost's `bounds` and the strokes it echoes. Use it for contributions the user didn't ask for. |
256
+ | `list_paint_proposals` | Read the pending paint proposals: id, title, rationale, who proposed it, op kinds, placement, bounds, the strokes it echoes and any since removed (`missing`), and whether accepting is allowed. Call before accepting or rejecting. |
257
+ | `accept_paint_proposal` | Paint a pending proposal on a new layer as one undo step recorded as the proposer's (`proposalId`). Only when the user said yes. Returns the layer id, stroke ids and undo step name. Refuses when a stroke it echoes is gone. |
258
+ | `reject_paint_proposal` | Dismiss a pending proposal (`proposalId`); nothing changes. Use it to withdraw a suggestion that no longer fits. Returns the id and title. |
259
+
275
260
  ### Layers
276
261
 
277
262
  | Tool | Use |
@@ -0,0 +1,19 @@
1
+ # Painting alongside the user
2
+
3
+ When the user is painting and you contribute on your own initiative, **suggest, don't paint**:
4
+ `propose_paint` shows your idea as a ghost over the canvas, and the user accepts or dismisses it.
5
+ Accepted, it lands on a new layer of its own as one undo step recorded as yours, and the user's
6
+ active layer stays theirs. Paint directly only when asked to.
7
+
8
+ - **Play to what agents do well**: a `wash` (a warm ground with `place: "bottom"`, a glaze over a
9
+ passage), an `echo` of the user's own strokes (grass along the horizon, rain, hatching, a
10
+ repeated motif: `list_strokes` for the ids, then `dx` / `dy` / `rotate` / `scale` / `repeat`),
11
+ or a few deliberate `stroke`s. Not a stroke-by-stroke rendering of a subject.
12
+ - **Small and complementary.** One idea per proposal, with a `title` that says what it adds and a
13
+ `rationale` that says why it fits what the user just painted.
14
+ - **Dry brushes only** (dab, particle, bristle or mixer without pickup). Suggestions are previewed
15
+ and painted on their own layer, which wet paint, thick paint and pickup can't do.
16
+ - **Withdraw what no longer fits** with `reject_paint_proposal`; an echo whose strokes the user
17
+ removed can't be accepted (`missing` in `list_paint_proposals`).
18
+
19
+ The tools are `propose_paint`, `list_paint_proposals`, `accept_paint_proposal` and `reject_paint_proposal` (see SKILL.md, Suggesting paint).
@@ -373,7 +373,7 @@ contents whole while you work, without losing the mask.
373
373
 
374
374
  | Command | Use it to |
375
375
  | --- | --- |
376
- | `vector.listBrushes` | List the natural-media brushes (pencils, pastels, charcoal, markers, inks) with their sizes. |
376
+ | `vector.listBrushes` | List the brushes a brushstroke can use: the dry presets (pencils, pastels, charcoal, markers, inks), then saved brushes from the brush library the vector profile admits, with their sizes and `builtin`. Brushes it refuses (wet, thick, particles, stickers, tools) are refused by `vector.addBrushstroke` with the reason. A saved brush is kept in the layer, so the document renders without the library. |
377
377
  | `vector.addBrushstroke` | Paint paths with a brush along their outline (`brushId`, `size`, `pressure` profile such as [0.2, 1, 0.2] for a taper, `color`): sketchy, hand-drawn or inked lines that stay editable. Pass a `seed` (any integer) so the brush gets its character, the seeded jitter and paper grain that make pencil, pastel and charcoal look real; `paper` picks the grain (`{ pattern, seed, scale, depth }`) or `null` turns it off. Returns the stroke stacks. |
378
378
  | `vector.setBrushstroke` | Change brushstroke layer `index` of path `id`: brush, size, pressure, colour, opacity, `seed` (a new seed is a new take on the same stroke; null: plain dabs) and `paper` (replaced whole; null: no grain; {}: the default sanded paper). Remove it with `vector.removeStroke`. |
379
379