@pixerate/editor 0.259.0 → 0.261.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 (80) 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 +61 -59
  4. package/dist/animation-editor/index.js +5 -3
  5. package/dist/brushes/index.cjs +52 -52
  6. package/dist/brushes/index.js +6 -6
  7. package/dist/{brushstrokeRender-JI4W6F2G.js → brushstrokeRender-25Q5BEY6.js} +17 -7
  8. package/dist/{brushstrokeRender-MQ6PSDM5.cjs → brushstrokeRender-N6UPZSQS.cjs} +31 -21
  9. package/dist/canvas/brushTypes.d.cts +5 -0
  10. package/dist/canvas/brushTypes.d.ts +5 -0
  11. package/dist/canvas/drawController.d.cts +15 -1
  12. package/dist/canvas/drawController.d.ts +15 -1
  13. package/dist/canvas/index.cjs +136 -102
  14. package/dist/canvas/index.js +35 -1
  15. package/dist/{chunk-3PPJRMIG.js → chunk-3O47TPGX.js} +197 -18
  16. package/dist/{chunk-5Q5MLXAD.cjs → chunk-3TQW5MPW.cjs} +23 -23
  17. package/dist/{chunk-JBBJE76O.cjs → chunk-3ZRNK3RO.cjs} +1138 -85
  18. package/dist/{chunk-YSGW7VV4.cjs → chunk-43NT242D.cjs} +1401 -228
  19. package/dist/{chunk-SMTUQWGY.js → chunk-5IW65VQ6.js} +1 -1
  20. package/dist/{chunk-YQ4PQTBB.js → chunk-6NZL24W4.js} +9 -1
  21. package/dist/{chunk-O3ZVZSNJ.cjs → chunk-7AUQ2UJ3.cjs} +137 -129
  22. package/dist/{chunk-4M4E3N6X.js → chunk-CQVTGBQI.js} +1 -1
  23. package/dist/{chunk-SSFMWJDK.js → chunk-D4TE5JNW.js} +1192 -19
  24. package/dist/{chunk-AORZ5M5C.js → chunk-EHALWBBD.js} +1 -1
  25. package/dist/{chunk-6WCQ35GI.cjs → chunk-NHA26O5X.cjs} +2 -2
  26. package/dist/{chunk-DPYAO5U6.cjs → chunk-PVJMTLPJ.cjs} +197 -18
  27. package/dist/{chunk-MQGQTKVW.cjs → chunk-PYTJBVW5.cjs} +29 -29
  28. package/dist/{chunk-IXQZHCOF.js → chunk-QAR2N7HZ.js} +1 -1
  29. package/dist/{chunk-5P4JEOIV.js → chunk-TMDDMJCR.js} +2 -2
  30. package/dist/{chunk-RSIKDWOP.cjs → chunk-TZOX7KWV.cjs} +15 -15
  31. package/dist/{chunk-IPUTIUGQ.cjs → chunk-UVZ7RQOE.cjs} +1 -1
  32. package/dist/{chunk-A7GJ6BXI.js → chunk-WJ4WQJCC.js} +1062 -9
  33. package/dist/{chunk-ICTPLVSR.js → chunk-X2EXYY4J.js} +1 -1
  34. package/dist/{chunk-WQCNC4DK.js → chunk-XC6DPRYI.js} +695 -4
  35. package/dist/{chunk-KBYB7OWB.cjs → chunk-YHDHFJUC.cjs} +776 -85
  36. package/dist/{chunk-P3PZ63M3.cjs → chunk-YLUIFXSL.cjs} +35 -35
  37. package/dist/commands/index.cjs +2 -2
  38. package/dist/commands/index.js +1 -1
  39. package/dist/image-editor/index.cjs +1385 -2484
  40. package/dist/image-editor/index.js +139 -1238
  41. package/dist/image-editor/selection-worker.cjs +3 -4
  42. package/dist/image-editor/selection-worker.js +1 -2
  43. package/dist/paint/index.cjs +3 -5
  44. package/dist/paint/index.js +40 -42
  45. package/dist/paint-editor/contract.d.cts +29 -0
  46. package/dist/paint-editor/contract.d.ts +29 -0
  47. package/dist/paint-editor/controller.d.cts +17 -0
  48. package/dist/paint-editor/controller.d.ts +17 -0
  49. package/dist/paint-editor/dab-stroke.d.cts +5 -0
  50. package/dist/paint-editor/dab-stroke.d.ts +5 -0
  51. package/dist/paint-editor/index.cjs +610 -327
  52. package/dist/paint-editor/index.d.cts +2 -0
  53. package/dist/paint-editor/index.d.ts +2 -0
  54. package/dist/paint-editor/index.js +337 -54
  55. package/dist/paint-editor/path-samples.d.cts +19 -0
  56. package/dist/paint-editor/path-samples.d.ts +19 -0
  57. package/dist/paint-editor/stamp-tip.d.cts +17 -0
  58. package/dist/paint-editor/stamp-tip.d.ts +17 -0
  59. package/dist/paint-editor/ui.d.cts +27 -1
  60. package/dist/paint-editor/ui.d.ts +27 -1
  61. package/dist/raster/index.cjs +2 -4
  62. package/dist/raster/index.js +6 -8
  63. package/dist/vector/brushstroke.d.cts +6 -0
  64. package/dist/vector/brushstroke.d.ts +6 -0
  65. package/dist/vector/controller.d.cts +11 -2
  66. package/dist/vector/controller.d.ts +11 -2
  67. package/dist/vector/index.cjs +990 -973
  68. package/dist/vector/index.js +28 -11
  69. package/package.json +1 -1
  70. package/skills/pixerate-brushes/SKILL.md +1 -1
  71. package/skills/pixerate-canvas/SKILL.md +3 -1
  72. package/skills/pixerate-paint-editor/SKILL.md +2 -0
  73. package/skills/pixerate-paint-sketch-to-finish/SKILL.md +5 -1
  74. package/skills/pixerate-vector/SKILL.md +2 -2
  75. package/dist/chunk-2VJAVXLI.js +0 -192
  76. package/dist/chunk-62Z5WLRA.js +0 -700
  77. package/dist/chunk-BOHDJJ2O.cjs +0 -192
  78. package/dist/chunk-NKGB64CW.cjs +0 -700
  79. package/dist/chunk-SS53NNZO.cjs +0 -1066
  80. package/dist/chunk-ZNA3IQZF.js +0 -1066
