@pixerate/editor 0.251.0 → 0.253.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 (58) hide show
  1. package/dist/brushes/index.cjs +240 -1344
  2. package/dist/brushes/index.d.cts +10 -1
  3. package/dist/brushes/index.d.ts +10 -1
  4. package/dist/brushes/index.js +250 -1354
  5. package/dist/brushes/library.d.cts +104 -0
  6. package/dist/brushes/library.d.ts +104 -0
  7. package/dist/brushes/patch.d.cts +23 -0
  8. package/dist/brushes/patch.d.ts +23 -0
  9. package/dist/brushes/profiles.d.cts +85 -0
  10. package/dist/brushes/profiles.d.ts +85 -0
  11. package/dist/brushes/tools.d.cts +37 -0
  12. package/dist/brushes/tools.d.ts +37 -0
  13. package/dist/brushes/ui.d.cts +105 -0
  14. package/dist/brushes/ui.d.ts +105 -0
  15. package/dist/chunk-34KLWUH4.cjs +2284 -0
  16. package/dist/chunk-QPHNVZHO.js +2284 -0
  17. package/dist/paint-editor/brush-controls.d.cts +57 -0
  18. package/dist/paint-editor/brush-controls.d.ts +57 -0
  19. package/dist/paint-editor/brush-library-tools.d.cts +15 -0
  20. package/dist/paint-editor/brush-library-tools.d.ts +15 -0
  21. package/dist/paint-editor/brush-library.d.cts +29 -0
  22. package/dist/paint-editor/brush-library.d.ts +29 -0
  23. package/dist/paint-editor/controller.d.cts +76 -17
  24. package/dist/paint-editor/controller.d.ts +76 -17
  25. package/dist/paint-editor/index.cjs +537 -852
  26. package/dist/paint-editor/index.d.cts +1 -0
  27. package/dist/paint-editor/index.d.ts +1 -0
  28. package/dist/paint-editor/index.js +327 -642
  29. package/dist/paint-editor/serialize.d.cts +7 -4
  30. package/dist/paint-editor/serialize.d.ts +7 -4
  31. package/dist/paint-editor/sticker-tools.d.cts +1 -1
  32. package/dist/paint-editor/sticker-tools.d.ts +1 -1
  33. package/dist/paint-editor/strokes.d.cts +10 -1
  34. package/dist/paint-editor/strokes.d.ts +10 -1
  35. package/dist/paint-editor/tools.d.cts +1 -1
  36. package/dist/paint-editor/tools.d.ts +1 -1
  37. package/dist/paint-editor/ui.d.cts +3 -51
  38. package/dist/paint-editor/ui.d.ts +3 -51
  39. package/dist/paint-editor/wet-tools.d.cts +1 -1
  40. package/dist/paint-editor/wet-tools.d.ts +1 -1
  41. package/dist/tools/skill-evals.d.cts +8 -1
  42. package/dist/tools/skill-evals.d.ts +8 -1
  43. package/package.json +1 -1
  44. package/skills/README.md +2 -0
  45. package/skills/pixerate-brushes/SKILL.md +106 -0
  46. package/skills/pixerate-brushes-design/SKILL.md +88 -0
  47. package/skills/pixerate-brushes-design/evals/canvas-chalk.json +15 -0
  48. package/skills/pixerate-brushes-design/evals/pink-highlighter.json +15 -0
  49. package/skills/pixerate-brushes-design/evals/port-to-vector.json +17 -0
  50. package/skills/pixerate-paint-editor/SKILL.md +26 -24
  51. package/skills/pixerate-paint-editor/references/brush-facets.md +30 -0
  52. package/skills/pixerate-paint-editor/references/gradient-paint.md +1 -1
  53. package/skills/pixerate-paint-editor/references/light-paint.md +2 -2
  54. package/skills/pixerate-paint-editor/references/oils.md +1 -1
  55. package/skills/pixerate-paint-editor/references/particle-brushes.md +8 -5
  56. package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -1
  57. package/skills/pixerate-stickers/SKILL.md +1 -1
  58. package/skills/pixerate-video-editor/SKILL.md +1 -1
@@ -9,11 +9,14 @@
9
9
  * as new assets keyed by layer id without breaking older readers (see `PaintDocumentExtensions`).
10
10
  */
11
11
  import { type ImageEditorAssetStore, type SerializedLayer } from '../image-editor/layers/serialize.cjs';
12
- import { type RecordedStroke } from './strokes.cjs';
12
+ import { strokeToJSON } from './strokes.cjs';
13
13
  import { type PaintDocumentState } from './document.cjs';
14
14
  import { type PaperSettings } from './paper.cjs';
15
15
  export declare const PAINT_FORMAT: "pixerate-paint";
16
- export declare const PAINT_SCHEMA_VERSION: 1;
16
+ /** 2: recorded strokes keep facet brushes (version 1 documents, with flat engine brushes, still load). */
17
+ export declare const PAINT_SCHEMA_VERSION: 2;
18
+ /** A recorded stroke as a document stores it (`strokeToJSON`). */
19
+ export type SerializedStroke = ReturnType<typeof strokeToJSON>;
17
20
  /** Where a host keeps layer PNGs outside the JSON (the image editor's asset store contract). */
18
21
  export type PaintAssetStore = ImageEditorAssetStore;
19
22
  /**
@@ -34,8 +37,8 @@ export interface SerializedPaintDocument {
34
37
  /** Asset id → PNG data URL (or an external reference). */
35
38
  assets: Record<string, string>;
36
39
  activeLayerId: string | null;
37
- /** Recorded strokes (strokes are data), oldest first; missing when saved without them. */
38
- strokes?: RecordedStroke[];
40
+ /** Recorded strokes (strokes are data), oldest first, each brush a facet `Brush`; missing when saved without them. */
41
+ strokes?: SerializedStroke[];
39
42
  /** Everything else (document-level state of later phases), written back as read. */
40
43
  [extension: string]: unknown;
41
44
  }
@@ -9,11 +9,14 @@
9
9
  * as new assets keyed by layer id without breaking older readers (see `PaintDocumentExtensions`).
10
10
  */