@@ -26,7 +26,7 @@ import {
26
26
  traceField,
27
27
  traceVectorImage,
28
28
  vectorTraceContours
29
- } from "../chunk-4M4E3N6X.js";
29
+ } from "../chunk-CQVTGBQI.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-IXQZHCOF.js";
56
+ } from "../chunk-QAR2N7HZ.js";
57
57
  import {
58
58
  DEFAULT_VECTOR_FONT,
59
59
  IDENTITY_MATRIX,
@@ -380,6 +380,7 @@ import {
380
380
  vectorBrushstrokeBrushOf,
381
381
  vectorBrushstrokeBrushes,
382
382
  vectorBrushstrokeCharacterError,
383
+ vectorBrushstrokeDefaultColour,
383
384
  vectorBrushstrokeImages,
384
385
  vectorBrushstrokeReach,
385
386
  vectorChildAppearanceShapes,
@@ -528,10 +529,10 @@ import {
528
529
  withVectorStrokes,
529
530
  withVectorTargetPaint,
530
531
  zigzagPaths
531
- } from "../chunk-YQ4PQTBB.js";
532
+ } from "../chunk-6NZL24W4.js";
532
533
  import {
533
534
  snapshotBrushLibrary
534
- } from "../chunk-SMTUQWGY.js";
535
+ } from "../chunk-5IW65VQ6.js";
535
536
  import "../chunk-APX63ILU.js";