11
11
  import { type ImageEditorAssetStore, type SerializedLayer } from '../image-editor/layers/serialize.js';
12
- import { type RecordedStroke } from './strokes.js';
12
+ import { strokeToJSON } from './strokes.js';
13
13
  import { type PaintDocumentState } from './document.js';
14
14
  import { type PaperSettings } from './paper.js';
15
15
  export declare const PAINT_FORMAT: "pixerate-paint";
16
- export declare const PAINT_SCHEMA_VERSION: 1;
16
+ /** 2: recorded strokes keep facet brushes (version 1 documents, with flat engine brushes, still load). */
17
+ export declare const PAINT_SCHEMA_VERSION: 2;
18
+ /** A recorded stroke as a document stores it (`strokeToJSON`). */
19
+ export type SerializedStroke = ReturnType<typeof strokeToJSON>;
17
20
  /** Where a host keeps layer PNGs outside the JSON (the image editor's asset store contract). */
18
21
  export type PaintAssetStore = ImageEditorAssetStore;
19
22
  /**
@@ -34,8 +37,8 @@ export interface SerializedPaintDocument {
34
37
  /** Asset id → PNG data URL (or an external reference). */
35
38
  assets: Record<string, string>;
36
39
  activeLayerId: string | null;
37
- /** Recorded strokes (strokes are data), oldest first; missing when saved without them. */
38
- strokes?: RecordedStroke[];
40
+ /** Recorded strokes (strokes are data), oldest first, each brush a facet `Brush`; missing when saved without them. */
41
+ strokes?: SerializedStroke[];
39
42
  /** Everything else (document-level state of later phases), written back as read. */
40
43
  [extension: string]: unknown;
41
44
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Agent tools for stickers: stamps sprayed with a sticker brush (`paint_stroke`, list_brushes
2
+ * Agent tools for stickers: stamps sprayed with a sticker brush (`paint_stroke`, list_brush_presets
3
3
  * category sticker) stay objects on their layer, which these tools list, move, turn, resize,
4
4
  * recolour, reorder and delete, find by point (`sticker_at`), and places new ones exactly (`add_stickers`). Each change is one undo step and redraws the layer.
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Agent tools for stickers: stamps sprayed with a sticker brush (`paint_stroke`, list_brushes
2
+ * Agent tools for stickers: stamps sprayed with a sticker brush (`paint_stroke`, list_brush_presets
3
3
  * category sticker) stay objects on their layer, which these tools list, move, turn, resize,
4
4
  * recolour, reorder and delete, find by point (`sticker_at`), and places new ones exactly (`add_stickers`). Each change is one undo step and redraws the layer.
5
5
  */
@@ -2,8 +2,13 @@
2
2
  * Strokes as data. Every committed stroke is recorded with the brush, colour, raw pen samples,
3
3
  * jitter seed and symmetry copies it was painted with, so it replays to the same pixels, and can
4
4
  * be replayed moved, scaled, rotated, recoloured or with another brush (`replay_strokes`).
5
+ *
6
+ * In memory a stroke keeps its brush as the engine painted it. Saved, the brush is a facet brush
7
+ * (`strokeToJSON`, documents of version 2); strokes saved with the flat engine brush (version 1)
8
+ * still read, and both replay to the same pixels.
5
9
  */
6
10
  import { type PaintBrush } from './brushes.cjs';
11
+ import type { Brush } from '../brushes/types.cjs';
7
12
  import type { PenPoint, StrokeCopy } from './engine.cjs';
8
13
  import type { MixerLoad } from '../paint/exchange.cjs';
9
14
  import { type Stencil } from './stencils.cjs';
@@ -46,7 +51,11 @@ export declare function invertAffine(m: Affine): Affine;
46
51
  export declare function mapPenPoint(m: Affine, p: PenPoint): PenPoint;
47
52
  /** An affine copy as the engine's `StrokeCopy`. */
48
53
  export declare function affineCopy(m: Affine): StrokeCopy;
49
- /** A recorded stroke read from JSON, or `null` when it is not one. */
54
+ /** A recorded stroke as saved: plain JSON with its brush as a facet brush. */
55
+ export declare function strokeToJSON(stroke: RecordedStroke): Omit<RecordedStroke, 'brush'> & {
56
+ brush: Brush;
57
+ };
58
+ /** A recorded stroke read from JSON (its brush version 1 or 2), or `null` when it is not one. */
50
59
  export declare function normalizeBrushStroke(raw: unknown): RecordedStroke | null;
51
60
  /** The bounding box of a stroke's points. */
52
61
  export declare function strokeBounds(points: readonly PenPoint[]): {
@@ -2,8 +2,13 @@
2
2
  * Strokes as data. Every committed stroke is recorded with the brush, colour, raw pen samples,
3
3
  * jitter seed and symmetry copies it was painted with, so it replays to the same pixels, and can
4
4
  * be replayed moved, scaled, rotated, recoloured or with another brush (`replay_strokes`).
5
+ *
6
+ * In memory a stroke keeps its brush as the engine painted it. Saved, the brush is a facet brush
7
+ * (`strokeToJSON`, documents of version 2); strokes saved with the flat engine brush (version 1)
8
+ * still read, and both replay to the same pixels.
5
9
  */
6
10
  import { type PaintBrush } from './brushes.js';
11
+ import type { Brush } from '../brushes/types.js';
7
12
  import type { PenPoint, StrokeCopy } from './engine.js';
8
13
  import type { MixerLoad } from '../paint/exchange.js';
9
14
  import { type Stencil } from './stencils.js';
@@ -46,7 +51,11 @@ export declare function invertAffine(m: Affine): Affine;
46
51
  export declare function mapPenPoint(m: Affine, p: PenPoint): PenPoint;
47
52
  /** An affine copy as the engine's `StrokeCopy`. */
48
53
  export declare function affineCopy(m: Affine): StrokeCopy;
49
- /** A recorded stroke read from JSON, or `null` when it is not one. */
54
+ /** A recorded stroke as saved: plain JSON with its brush as a facet brush. */
55
+ export declare function strokeToJSON(stroke: RecordedStroke): Omit<RecordedStroke, 'brush'> & {
56
+ brush: Brush;
57
+ };
58
+ /** A recorded stroke read from JSON (its brush version 1 or 2), or `null` when it is not one. */
50
59
  export declare function normalizeBrushStroke(raw: unknown): RecordedStroke | null;
51
60
  /** The bounding box of a stroke's points. */
52
61
  export declare function strokeBounds(points: readonly PenPoint[]): {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent tools for the paint editor (`createPaintEditorToolProvider`). Strict schemas, every
3
3
  * argument described, all four annotations. Each write is one undo step (or editor state, said so)
4
- * and returns what it changed; ids come from the read tools (`list_layers`, `list_brushes`,
4
+ * and returns what it changed; ids come from the read tools (`list_layers`, the brush library's `list_brush_presets`,
5
5
  * `list_papers`, `list_strokes`).
6
6
  */
7
7
  import type { AgentToolProvider } from '../tools/registry.cjs';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Agent tools for the paint editor (`createPaintEditorToolProvider`). Strict schemas, every
3
3
  * argument described, all four annotations. Each write is one undo step (or editor state, said so)
4
- * and returns what it changed; ids come from the read tools (`list_layers`, `list_brushes`,
4
+ * and returns what it changed; ids come from the read tools (`list_layers`, the brush library's `list_brush_presets`,
5
5
  * `list_papers`, `list_strokes`).
6
6
  */
7
7
  import type { AgentToolProvider } from '../tools/registry.js';
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { MixerLoad } from '../paint/exchange.cjs';
12
12
  import { type BlendModeInfo } from '../raster/blend.cjs';
13
- import type { ImpastoTool, PaintBrush, PaintBrushInput, WetMediumKind, WetTool } from './brushes.cjs';
13
+ import type { ImpastoTool, WetMediumKind, WetTool } from './brushes.cjs';
14
14
  import type { PaintEditorController } from './controller.cjs';
15
15
  import type { PenPoint } from './engine.cjs';
16
16
  import { type Point2 } from './guides.cjs';
@@ -133,15 +133,8 @@ export declare function guideOverlay(controller: PaintEditorController, drag?: {
133
133
  to: Point2;
134
134
  kind: GuideOverlayKind;
135
135
  }>;
136
- /** One slider of a panel: a path into the brush (or paper) settings. */
137
- export interface SliderControl {
138
- path: string;
139
- label: string;
140
- min: number;
141
- max: number;
142
- step: number;
143
- }
144
- export declare const BRUSH_SLIDERS: readonly SliderControl[];
136
+ export { BRUSH_SLIDERS, brushEngineControls, multiColorSliders, patchBrushValue, type BrushEngineControls, type SliderControl } from './brush-controls.cjs';
137
+ import type { SliderControl } from './brush-controls.cjs';
145
138
  export declare const PAPER_SLIDERS: readonly SliderControl[];
146
139
  /** Reads `a.b` from an object. */
147
140
  export declare function readPath(obj: unknown, path: string): number;
@@ -174,47 +167,6 @@ export interface BlendMenuGroup {
174
167
  */
175
168
  export declare const BLEND_MENU_GROUPS: readonly BlendMenuGroup[];
176
169
  /** The engine-specific part of the brush panel: which sliders and toggles the active brush has. */
177
- export interface BrushEngineControls {
178
- /** Section heading. */
179
- title: string;
180
- sliders: readonly SliderControl[];
181
- /** Where the RGB / pigment choice is stored, or `null` when the engine has none. */
182
- mixingPath: 'behavior.mixing' | 'blender.mixing' | 'smudge.mixing' | null;
183
- /** The dirty-brush toggle (`behavior.dirty`). */
184
- dirty: boolean;
185
- /** The multi-colour controls (`behavior.multicolor`; the panels show the brush gradient, `brush.paint`, instead). */
186
- multicolor: boolean;
187
- /** Load / Clean buttons and a swatch of what the brush carries. */
188
- load: boolean;
189
- /** The wet tool menu (`wet.tool`, `WET_TOOLS`) and medium menu (`wet.medium`, `WET_MEDIA`). */
190
- wetTool?: boolean;
191
- /** The impasto tool menu (`impasto.tool`, `IMPASTO_TOOLS`). */
192
- impastoTool?: boolean;
193
- /**
194
- * The particle menus: kind and render (`particle.kind`, `particle.render`), ribbon pairs
195
- * (`particle.ribbonPairs`), and the textured-dot stamps, order and rotation
196
- * (`particle.dotTextures` up to `MAX_DOT_TEXTURES` of `STAMP_KINDS`, `dotTextureOrder`, `dotRotation`).
197
- */
198
- particleMenus?: boolean;
199
- /** The sticker picker for the brush's stickers (`sticker.stamps`, refs from the sticker library; `toggleStickerRef`). */
200
- stickerStamps?: boolean;
201
- /** Light paint: the "light follows pressure" toggle (`light.pressure`); the Light slider is in `sliders`. */
202
- light?: boolean;
203
- }
204
- /**
205
- * The controls the active brush's engine adds to the brush panel (`null` for the eraser):
206
- * blender strength and blend distance; smudge strength and length; the Mixer Brush's pick-up,
207
- * mix and load; a dab brush's pick-up and mix ratios. Every engine but the eraser has an RGB /
208
- * pigment choice; dab and mixer brushes also have the dirty and multi-colour controls. Pick-up
209
- * and mix always go through `behavior`, which keeps a Mixer Brush's `mixer` block in sync.
210
- */
211
- export declare function brushEngineControls(brush: Pick<PaintBrush, 'engine'> & {
212
- wet?: PaintBrush['wet'];
213
- }): BrushEngineControls | null;
214
- /** The multi-colour sliders for a mode: softness, and dabs per colour in `dab` mode. */
215
- export declare function multiColorSliders(mode: string): readonly SliderControl[];
216
- /** A brush change setting `path` (e.g. `smudge.mixing`, `behavior.dirty`) to any value, keeping the rest of its block. */
217
- export declare function patchBrushValue(brush: PaintBrush, path: string, value: unknown): PaintBrushInput;
218
170
  /** What the brush carries (picked-up paint, else loaded paint) as #rrggbb, or `null` when it is clean and empty. */
219
171
  export declare function brushLoadColor(load: MixerLoad): string | null;
220
172
  /** Sample sizes (square, in pad pixels) for picking from the mixing pad. */
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { MixerLoad } from '../paint/exchange.js';
12
12
  import { type BlendModeInfo } from '../raster/blend.js';
13
- import type { ImpastoTool, PaintBrush, PaintBrushInput, WetMediumKind, WetTool } from './brushes.js';
13
+ import type { ImpastoTool, WetMediumKind, WetTool } from './brushes.js';
14
14
  import type { PaintEditorController } from './controller.js';
15
15
  import type { PenPoint } from './engine.js';
16
16
  import { type Point2 } from './guides.js';
@@ -133,15 +133,8 @@ export declare function guideOverlay(controller: PaintEditorController, drag?: {
133
133
  to: Point2;
134
134
  kind: GuideOverlayKind;
135
135
  }>;
136
- /** One slider of a panel: a path into the brush (or paper) settings. */
137
- export interface SliderControl {
138
- path: string;
139
- label: string;
140
- min: number;
141
- max: number;
142
- step: number;
143
- }
144
- export declare const BRUSH_SLIDERS: readonly SliderControl[];
136
+ export { BRUSH_SLIDERS, brushEngineControls, multiColorSliders, patchBrushValue, type BrushEngineControls, type SliderControl } from './brush-controls.js';
137
+ import type { SliderControl } from './brush-controls.js';
145
138
  export declare const PAPER_SLIDERS: readonly SliderControl[];
146
139
  /** Reads `a.b` from an object. */
147
140
  export declare function readPath(obj: unknown, path: string): number;
@@ -174,47 +167,6 @@ export interface BlendMenuGroup {
174
167
  */
175
168
  export declare const BLEND_MENU_GROUPS: readonly BlendMenuGroup[];
176
169
  /** The engine-specific part of the brush panel: which sliders and toggles the active brush has. */
177
- export interface BrushEngineControls {
178
- /** Section heading. */
179
- title: string;
180
- sliders: readonly SliderControl[];
181
- /** Where the RGB / pigment choice is stored, or `null` when the engine has none. */
182
- mixingPath: 'behavior.mixing' | 'blender.mixing' | 'smudge.mixing' | null;
183
- /** The dirty-brush toggle (`behavior.dirty`). */
184
- dirty: boolean;
185
- /** The multi-colour controls (`behavior.multicolor`; the panels show the brush gradient, `brush.paint`, instead). */
186
- multicolor: boolean;
187
- /** Load / Clean buttons and a swatch of what the brush carries. */
188
- load: boolean;
189
- /** The wet tool menu (`wet.tool`, `WET_TOOLS`) and medium menu (`wet.medium`, `WET_MEDIA`). */
190
- wetTool?: boolean;
191
- /** The impasto tool menu (`impasto.tool`, `IMPASTO_TOOLS`). */
192
- impastoTool?: boolean;
193
- /**
194
- * The particle menus: kind and render (`particle.kind`, `particle.render`), ribbon pairs
195
- * (`particle.ribbonPairs`), and the textured-dot stamps, order and rotation
196
- * (`particle.dotTextures` up to `MAX_DOT_TEXTURES` of `STAMP_KINDS`, `dotTextureOrder`, `dotRotation`).
197
- */
198
- particleMenus?: boolean;
199
- /** The sticker picker for the brush's stickers (`sticker.stamps`, refs from the sticker library; `toggleStickerRef`). */
200
- stickerStamps?: boolean;
201
- /** Light paint: the "light follows pressure" toggle (`light.pressure`); the Light slider is in `sliders`. */
202
- light?: boolean;
203
- }
204
- /**
205
- * The controls the active brush's engine adds to the brush panel (`null` for the eraser):
206
- * blender strength and blend distance; smudge strength and length; the Mixer Brush's pick-up,
207
- * mix and load; a dab brush's pick-up and mix ratios. Every engine but the eraser has an RGB /
208
- * pigment choice; dab and mixer brushes also have the dirty and multi-colour controls. Pick-up
209
- * and mix always go through `behavior`, which keeps a Mixer Brush's `mixer` block in sync.
210
- */
211
- export declare function brushEngineControls(brush: Pick<PaintBrush, 'engine'> & {
212
- wet?: PaintBrush['wet'];
213
- }): BrushEngineControls | null;
214
- /** The multi-colour sliders for a mode: softness, and dabs per colour in `dab` mode. */
215
- export declare function multiColorSliders(mode: string): readonly SliderControl[];
216
- /** A brush change setting `path` (e.g. `smudge.mixing`, `behavior.dirty`) to any value, keeping the rest of its block. */
217
- export declare function patchBrushValue(brush: PaintBrush, path: string, value: unknown): PaintBrushInput;
218
170
  /** What the brush carries (picked-up paint, else loaded paint) as #rrggbb, or `null` when it is clean and empty. */
219
171
  export declare function brushLoadColor(load: MixerLoad): string | null;
220
172
  /** Sample sizes (square, in pad pixels) for picking from the mixing pad. */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent tools for wet media (watercolour and gouache on a simulated wet layer). Wet strokes are
3
- * painted with `paint_stroke` and a wet brush (`list_brushes` category `watercolor` or
3
+ * painted with `paint_stroke` and a wet brush (`list_brush_presets` variety `watercolor` or
4
4
  * `wet-tool`); these tools read and steer the wet paint. Every result says whether paint is still
5
5
  * wet. Settings (wet parameters, flow maps, tilt) are saved with the document but are not undo
6
6
  * steps; drying and wetting the paper are.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent tools for wet media (watercolour and gouache on a simulated wet layer). Wet strokes are
3
- * painted with `paint_stroke` and a wet brush (`list_brushes` category `watercolor` or
3
+ * painted with `paint_stroke` and a wet brush (`list_brush_presets` variety `watercolor` or
4
4
  * `wet-tool`); these tools read and steer the wet paint. Every result says whether paint is still
5
5
  * wet. Settings (wet parameters, flow maps, tilt) are saved with the document but are not undo
6
6
  * steps; drying and wetting the paper are.
@@ -20,7 +20,7 @@ export interface SkillEvalCheck extends SkillEvalToolCall {
20
20
  }
21
21
  export interface SkillEval {
22
22
  prompt: string;
23
- fixture: SpreadsheetEvalFixture | ImageEvalFixture | VectorEvalFixture | PaintEvalFixture | VideoEvalFixture | ChartEvalFixture | DiagramEvalFixture | RichTextEvalFixture | AnimationEvalFixture | GenerationEvalFixture | EffectEvalFixture | CanvasEvalFixture;
23
+ fixture: SpreadsheetEvalFixture | ImageEvalFixture | VectorEvalFixture | PaintEvalFixture | VideoEvalFixture | ChartEvalFixture | DiagramEvalFixture | RichTextEvalFixture | AnimationEvalFixture | GenerationEvalFixture | EffectEvalFixture | CanvasEvalFixture | BrushesEvalFixture;
24
24
  checks: SkillEvalCheck[];
25
25
  solution: SkillEvalToolCall[];
26
26
  }
@@ -116,6 +116,13 @@ export interface CanvasEvalFixture {
116
116
  /** Tool calls that prepare the canvas (components, instances), run in order. */
117
117
  setup?: SkillEvalToolCall[];
118
118
  }
119
+ export interface BrushesEvalFixture {
120
+ editor: 'brushes';
121
+ /** The host profile the brush library tools run for: paint (default), image, canvas or vector. */
122
+ host?: 'paint' | 'image' | 'canvas' | 'vector';
123
+ /** Tool calls that prepare the library (saved or imported brushes), run in order. */
124
+ setup?: SkillEvalToolCall[];
125
+ }
119
126
  /** One eval file of a skill. */
120
127
  export interface SkillEvalFile {
121
128
  skill: string;
@@ -20,7 +20,7 @@ export interface SkillEvalCheck extends SkillEvalToolCall {
20
20
  }
21
21
  export interface SkillEval {
22
22
  prompt: string;
23
- fixture: SpreadsheetEvalFixture | ImageEvalFixture | VectorEvalFixture | PaintEvalFixture | VideoEvalFixture | ChartEvalFixture | DiagramEvalFixture | RichTextEvalFixture | AnimationEvalFixture | GenerationEvalFixture | EffectEvalFixture | CanvasEvalFixture;
23
+ fixture: SpreadsheetEvalFixture | ImageEvalFixture | VectorEvalFixture | PaintEvalFixture | VideoEvalFixture | ChartEvalFixture | DiagramEvalFixture | RichTextEvalFixture | AnimationEvalFixture | GenerationEvalFixture | EffectEvalFixture | CanvasEvalFixture | BrushesEvalFixture;
24
24
  checks: SkillEvalCheck[];
25
25
  solution: SkillEvalToolCall[];
26
26
  }
@@ -116,6 +116,13 @@ export interface CanvasEvalFixture {
116
116
  /** Tool calls that prepare the canvas (components, instances), run in order. */
117
117
  setup?: SkillEvalToolCall[];
118
118
  }
119
+ export interface BrushesEvalFixture {
120
+ editor: 'brushes';
121
+ /** The host profile the brush library tools run for: paint (default), image, canvas or vector. */
122
+ host?: 'paint' | 'image' | 'canvas' | 'vector';
123
+ /** Tool calls that prepare the library (saved or imported brushes), run in order. */
124
+ setup?: SkillEvalToolCall[];
125
+ }
119
126
  /** One eval file of a skill. */
120
127
  export interface SkillEvalFile {
121
128
  skill: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.251.0",
3
+ "version": "0.253.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
package/skills/README.md CHANGED
@@ -30,6 +30,7 @@ model and its general practice.
30
30
  | [`pixerate-color`](pixerate-color/SKILL.md) | Colour inspection, WCAG accessibility and contrast optimization, CVD simulation, palette audits, harmonies, and OKLab/OKLCH blends. |
31
31
  | [`pixerate-typography`](pixerate-typography/SKILL.md) | Fonts and fallback stacks, font loading, modular type scales and hierarchies, fluid sizes, readability and type-aware contrast audits, text segmentation, the font picker. |
32
32
  | [`pixerate-stickers`](pixerate-stickers/SKILL.md) | Sticker libraries: finding stickers by ref (`provider:id`), categories, tags, tintable art and licences, for the editors that spray or place them. |
33
+ | [`pixerate-brushes`](pixerate-brushes/SKILL.md) | The shared brush library: brushes made of facets (medium, tip, marks, dynamics, behaviour, light, motion, colour), the presets, host profiles for paint, image, canvas and vector, and creating, checking and sharing brushes. |
33
34
 
34
35
  ## Workflow skills
35
36
 
@@ -51,6 +52,7 @@ request like "clean up this export" finds it.
51
52
  | [`pixerate-paint-watercolour`](pixerate-paint-watercolour/SKILL.md) | Paint (`pixerate-paint-editor`) | Watercolour paper, light to dark with the paper's white kept, wet-in-wet and wet-on-dry washes, drying before glazing, steering the water. |
52
53
  | [`pixerate-paint-oil-impasto`](pixerate-paint-oil-impasto/SKILL.md) | Paint (`pixerate-paint-editor`) | A toned ground, thin block-in to thick paint, pigment mixing, impasto relief and its light. |
53
54
  | [`pixerate-paint-sketch-to-finish`](pixerate-paint-sketch-to-finish/SKILL.md) | Paint (`pixerate-paint-editor`) | Sketch, ink, flats under the lines, clipped shading and light, one named layer per pass. |
55
+ | [`pixerate-brushes-design`](pixerate-brushes-design/SKILL.md) | Brushes (`pixerate-brushes`) | From a description to a saved brush: the nearest preset, only the facets that change, checked for the host that will use it, previewed. |
54
56
  | [`pixerate-video-social-cut`](pixerate-video-social-cut/SKILL.md) | Video (`pixerate-video-editor`) | Vertical or square reframes that fill the frame, the hook first, platform lengths, titles in the safe area. |
55
57
  | [`pixerate-video-colour-grade`](pixerate-video-colour-grade/SKILL.md) | Video (`pixerate-video-editor`) | Correct and match shots, then one look on an adjustment clip; LUTs at moderate strength with skin protected. |
56
58
  | [`pixerate-video-montage`](pixerate-video-montage/SKILL.md) | Video (`pixerate-video-editor`) | Selects on the beat, varied pacing, short transitions with handles, music under everything and a clean ending. |
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: pixerate-brushes
3
+ description: How to find, describe, create, change, check and share brushes in the Pixerate brush library through list_brush_presets, describe_brush, create_brush, update_brush, duplicate_brush, remove_brush, check_brush, export_brushes, import_brushes and preview_brush - brushes made of facets (kind, medium and variety or tool, tip, marks, dynamics, behaviour, light, motion, colour), the built-in presets, and the host profiles that say what the paint editor, image editor, canvas and vector strokes can render. Use when asked to make, design, tweak, save, copy, delete, export, import or check a brush, find a brush for a medium (pencil, ink, watercolour, oil, airbrush, highlighter), or make a brush work in another editor.
4
+ ---
5
+
6
+ # Brushes
7
+
8
+ A brush is made of **facets** that say what it is and how it behaves, not which engine draws it.
9
+ Only `id`, `name`, `kind` and `tip.shape` are required; every other field takes the default of
10
+ the engine the facets pick, so a brush stores only what makes it what it is. The library holds the
11
+ built-in presets (read-only) and the user's brushes, shared by every editor that paints.
12
+
13
+ | Facet (field) | What it holds |
14
+ | --- | --- |
15
+ | Kind | `mark` lays a material; `tool` works on paint already down. |
16
+ | Medium and variety | Marks: what it lays (`paint`, `ink`, `dry`, `spray`) and what kind (`oil`, `watercolor`, `gouache`, `pencil`, `charcoal`, `airbrush`, `marker`, `highlighter`…). Descriptive, for finding brushes; a wet brush whose variety is `watercolor`, `gouache` or `oil` paints that medium. |
17
+ | Tool | Tools: `eraser`, `blender`, `smudge`, and on wet paint `water`, `dryer`, `blow`, `blot`, `wet-blend`, `wet-drag`. |
18
+ | Tip | `shape` (`round`, `flat`, `chisel` are ellipses set by `roundness` and `angle`; `bristle` has a tuft; `stamp` lays stickers), size in px, hardness, spacing, opacity, flow, blend mode. |
19
+ | Marks | `continuous` dabs (default), `particles` (strands, springs, ribbons, sparks) or `objects` (each mark an editable sticker). |
20
+ | Dynamics | Pen feel: pressure to size and opacity, smoothing, tilt, velocity, jitter, rotation. |
21
+ | Behaviour | `surface` `dry`, `wet` (`wet` settings) or `thick` (`thick` impasto settings); paper `grain`; `exchange` (pick-up and mix; a `reservoir` makes a Mixer Brush); tool `strength`. |
22
+ | Light | Light the brush gives off (`amount`), plus suggested layer `spill` and `glow`. |
23
+ | Motion | Animation after painting (live effect paint, particle `waveSpeed`). |
24
+ | Colour | A `default` colour, a `palette` of up to 16, `own` (always its own colour). A gradient default is always painted; a solid one is a suggestion. |
25
+
26
+ How media map to facets: a **pencil** or **charcoal** is `dry` with high `grain` and tilt;
27
+ **ink** is a hard round tip with pressure to size (a pen thins with velocity); **watercolour**
28
+ and **wet oil** are `paint` on `surface: 'wet'`; **impasto** is `surface: 'thick'`; an
29
+ **airbrush** is `spray`, soft and low flow; a **highlighter** is `ink` / `highlighter`, a
30
+ multiply chisel tip with a little light and `spill` radius 0; **particle** and **sticker**
31
+ brushes are `marks` particles or objects on a `stamp` tip.
32
+
33
+ ## Host profiles
34
+
35
+ Each host renders a part of this. `check_brush` says what a host refuses (errors) and what it
36
+ paints without (warnings), each with a fix; lists and writes use the current host.
37
+
38
+ | Host | Renders |
39
+ | --- | --- |
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. |
44
+
45
+ ## Working method
46
+
47
+ 1. **Find the nearest brush.** `list_brush_presets` with `medium`, `variety`, `kind` or `text`.
48
+ It lists only what this host can render (`all: true` lists the rest with `renderable`).
49
+ 2. **Read it.** `describe_brush` for the full spec, the engine it resolves to and its check.
50
+ 3. **Make a brush.** `create_brush` with a new `id`, a `name`, `basedOn` the nearest preset and
51
+ only the facets that change (objects merge; lists replace). Read `problems` (values clamped or
52
+ dropped) and `warnings`. A brush the host can't render is refused and nothing is saved.
53
+ 4. **Look at it.** `preview_brush` when the host has a renderer; otherwise paint a test stroke
54
+ in the editor (`set_brush` with the new id, then `paint_stroke`).
55
+ 5. **Tune it.** `update_brush` with the facets to change, or `unset` to drop whole facets. It
56
+ returns the leaf paths `changed`. Built-in presets can't be changed: `duplicate_brush` first.
57
+ 6. **Share it.** `export_brushes` writes a library file; `import_brushes` adds one (version 1
58
+ files of the paint editor's older brushes are migrated). `check_brush` with `host` before
59
+ handing a brush to another editor.
60
+
61
+ ## Best practices
62
+
63
+ - Start from the nearest preset and change two or three facets; a brush from nothing gets
64
+ engine defaults that rarely suit a medium.
65
+ - Test on the paper and colour the brush will be used on: grain response and glazes look
66
+ different on smooth and rough paper.
67
+ - Give light brushes dark paper, and give highlighters a `light.spill` radius of 0 so the
68
+ glow stays on the stroke.
69
+ - Keep `variety` truthful (it is how people find the brush) and add `tags` for the job
70
+ (`lettering`, `foliage`).
71
+ - Use `colour.palette` for suggested colours and `colour.own` only for brushes that must
72
+ always paint their own (a rainbow marker).
73
+
74
+ ## Pitfalls
75
+
76
+ - **A facet the host refuses.** `create_brush` and `update_brush` refuse it with the facet and
77
+ fix; `check_brush` returns `degraded`, the brush with those facets stripped, to start from.
78
+ - **Ids.** Built-in ids can't be reused or changed; a taken id is refused (update it instead).
79
+ - **Stamps.** Particle marks lay only the editor's stamps (`editor:star`…); sticker (`objects`)
80
+ marks take any sticker ref from `find_stickers`.
81
+ - **Out-of-range values** are refused by the schema (`tip.size` is 1..2500 px); a stored brush
82
+ is clamped on import and reported in `problems`.
83
+ - **The paint editor paints from this library.** Its `set_brush` picks a brush by id here and
84
+ lays facets over it without saving; `create_brush` / `update_brush` store one. A paint host
85
+ registers these tools with its own preview (`createPaintBrushLibraryToolProvider`), so the
86
+ paint editor has no list, save, export, import or thumbnail tools of its own.
87
+
88
+ ## Workflow skills
89
+
90
+ - `pixerate-brushes-design`: from a description ("a soft pink highlighter", "chalk for the
91
+ canvas") to a saved, checked brush.
92
+
93
+ ## Tool reference
94
+
95
+ | Tool | Use |
96
+ | --- | --- |
97
+ | `list_brush_presets` | Find brushes this host renders: `kind`, `medium`, `variety`, `tool`, `tag`, `text`, `source` (all, builtin, user), `all`, `offset`, `limit`. Returns `brushes` (id, name, builtin, kind, medium, variety or tool, category, engine, tip, size, marks, surface, light) and `total`. Call first. |
98
+ | `describe_brush` | One brush by `id`: its full facet `brush`, `builtin`, `engine`, `category` and `check` for this host. Unknown ids are refused with the closest ones. |
99
+ | `create_brush` | Save a new brush: `id`, `name`, optional `basedOn`, and the facets (to keep an editor's current brush, pass the `brush` its `get_brush` returns). Normalised and checked for this host; refused brushes save nothing. Returns `id`, `brush`, `engine`, `problems`, `warnings`. |
100
+ | `update_brush` | Change a saved brush: facets to merge in, `unset` facets to drop. Refuses built-ins (duplicate first) and changes the host can't render. Returns `brush`, `changed` paths, `problems`, `warnings`. |
101
+ | `duplicate_brush` | Copy any brush, built-ins included, to a saved brush (`newId` default `<id>-copy`, `name`). Returns the new `id` and `brush`; then `update_brush` it. |
102
+ | `remove_brush` | Delete a saved brush for good (export it first to keep a copy). Built-ins are refused. Returns `removed`. |
103
+ | `check_brush` | Check a brush (`id`, or an unsaved `brush` spec) for a `host` (paint, image, canvas, vector). Returns `ok`, `issues` (facet, severity, reason, fix) and, when refused, `degraded`. |
104
+ | `export_brushes` | Library file (pixerate-brushes v2) of the saved brushes or the `ids` given. Returns `file`, for `import_brushes`. |
105
+ | `import_brushes` | Add a library `file` (v2, v1 migrated, or a bare list): built-in ids and taken ids are skipped unless `overwrite`. Returns `added`, `replaced`, `skipped`, `problems`. |
106
+ | `preview_brush` | Only when the host renders previews: a sample stroke with a brush (`id`, `width`, `height`, `color`) as an image. |
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: pixerate-brushes-design
3
+ description: The brush design workflow in the Pixerate brush library - turning a description ("a soft pink highlighter", "chalk for canvas strokes", "make this brush work in the vector editor") into a saved brush, by picking the nearest preset, changing only the facets that matter, checking it against the host that will use it and previewing it. Use when asked to make, design, customise, adapt or port a brush, or when create_brush, check_brush or list_brush_presets are available. Load with pixerate-brushes, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-brushes
7
+ ---
8
+
9
+ # Designing a brush
10
+
11
+ A brush is a preset with a few facets changed. The presets already hold good settings for each
12
+ medium (pencil grain, ink velocity, watercolour water), so the fastest good brush starts from
13
+ the nearest one and changes two or three facets: the colour, the tip, how it feels. What a brush
14
+ may use depends on the editor that will paint with it: check it against that host before saving.
15
+
16
+ This skill is the method. `pixerate-brushes` has the full tool reference.
17
+
18
+ ## Goal and quality bar
19
+
20
+ - The brush is saved under a clear `id` and `name`, with a truthful `medium` and `variety`.
21
+ - `check_brush` for the host it is for returns `ok: true`; any warnings are understood.
22
+ - It differs from its base preset only where the request asks (`describe_brush` shows it).
23
+ - Built-in presets are untouched.
24
+
25
+ ## Inputs to confirm
26
+
27
+ Ask when you cannot tell from the request; say what you assumed when you can.
28
+
29
+ - **Host:** which editor will use it (paint by default; canvas and vector strokes take only dry
30
+ dab brushes, the image editor no wet, thick or particle brushes).
31
+ - **Medium and character:** what it imitates (pencil, ink, chalk, watercolour, marker…), and
32
+ the size, softness and texture.
33
+ - **Colour:** a default colour or palette, or the user's own colour (the default).
34
+
35
+ ## Working method
36
+
37
+ 1. **Find the base.** `list_brush_presets` with `medium` or `variety`, or `text`. Pick the
38
+ closest; `describe_brush` it to read its facets.
39
+ **Check:** the base's `check` is `ok` for the target host (or `check_brush` with `host`).
40
+ 2. **Create from it.** `create_brush` with a new `id`, `name`, `basedOn` and only the facets
41
+ that change: `variety` for what it is now, `colour.default` / `colour.palette`, `tip`
42
+ (size, hardness, roundness, angle), `dynamics`, `behaviour.grain`.
43
+ **Check:** `problems` is empty or understood; nothing refused.
44
+ 3. **Check it for the host.** `check_brush` with `id` and `host`. A refusal names the facet and
45
+ fix; its `degraded` brush is a good next spec.
46
+ **Check:** `ok: true`.
47
+ 4. **Look at it.** `preview_brush` when available; then `update_brush` to tune (each call
48
+ returns the paths `changed`).
49
+ **Check:** `describe_brush` shows the intended facets.
50
+
51
+ ## Decision table
52
+
53
+ | Situation | Approach |
54
+ | --- | --- |
55
+ | A colour variant of a preset | `basedOn` it, set `colour.default` (and a `palette`), keep everything else. |
56
+ | For canvas or vector strokes | Base on a dry preset (pencil, pastel, charcoal, marker, ink); no wet surface, particles, stickers or tools; no light on canvas. |
57
+ | A brush that must work in another editor | `check_brush` with that `host`, then `create_brush` from its `degraded` brush under a new id. |
58
+ | A texture (chalk, charcoal, dry brush) | Raise `behaviour.grain` (0.8..1) and add `dynamics.sizeJitter`; test on rough paper. |
59
+ | A light brush (neon, glow) | `light.amount`, and paint it on dark paper; a highlighter keeps `light.spill.radius` 0. |
60
+ | A change to a built-in | `duplicate_brush`, then `update_brush` the copy. |
61
+
62
+ ## Recipes
63
+
64
+ **A pink highlighter.** `create_brush` `{ id: "pink-highlighter", name: "Pink highlighter",
65
+ basedOn: "highlighter", colour: { default: "#ff6ec7" } }`, then `check_brush`.
66
+
67
+ **Chalk for canvas strokes.** On the canvas host, `create_brush` `{ id: "chalk", name: "Chalk",
68
+ basedOn: "soft-pastel", variety: "chalk", behaviour: { grain: 1 }, dynamics: { sizeJitter: 0.12 } }`.
69
+
70
+ **Port a wet brush to vector.** `check_brush` `{ id, host: "vector" }`; `create_brush` from the
71
+ `degraded` brush with `id` `<id>-vector`; `check_brush` again.
72
+
73
+ ## Pitfalls
74
+
75
+ - **Starting from nothing.** A brush without `basedOn` gets engine defaults (a hard round dab);
76
+ it rarely looks like the medium asked for.
77
+ - **A host that refuses the base.** A wet or particle preset can't be the base of a canvas
78
+ brush; set `behaviour.surface` to `dry` or start from a dry preset.
79
+ - **Lists replace.** Giving `colour.palette` or `tip.stamps` replaces the whole list; include
80
+ every entry you want.
81
+ - **Changing a built-in.** `update_brush` refuses built-ins; duplicate first.
82
+
83
+ ## Tools used
84
+
85
+ - `list_brush_presets`, `describe_brush`: find the base preset and read its facets.
86
+ - `create_brush`, `update_brush`, `duplicate_brush`: make and tune the brush.
87
+ - `check_brush`: prove it works for the host, and get a degraded spec when it doesn't.
88
+ - `preview_brush`: look at a sample stroke when the host renders previews.
@@ -0,0 +1,15 @@
1
+ {
2
+ "prompt": "I'm drawing brushed strokes on the canvas. Make a soft, grainy chalk brush with the id chalk that the canvas can actually use.",
3
+ "fixture": { "editor": "brushes", "host": "canvas" },
4
+ "checks": [
5
+ { "why": "the canvas can render it", "tool": "check_brush", "args": { "id": "chalk", "host": "canvas" }, "expect": { "ok": true } },
6
+ { "why": "a dry chalk", "tool": "describe_brush", "args": { "id": "chalk" }, "expect": { "brush": { "kind": "mark", "medium": "dry", "variety": "chalk" } } },
7
+ { "why": "it catches the paper grain strongly", "tool": "describe_brush", "args": { "id": "chalk" }, "expect": { "brush": { "behaviour": { "grain": { "$min": 0.8 } } } } },
8
+ { "why": "soft-edged", "tool": "describe_brush", "args": { "id": "chalk" }, "expect": { "brush": { "tip": { "hardness": { "$max": 0.6 } } } } }
9
+ ],
10
+ "solution": [
11
+ { "tool": "list_brush_presets", "args": { "medium": "dry" } },
12
+ { "tool": "create_brush", "args": { "id": "chalk", "name": "Chalk", "basedOn": "soft-pastel", "variety": "chalk", "behaviour": { "grain": 1 }, "dynamics": { "sizeJitter": 0.12 } } },
13
+ { "tool": "check_brush", "args": { "id": "chalk", "host": "canvas" } }
14
+ ]
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "prompt": "Make me a pink highlighter brush, saved as pink-highlighter, that behaves like the built-in highlighter but starts out pink.",
3
+ "fixture": { "editor": "brushes" },
4
+ "checks": [
5
+ { "why": "a saved brush, not a built-in", "tool": "describe_brush", "args": { "id": "pink-highlighter" }, "expect": { "builtin": false, "check": { "ok": true } } },
6
+ { "why": "still a highlighter: multiply chisel ink with no spill", "tool": "describe_brush", "args": { "id": "pink-highlighter" }, "expect": { "brush": { "medium": "ink", "variety": "highlighter", "tip": { "shape": "chisel", "blend": "multiply" }, "light": { "spill": { "radius": 0 } } } } },
7
+ { "why": "it starts out pink", "tool": "describe_brush", "args": { "id": "pink-highlighter" }, "expect": { "brush": { "colour": { "default": { "$expr": "(([r, g, b]) => r > 200 && b > 120 && g < r - 60)([1, 3, 5].map((i) => parseInt(v.slice(i, i + 2), 16)))" } } } } },
8
+ { "why": "the built-in highlighter is untouched", "tool": "describe_brush", "args": { "id": "highlighter" }, "expect": { "brush": { "colour": { "default": "#fff200" } } } }
9
+ ],
10
+ "solution": [
11
+ { "tool": "list_brush_presets", "args": { "variety": "highlighter" } },
12
+ { "tool": "create_brush", "args": { "id": "pink-highlighter", "name": "Pink highlighter", "basedOn": "highlighter", "colour": { "default": "#ff6ec7" } } },
13
+ { "tool": "check_brush", "args": { "id": "pink-highlighter" } }
14
+ ]
15
+ }