536
537
  import {
537
538
  createProvenance
@@ -558,6 +559,8 @@ import {
558
559
  fitCubics,
559
560
  parseSvgPath
560
561
  } from "../chunk-PWBZH3FK.js";
562
+ import "../chunk-FJBQTZS4.js";
563
+ import "../chunk-3CI7FRMK.js";
561
564
  import {
562
565
  StickerError
563
566
  } from "../chunk-SZTI6EER.js";
@@ -6823,7 +6826,9 @@ var VectorEditorController = class _VectorEditorController {
6823
6826
  committing = false;
6824
6827
  revision = 0;
6825
6828
  state;
6829
+ colorScheme;
6826
6830
  constructor(options = {}) {
6831
+ this.colorScheme = options.colorScheme ?? "light";
6827
6832
  this.brushes = options.brushLibrary ? snapshotBrushLibrary(options.brushLibrary, () => this.notify()) : null;
6828
6833
  this.history = options.history ?? new HistoryManager();
6829
6834
  this.doc = options.document ? loadedDocument(options.document) : { width: 800, height: 600, nodes: [] };
@@ -8182,7 +8187,8 @@ var VectorEditorController = class _VectorEditorController {
8182
8187
  * Adds a brushstroke to the selected (or given) paths: a stroke layer that paints the path with
8183
8188
  * brush `brushId` (a preset, or a saved brush of the brush library: `listBrushstrokeBrushes`), `size` document
8184
8189
  * units across (default the brush's own), an optional `pressure` profile along the length, in
8185
- * `color` (default the top stroke's paint, else black) at `opacity`. At the top of the stroke
8190
+ * `color` (default the brush's own default colour, unless `keepColour`; else the top stroke's
8191
+ * paint, else black) at `opacity`. At the top of the stroke
8186
8192
  * stack or at `index`. A `seed` gives it the brush's character (jitter and the grain of `paper`,
8187
8193
  * `null` for none); without one it renders plain. One undo step.
8188
8194
  */
@@ -8196,9 +8202,10 @@ var VectorEditorController = class _VectorEditorController {
8196
8202
  if (characterError) return { ok: false, message: characterError };
8197
8203
  if (options.color !== void 0 && !parseVectorColor(options.color)) return { ok: false, message: `"${options.color}" is not a colour.` };
8198
8204
  if (options.opacity !== void 0 && !(options.opacity >= 0 && options.opacity <= 1)) return { ok: false, message: "opacity must be from 0 to 1." };
8205
+ const own = options.keepColour ? null : vectorBrushstrokeDefaultColour(clean.brushId, user, this.colorScheme);
8199
8206
  return this.editStacks("stroke", options.ids, "Add Brushstroke", (stack, path) => {
8200
8207
  const top = [...stack].reverse().find((l) => (l.kind ?? "paint") === "paint");
8201
- const paint = options.color ?? (typeof top?.paint === "string" && top.paint !== "none" ? top.paint : typeof path.stroke === "string" && path.stroke !== "none" ? path.stroke : "#000000");
8208
+ const paint = options.color ?? own ?? (typeof top?.paint === "string" && top.paint !== "none" ? top.paint : typeof path.stroke === "string" && path.stroke !== "none" ? path.stroke : "#000000");
8202
8209
  const layer = { kind: "brushstroke", paint, brush: clean, ...options.opacity !== void 0 && options.opacity < 1 ? { opacity: options.opacity } : {} };
8203
8210
  const at = options.index === void 0 ? stack.length : Math.min(stack.length, Math.max(0, Math.round(options.index)));
8204
8211
  return [...stack.slice(0, at), layer, ...stack.slice(at)];
@@ -8208,7 +8215,8 @@ var VectorEditorController = class _VectorEditorController {
8208
8215
  * Changes brushstroke layer `index` (in the stroke stack, 0 is the bottom) of the selected (or
8209
8216
  * given) paths: another brush, size, pressure profile (null removes it), colour or opacity, the
8210
8217
  * character `seed` (null removes it: plain dabs) or the `paper` (replaced whole: `null` for no
8211
- * grain, `{}` for the default paper). Fails, changing nothing, when that layer is not a
8218
+ * grain, `{}` for the default paper). Another brush without a `color` takes that brush's default
8219
+ * colour, if it has one, unless `keepColour`. Fails, changing nothing, when that layer is not a
8212
8220
  * brushstroke. With `merge`, consecutive edits fold into one undo step.
8213
8221
  */
8214
8222
  setBrushstroke(index, patch, options = {}) {
@@ -8236,12 +8244,18 @@ var VectorEditorController = class _VectorEditorController {
8236
8244
  if (patch.seed === null) delete brush.seed;
8237
8245
  else if (patch.seed !== void 0) brush.seed = patch.seed;
8238
8246
  if (patch.paper !== void 0) brush.paper = patch.paper;
8239
- const next = { ...layer, brush: sanitizeVectorBrushstroke(brush, user) ?? layer.brush, ...patch.color ? { paint: patch.color } : {}, ...patch.opacity !== void 0 ? { opacity: patch.opacity } : {} };
8247
+ const own = patch.brushId && patch.brushId !== layer.brush.brushId && !patch.keepColour ? vectorBrushstrokeDefaultColour(patch.brushId, user, this.colorScheme) : null;
8248
+ const colour = patch.color ?? own;
8249
+ const next = { ...layer, brush: sanitizeVectorBrushstroke(brush, user) ?? layer.brush, ...colour ? { paint: colour } : {}, ...patch.opacity !== void 0 ? { opacity: patch.opacity } : {} };
8240
8250
  return stack.map((l, i) => i === index ? next : l);
8241
8251
  },
8242
8252
  merge
8243
8253
  );
8244
8254
  }
8255
+ /** Sets the colour scheme that picks the side of a brush's light / dark default colour. Existing strokes keep their colour. */
8256
+ setColorScheme(scheme) {
8257
+ this.colorScheme = scheme;
8258
+ }
8245
8259
  /** Removes layer `index` of the fill or stroke stack of the selected (or given) paths. */
8246
8260
  removePaintLayer(kind, index, options = {}) {
8247
8261
  return this.editStacks(
@@ -15932,7 +15946,8 @@ function createVectorCommands(controller, options = {}) {
15932
15946
  brushId: { type: "string", description: 'Brush id from vector.listBrushes, e.g. "pencil-hb".' },
15933
15947
  size: { type: "number", exclusiveMinimum: 0, maximum: 500, description: "Brush diameter in document units (default: the brush's own)." },
15934
15948
  pressure: { type: "array", items: { type: "number", minimum: 0, maximum: 1 }, minItems: 1, maxItems: 64, description: "Pressure 0\u20131 along the length, spread evenly from start to end (default: full pressure)." },
15935
- color: { type: "string", description: "CSS colour (default: the path's stroke colour, else black)." },
15949
+ color: { type: "string", description: "CSS colour (default: the brush's own default colour, such as the ballpoint's blue, unless keepColour; else the path's stroke colour, else black)." },
15950
+ keepColour: { type: "boolean", description: "Ignore the brush's default colour and use the path's stroke colour (default false)." },
15936
15951
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131 (default 1)." },
15937
15952
  index: { type: "integer", minimum: 0, description: "Position in the stroke stack, 0 the bottom (default: on top)." },
15938
15953
  seed: { ...BRUSH_SEED, description: "Character seed 0\u20132147483647: turns on the brush's jitter and paper grain; the same seed always renders the same stroke (default: none, plain dabs)." },
@@ -15954,7 +15969,7 @@ function createVectorCommands(controller, options = {}) {
15954
15969
  ...params.seed !== void 0 ? { seed: params.seed } : {},
15955
15970
  ...params.paper !== void 0 ? { paper: params.paper } : {}
15956
15971
  },
15957
- { ...params.ids ? { ids: params.ids } : {}, ...params.index !== void 0 ? { index: params.index } : {}, ...params.color ? { color: params.color } : {}, ...params.opacity !== void 0 ? { opacity: params.opacity } : {} }
15972
+ { ...params.ids ? { ids: params.ids } : {}, ...params.index !== void 0 ? { index: params.index } : {}, ...params.color ? { color: params.color } : {}, ...params.keepColour ? { keepColour: true } : {}, ...params.opacity !== void 0 ? { opacity: params.opacity } : {} }
15958
15973
  )
15959
15974
  );
15960
15975
  const paths = [];
@@ -15979,7 +15994,8 @@ function createVectorCommands(controller, options = {}) {
15979
15994
  brushId: { type: "string", description: "Another brush from vector.listBrushes." },
15980
15995
  size: { type: ["number", "null"], description: "Diameter in document units, or null for the brush's own." },
15981
15996
  pressure: { type: ["array", "null"], items: { type: "number", minimum: 0, maximum: 1 }, description: "Pressure profile 0\u20131 from start to end, or null for full pressure." },
15982
- color: { type: "string", description: "CSS colour." },
15997
+ color: { type: "string", description: "CSS colour (with another brushId and no color, the new brush's default colour, if it has one, unless keepColour)." },
15998
+ keepColour: { type: "boolean", description: "With another brushId: keep the layer's colour instead of taking the new brush's default colour (default false)." },
15983
15999
  opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131." },
15984
16000
  seed: { anyOf: [BRUSH_SEED, { type: "null" }], description: "Character seed 0\u20132147483647 (jitter and paper grain; a new seed gives a new take on the same stroke), or null for plain dabs." },
15985
16001
  paper: { anyOf: [BRUSH_PAPER, { type: "null" }], description: "The paper the grain comes from { pattern, seed, scale, depth } (applies with a seed), null for no grain, or {} for the default sanded paper." }
@@ -18873,6 +18889,7 @@ export {
18873
18889
  vectorBrushstrokeBrushOf,
18874
18890
  vectorBrushstrokeBrushes,
18875
18891
  vectorBrushstrokeCharacterError,
18892
+ vectorBrushstrokeDefaultColour,
18876
18893
  vectorBrushstrokeImages,
18877
18894
  vectorBrushstrokeReach,
18878
18895
  vectorBuilderErasedEdges,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.259.0",
3
+ "version": "0.261.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@ built-in presets (read-only) and the user's brushes, shared by every editor that
21
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
22
  | Light | Light the brush gives off (`amount`), plus suggested layer `spill` and `glow`. |
23
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). Each is a hex or `rgba()` colour, a light / dark pair `{ light, dark }`, a gradient or effect paint. A gradient default is always painted; a solid or light / dark default becomes the editor's colour when the brush is chosen (unless the user locked theirs), resolved for the colour scheme. |
24
+ | Colour | A `default` colour, a `palette` of up to 16, `own` (always its own colour). Each is a hex or `rgba()` colour, a light / dark pair `{ light, dark }`, a gradient or effect paint. A gradient default is always painted; a solid or light / dark default becomes the editor's colour when the brush is chosen (unless the user locked theirs), resolved for the colour scheme. Canvas strokes and vector brushstrokes take it too, when they are given no colour. |
25
25
 
26
26
  How media map to facets: a **pencil** or **charcoal** is `dry` with high `grain` and tilt;
27
27
  **ink** is a hard round tip with pressure to size (a pen thins with velocity); **watercolour**
@@ -99,6 +99,8 @@ you to. Withdraw stale ones with `reject_proposal`: when the user changes what a
99
99
  one width), the airbrush is a soft spray. The grain and jitter are seeded per stroke, so a
100
100
  stroke always looks the same, and neighbouring strokes share one sheet of paper. Pick the brush
101
101
  for the look, then the colour; for a sketch, use one or two brushes rather than all of them.
102
+ A brush with a default colour (ballpoint blue, highlighter yellow, chalk) paints in it when
103
+ you pass no `color`; pass `color` to use another.
102
104
  - **Annotate with gestures, not points.** To mark up work the way a designer marks up a print,
103
105
  use the gesture commands (`canvas.circle`, `canvas.underline`, `canvas.arrow`,
104
106
  `canvas.bracket`, `canvas.strike`, `canvas.cross`, `canvas.check`) with node ids
@@ -251,7 +253,7 @@ character: seeded jitter and the paper grain they catch) and, when a width was c
251
253
 
252
254
  | Command | Use it to |
253
255
  | --- | --- |
254
- | `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). |
256
+ | `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` and no `color`, the brush's default colour if it has one; 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). |
255
257
  | `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. |
256
258
 
257
259
  Gestures draw one hand-drawn stroke node each (one undo step) through the same path as
@@ -109,6 +109,7 @@ the pitfalls, and uses the tools below.
109
109
  - **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.
110
110
  - **Tilt** (`altitude` near 0.3..0.6 radians with an `azimuth`) turns a pencil into broad side shading; upright points draw a fine line.
111
111
  - **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.
112
+ - **Brush geometry with `stroke_path`**: for clean curves, circles, outlines and lettering, give SVG path data and a `pressure` taper instead of hundreds of points; the brush keeps its grain and character. Send the shape as one path with several subpaths to keep it one undo step.
112
113
  - **Use `rotation: "stroke"`** for flat tips (pastel, charcoal, chisel marker) so the mark turns with the path; `fixed` for calligraphic nibs.
113
114
  - **Blending tools**:
114
115
  - `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.
@@ -320,6 +321,7 @@ Listing, saving, sharing and previewing brushes are the brush library's tools (`
320
321
  | Tool | Use |
321
322
  | --- | --- |
322
323
  | `paint_stroke` | Paint one stroke through `points` (document px; `pressure` 0..1, `t` ms, `tiltX` / `tiltY` or `altitude` / `azimuth`, `twist`) with `brushId` (default the active brush), optional per-stroke `brush` facet changes (as `set_brush` takes), `color` or `paint` (a gradient for this stroke), `layerId`, `seed`, `snapToPerspective`. The symmetry guide adds its copies; a sampling tracing image supplies the colour when `color` is missing. Returns `layerId`, `bounds`, `dabs`, `strokeId` and, for brushes that pick up paint, `mixerLoad` (what the brush carries now). Refused on locked, hidden or group layers, with the fix in the message. |
324
+ | `stroke_path` | Brush SVG path data (`path`, document px; M, L, C, Q, A, Z…) with the active brush or `brushId` / `brush`, as if drawn along it by hand: each subpath one recorded stroke, sampled every `spacing` px (default 2), with a `pressure` profile spread along each subpath (`[0.2, 1, 0.2]` tapers both ends). One undo step. Returns `layerId`, `bounds`, `dabs` and `strokeIds` (for `replay_strokes`, `update_stroke`). Use it for clean curves, outlines of shapes and lettering guides instead of working out points; use `paint_stroke` when the marks need time or tilt. |
323
325
  | `list_strokes` | Read the recorded strokes, oldest first (default the last 100): id, layer, brush, colour, point count, seed, bounds, `live`. With `ids`, their points too; with `layerId`, one layer's. With `live: true`, the editable strokes of stroke layers as they are now, bottom first: the ids for the stroke layer tools. |
324
326
  | `get_live_stroke` | Read the stroke the user is painting right now (layer, brush, colour, samples so far; `live: false` when none). For painting along with the user; hosts stream the samples live. |
325
327
  | `stroke_at` | Read the top-most stroke of a stroke layer that painted the pixel at `x`, `y`, or null. Use it to pick one stroke among overlapping ones. |
@@ -39,7 +39,9 @@ This skill is the method. `pixerate-paint-editor` has the full tool reference.
39
39
  reference.
40
40
  **Check:** `list_strokes` shows pencil strokes on the Sketch layer.
41
41
  3. **Ink.** `add_layer` "Ink" above it; `ink-pen` or `brush-pen`, dark colour, long strokes with
42
- points that carry `pressure` (lighter at the ends) and `t` for speed dynamics.
42
+ points that carry `pressure` (lighter at the ends) and `t` for speed dynamics. For clean
43
+ geometric lines (curves, circles, panel borders) use `stroke_path` with SVG path data and a
44
+ `pressure` taper such as `[0.3, 1, 0.3]`.
43
45
  **Check:** `pick_color` on a line is dark.
44
46
  4. **Hide the sketch.** `update_layer` the Sketch layer `visible: false` (or lower its `opacity`
45
47
  while inking, then hide it).
@@ -61,6 +63,7 @@ This skill is the method. `pixerate-paint-editor` has the full tool reference.
61
63
  | --- | --- |
62
64
  | Sketch | `pencil-hb`, light colour, own layer, hidden at the end |
63
65
  | Line art | `ink-pen` (even), `brush-pen` (swelling); own layer on top |
66
+ | Clean geometric lines (curves, circles, borders) | `stroke_path` with SVG path data and a `pressure` taper, on the Ink layer |
64
67
  | Flats | `marker` or a flat brush on a layer under the ink |
65
68
  | Patterned or textured flats (sky, gradient, galaxy) | `set_effect_paint` with a provider generator, then a flat dry brush on the Flats layer; `set_color` after |
66
69
  | Shadows | Clipped layer, `multiply` or `shadow`, a cool colour |
@@ -98,6 +101,7 @@ clipped: true }` → `set_brush` `airbrush` → paint the shadow side.
98
101
  - `set_paper`, `fill_layer`: the ground and backgrounds
99
102
  - `add_layer`, `update_layer`, `move_layer`, `remove_layer`: one layer per pass; hide, clip, blend
100
103
  - `set_brush`, `set_color`, `paint_stroke`, `replay_strokes`: sketch, ink, flats and shading
104
+ - `stroke_path`: clean geometric line art from SVG path data
101
105
  - `search_effects`, `describe_effect`, `set_effect_paint`: flats painted with a provider effect
102
106
  - `set_symmetry`, `set_perspective_guide`, `set_tracing_image`: construction guides
103
107
  - `undo_paint`: take back a step
@@ -374,8 +374,8 @@ contents whole while you work, without losing the mask.
374
374
  | Command | Use it to |
375
375
  | --- | --- |
376
376
  | `vector.listBrushes` | List the brushes a brushstroke can use: the dry presets (pencils, pastels, charcoal, crayon, chalk, markers, inks, ballpoint, gel pen, fineliner), 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
- | `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
- | `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`. |
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`, default the brush's default colour (ballpoint blue, chalk), else the path's stroke; `keepColour` keeps the path's colour): 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
+ | `vector.setBrushstroke` | Change brushstroke layer `index` of path `id`: brush (another brush without `color` takes its default colour, unless `keepColour`), 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
 
380
380
  Give brushstrokes a `seed` unless you want flat, even dabs: without one a charcoal or pastel line looks like a felt pen. Use different seeds on neighbouring strokes so they do not repeat each other, and `paper: null` for clean ink or marker lines that should not catch grain. The grain is laid in each path's own space, so it moves with the path.
381
381
 
@@ -1,192 +0,0 @@
1
- import {
2
- LAYER_EFFECT_KINDS,
3
- builtinPattern,
4
- builtinPatternIds,
5
- defaultEffectGradient,
6
- hasActiveEffects,
7
- layerEffectLabel
8
- } from "./chunk-3PPJRMIG.js";
9
- import {
10
- BLEND_MODES
11
- } from "./chunk-IRSTHLQQ.js";
12
-
13
- // src/raster/effects/ui.ts
14
- function layerEffectsPanel(layer) {
15
- const empty = { readOnly: true, enabled: true, scale: 1, rows: [] };
16
- if (!layer) return { ...empty, available: false, reason: "Select a layer to add a layer style." };
17
- if (layer.type === "adjustment") return { ...empty, available: false, reason: "Adjustment layers cannot have a layer style." };
18
- const fx = layer.effects;
19
- const readOnly = Boolean(layer.locks?.all);
20
- return {
21
- available: true,
22
- readOnly,
23
- reason: readOnly ? "The layer is locked; unlock it to change its layer style." : void 0,
24
- enabled: fx?.enabled ?? true,
25
- scale: fx?.scale ?? 1,
26
- rows: (fx?.items ?? []).map((e) => ({ id: e.id, kind: e.kind, label: layerEffectLabel(e.kind), enabled: e.enabled }))
27
- };
28
- }
29
- function layerEffectsBadge(layer) {
30
- if (!layer.effects?.items.length) return null;
31
- return hasActiveEffects(layer.effects) ? "on" : "off";
32
- }
33
- var LAYER_EFFECT_MENU = LAYER_EFFECT_KINDS.map((kind) => ({ kind, label: layerEffectLabel(kind) }));
34
- var BLENDS = BLEND_MODES.filter((m) => m.id !== "pigment").map((m) => ({ value: m.id, label: m.label }));
35
- var opt = (...pairs) => pairs.map(([value, label]) => ({ value, label }));
36
- function patternOptions(patterns) {
37
- return [...patterns.map((p) => ({ value: p.id, label: p.name || p.id })), ...builtinPatternIds().map((id) => ({ value: id, label: builtinPattern(id).name }))];
38
- }
39
- function getPath(o, path) {
40
- let v = o;
41
- for (const p of path.split(".")) {
42
- if (v === null || v === void 0) return void 0;
43
- if (Array.isArray(v)) v = p === "last" ? v[v.length - 1] : v[Number(p)];
44
- else v = v[p];
45
- }
46
- return v;
47
- }
48
- function setPath(o, parts, value) {
49
- if (!parts.length) return value;
50
- const [p, ...rest] = parts;
51
- if (Array.isArray(o)) {
52
- const i = p === "last" ? o.length - 1 : Number(p);
53
- const out = [...o];
54
- out[i] = setPath(o[i], rest, value);
55
- return out;
56
- }
57
- const r = o ?? {};
58
- return { ...r, [p]: setPath(r[p], rest, value) };
59
- }
60
- function fillControls(f, allowPattern, patterns) {
61
- const types = allowPattern ? opt(["color", "Color"], ["gradient", "Gradient"], ["pattern", "Pattern"]) : opt(["color", "Color"], ["gradient", "Gradient"]);
62
- const out = [{ key: "fill.type", label: "Fill", type: "select", value: f.type, options: types }];
63
- if (f.type === "color") out.push({ key: "fill.color", label: "Color", type: "color", value: f.color });
64
- else if (f.type === "gradient") out.push(...gradientControls("fill.gradient", f.gradient));
65
- else {
66
- out.push(
67
- { key: "fill.patternId", label: "Pattern", type: "select", value: f.patternId, options: patternOptions(patterns) },
68
- { key: "fill.scale", label: "Scale", type: "range", min: 0.01, max: 10, step: 0.01, value: f.scale, percent: true, unit: "%" }
69
- );
70
- }
71
- return out;
72
- }
73
- function gradientControls(prefix, g) {
74
- return [
75
- { key: `${prefix}.stops.0.color`, label: "Start colour", type: "color", value: g.stops[0]?.color ?? "#000000" },
76
- { key: `${prefix}.stops.last.color`, label: "End colour", type: "color", value: g.stops[g.stops.length - 1]?.color ?? "#ffffff" },
77
- { key: `${prefix}.style`, label: "Style", type: "select", value: g.style, options: opt(["linear", "Linear"], ["radial", "Radial"], ["angle", "Angle"], ["reflected", "Reflected"], ["diamond", "Diamond"]) },
78
- { key: `${prefix}.angle`, label: "Angle", type: "range", min: -180, max: 180, step: 1, value: g.angle, unit: "\xB0" },
79
- { key: `${prefix}.scale`, label: "Scale", type: "range", min: 0.1, max: 1.5, step: 0.01, value: g.scale, percent: true, unit: "%" },
80
- { key: `${prefix}.reverse`, label: "Reverse", type: "checkbox", value: g.reverse },
81
- { key: `${prefix}.align`, label: "Align with layer", type: "checkbox", value: g.align }
82
- ];
83
- }
84
- var px = (key, label, value, max = 250) => ({ key, label, type: "range", min: 0, max, step: 1, value, unit: "px" });
85
- var pct = (key, label, value, min = 0, max = 1) => ({ key, label, type: "range", min, max, step: 0.01, value, percent: true, unit: "%" });
86
- var deg = (key, label, value, min = -180, max = 180) => ({ key, label, type: "range", min, max, step: 1, value, unit: "\xB0" });
87
- function layerEffectControls(e, patterns = []) {
88
- const blended = e.kind === "bevel-emboss" ? [] : [
89
- { key: "blend", label: "Blend mode", type: "select", value: e.blend, options: BLENDS },
90
- pct("opacity", "Opacity", e.opacity)
91
- ];
92
- switch (e.kind) {
93
- case "drop-shadow":
94
- case "inner-shadow":
95
- return [
96
- ...blended,
97
- { key: "color", label: "Color", type: "color", value: e.color },
98
- { key: "useGlobalLight", label: "Use global light", type: "checkbox", value: e.useGlobalLight },
99
- deg("angle", "Angle", e.angle),
100
- px("distance", "Distance", e.distance, 500),
101
- e.kind === "drop-shadow" ? pct("spread", "Spread", e.spread) : pct("choke", "Choke", e.choke),
102
- px("size", "Size", e.size),
103
- pct("noise", "Noise", e.noise),
104
- ...e.kind === "drop-shadow" ? [{ key: "knocksOut", label: "Layer knocks out shadow", type: "checkbox", value: e.knocksOut }] : []
105
- ];
106
- case "outer-glow":
107
- case "inner-glow":
108
- return [
109
- ...blended,
110
- ...fillControls(e.fill, false, patterns),
111
- { key: "technique", label: "Technique", type: "select", value: e.technique, options: opt(["softer", "Softer"], ["precise", "Precise"]) },
112
- ...e.kind === "inner-glow" ? [{ key: "source", label: "Source", type: "select", value: e.source, options: opt(["edge", "Edge"], ["center", "Center"]) }] : [],
113
- e.kind === "outer-glow" ? pct("spread", "Spread", e.spread) : pct("choke", "Choke", e.choke),
114
- px("size", "Size", e.size),
115
- pct("range", "Range", e.range, 0.01, 1),
116
- pct("noise", "Noise", e.noise)
117
- ];
118
- case "bevel-emboss":
119
- return [
120
- { key: "style", label: "Style", type: "select", value: e.style, options: opt(["inner-bevel", "Inner Bevel"], ["outer-bevel", "Outer Bevel"], ["emboss", "Emboss"], ["pillow-emboss", "Pillow Emboss"], ["stroke-emboss", "Stroke Emboss"]) },
121
- { key: "technique", label: "Technique", type: "select", value: e.technique, options: opt(["smooth", "Smooth"], ["chisel-hard", "Chisel Hard"], ["chisel-soft", "Chisel Soft"]) },
122
- pct("depth", "Depth", e.depth, 0, 10),
123
- { key: "direction", label: "Direction", type: "select", value: e.direction, options: opt(["up", "Up"], ["down", "Down"]) },
124
- px("size", "Size", e.size),
125
- px("soften", "Soften", e.soften, 16),
126
- { key: "useGlobalLight", label: "Use global light", type: "checkbox", value: e.useGlobalLight },
127
- deg("angle", "Angle", e.angle),
128
- deg("altitude", "Altitude", e.altitude, 0, 90),
129
- { key: "highlightColor", label: "Highlight", type: "color", value: e.highlightColor },
130
- { key: "highlightBlend", label: "Highlight mode", type: "select", value: e.highlightBlend, options: BLENDS },
131
- pct("highlightOpacity", "Highlight opacity", e.highlightOpacity),
132
- { key: "shadowColor", label: "Shadow", type: "color", value: e.shadowColor },
133
- { key: "shadowBlend", label: "Shadow mode", type: "select", value: e.shadowBlend, options: BLENDS },
134
- pct("shadowOpacity", "Shadow opacity", e.shadowOpacity)
135
- ];
136
- case "satin":
137
- return [
138
- ...blended,
139
- { key: "color", label: "Color", type: "color", value: e.color },
140
- deg("angle", "Angle", e.angle),
141
- px("distance", "Distance", e.distance),
142
- px("size", "Size", e.size),
143
- { key: "invert", label: "Invert", type: "checkbox", value: e.invert }
144
- ];
145
- case "color-overlay":
146
- return [...blended, { key: "color", label: "Color", type: "color", value: e.color }];
147
- case "gradient-overlay":
148
- return [...blended, ...gradientControls("gradient", e.gradient)];
149
- case "pattern-overlay":
150
- return [
151
- ...blended,
152
- { key: "patternId", label: "Pattern", type: "select", value: e.patternId, options: patternOptions(patterns) },
153
- pct("scale", "Scale", e.scale, 0.01, 10),
154
- deg("angle", "Angle", e.angle ?? 0),
155
- { key: "link", label: "Link with layer", type: "checkbox", value: e.link ?? true }
156
- ];
157
- case "stroke":
158
- return [
159
- ...blended,
160
- px("size", "Size", e.size),
161
- { key: "position", label: "Position", type: "select", value: e.position, options: opt(["outside", "Outside"], ["inside", "Inside"], ["center", "Center"]) },
162
- ...fillControls(e.fill, true, patterns)
163
- ];
164
- }
165
- }
166
- function fillOfType(current, type, patterns) {
167
- if (type === "gradient") return { type: "gradient", gradient: defaultEffectGradient() };
168
- if (type === "pattern") return { type: "pattern", patternId: patterns[0]?.id ?? "checker", scale: 1 };
169
- return { type: "color", color: current.type === "color" ? current.color : "#000000" };
170
- }
171
- function effectControlChange(effect, key, value, patterns = []) {
172
- const [top, ...rest] = key.split(".");
173
- if (top === "fill" && rest[0] === "type") return { fill: fillOfType(effect.fill, String(value), patterns) };
174
- if (top === "patternId" && !rest.length) {
175
- const name = patternOptions(patterns).find((o) => o.value === value)?.label;
176
- return name ? { patternId: value, name } : { patternId: value };
177
- }
178
- if (!rest.length) return { [top]: value };
179
- if (top === "fill" && rest[0] === "patternId") {
180
- const name = patternOptions(patterns).find((o) => o.value === value)?.label;
181
- return { fill: { ...effect.fill, patternId: value, ...name ? { name } : {} } };
182
- }
183
- return { [top]: setPath(getPath(effect, top), rest, value) };
184
- }
185
-
186
- export {
187
- layerEffectsPanel,
188
- layerEffectsBadge,
189
- LAYER_EFFECT_MENU,
190
- layerEffectControls,
191
- effectControlChange
192
- };