@pixerate/editor 0.238.0 → 0.240.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Gradients from sampled colours: `simplifyGradientStops` reduces a noisy run of colours (pixels
3
+ * read along a line from a photo, a paint stroke or a shape) to the few stops that reproduce it
4
+ * within a perceptual tolerance, and `sampleGradientFromLine` reads those colours from any pixel
5
+ * reader and returns a linear gradient.
6
+ *
7
+ * Distances are Euclidean in OKLab (ΔE OK, about 0.02 is a just-noticeable step), against the
8
+ * OKLab interpolation the returned gradients use, so what is measured is what is drawn.
9
+ */
10
+ import type { Gradient, GradientStop } from './types.cjs';
11
+ export interface SimplifyGradientStopsOptions {
12
+ /** Largest perceptual error left between the samples and the stops, in OKLab ΔE (default 0.06). */
13
+ tolerance?: number;
14
+ /** Fewest stops to return (default 2, at least 2). */
15
+ minStops?: number;
16
+ /** Most stops to return (default 5, at least `minStops`). */
17
+ maxStops?: number;
18
+ /**
19
+ * Samples on each side averaged into each one before fitting, to ignore grain, JPEG noise and
20
+ * brush texture (default 2; 0 fits the raw samples).
21
+ */
22
+ smoothing?: number;
23
+ }
24
+ export interface RawGradientSample {
25
+ /** Position along the line, 0..1. */
26
+ offset: number;
27
+ /** Any CSS colour, or RGBA bytes `[r, g, b, a]` (0..255). */
28
+ color: string | readonly [number, number, number, number];
29
+ }
30
+ export interface SimplifiedGradientStops {
31
+ stops: GradientStop[];
32
+ /** The largest OKLab distance left between a (smoothed) sample and the stops. */
33
+ maxError: number;
34
+ }
35
+ /**
36
+ * Like `simplifyGradientStops`, with the largest error left. Throws a sentence when the input
37
+ * can't make a gradient (fewer than two readable samples, or options out of range).
38
+ */
39
+ export declare function fitGradientStops(samples: readonly RawGradientSample[], options?: SimplifyGradientStopsOptions): SimplifiedGradientStops;
40
+ /**
41
+ * Reduces colours sampled along a line (offsets 0..1) to a minimal set of gradient stops. The
42
+ * first and last stops sit at 0 and 1; stops are added where the colour turns most until every
43
+ * sample is within `tolerance` (OKLab ΔE) of the OKLab blend between its stops, or `maxStops` is
44
+ * reached. Samples that can't be read (a bad colour string) are skipped. Throws when fewer than two
45
+ * samples can be read.
46
+ */
47
+ export declare function simplifyGradientStops(samples: readonly RawGradientSample[], options?: SimplifyGradientStopsOptions): GradientStop[];
48
+ export interface LineSamplingOptions extends SimplifyGradientStopsOptions {
49
+ /** Colours read along the line, ends included (default 32, 2..1024). */
50
+ sampleCount?: number;
51
+ /** Snap the line's angle to multiples of this many degrees (e.g. 15, for Shift), keeping its length. */
52
+ snapStep?: number;
53
+ }
54
+ /**
55
+ * Reads colours along the line from `start` to `end` with `readPixel` (which returns a CSS colour,
56
+ * RGBA bytes, or null where there is nothing to read) and returns a linear gradient (OKLab
57
+ * interpolation) whose angle is the line's (CSS degrees) and whose stops are the simplified
58
+ * colours, offsets measured along the line. Returns null when fewer than two points could be read.
59
+ */
60
+ export declare function sampleGradientFromLine(readPixel: (point: {
61
+ x: number;
62
+ y: number;
63
+ }) => string | readonly [number, number, number, number] | null, start: {
64
+ x: number;
65
+ y: number;
66
+ }, end: {
67
+ x: number;
68
+ y: number;
69
+ }, options?: LineSamplingOptions): Gradient | null;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Gradients from sampled colours: `simplifyGradientStops` reduces a noisy run of colours (pixels
3
+ * read along a line from a photo, a paint stroke or a shape) to the few stops that reproduce it
4
+ * within a perceptual tolerance, and `sampleGradientFromLine` reads those colours from any pixel
5
+ * reader and returns a linear gradient.
6
+ *
7
+ * Distances are Euclidean in OKLab (ΔE OK, about 0.02 is a just-noticeable step), against the
8
+ * OKLab interpolation the returned gradients use, so what is measured is what is drawn.
9
+ */
10
+ import type { Gradient, GradientStop } from './types.js';
11
+ export interface SimplifyGradientStopsOptions {
12
+ /** Largest perceptual error left between the samples and the stops, in OKLab ΔE (default 0.06). */
13
+ tolerance?: number;
14
+ /** Fewest stops to return (default 2, at least 2). */
15
+ minStops?: number;
16
+ /** Most stops to return (default 5, at least `minStops`). */
17
+ maxStops?: number;
18
+ /**
19
+ * Samples on each side averaged into each one before fitting, to ignore grain, JPEG noise and
20
+ * brush texture (default 2; 0 fits the raw samples).
21
+ */
22
+ smoothing?: number;
23
+ }
24
+ export interface RawGradientSample {
25
+ /** Position along the line, 0..1. */
26
+ offset: number;
27
+ /** Any CSS colour, or RGBA bytes `[r, g, b, a]` (0..255). */
28
+ color: string | readonly [number, number, number, number];
29
+ }
30
+ export interface SimplifiedGradientStops {
31
+ stops: GradientStop[];
32
+ /** The largest OKLab distance left between a (smoothed) sample and the stops. */
33
+ maxError: number;
34
+ }
35
+ /**
36
+ * Like `simplifyGradientStops`, with the largest error left. Throws a sentence when the input
37
+ * can't make a gradient (fewer than two readable samples, or options out of range).
38
+ */
39
+ export declare function fitGradientStops(samples: readonly RawGradientSample[], options?: SimplifyGradientStopsOptions): SimplifiedGradientStops;
40
+ /**
41
+ * Reduces colours sampled along a line (offsets 0..1) to a minimal set of gradient stops. The
42
+ * first and last stops sit at 0 and 1; stops are added where the colour turns most until every
43
+ * sample is within `tolerance` (OKLab ΔE) of the OKLab blend between its stops, or `maxStops` is
44
+ * reached. Samples that can't be read (a bad colour string) are skipped. Throws when fewer than two
45
+ * samples can be read.
46
+ */
47
+ export declare function simplifyGradientStops(samples: readonly RawGradientSample[], options?: SimplifyGradientStopsOptions): GradientStop[];
48
+ export interface LineSamplingOptions extends SimplifyGradientStopsOptions {
49
+ /** Colours read along the line, ends included (default 32, 2..1024). */
50
+ sampleCount?: number;
51
+ /** Snap the line's angle to multiples of this many degrees (e.g. 15, for Shift), keeping its length. */
52
+ snapStep?: number;
53
+ }
54
+ /**
55
+ * Reads colours along the line from `start` to `end` with `readPixel` (which returns a CSS colour,
56
+ * RGBA bytes, or null where there is nothing to read) and returns a linear gradient (OKLab
57
+ * interpolation) whose angle is the line's (CSS degrees) and whose stops are the simplified
58
+ * colours, offsets measured along the line. Returns null when fewer than two points could be read.
59
+ */
60
+ export declare function sampleGradientFromLine(readPixel: (point: {
61
+ x: number;
62
+ y: number;
63
+ }) => string | readonly [number, number, number, number] | null, start: {
64
+ x: number;
65
+ y: number;
66
+ }, end: {
67
+ x: number;
68
+ y: number;
69
+ }, options?: LineSamplingOptions): Gradient | null;
@@ -1,6 +1,10 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7; var _class8; var _class9; var _class10; var _class11; var _class12; var _class13; var _class14; var _class15; var _class16; var _class17; var _class18;
2
2
 
3
3
 
4
+ var _chunkS6OT6CTScjs = require('../chunk-S6OT6CTS.cjs');
5
+
6
+
7
+
4
8
 
5
9
 
6
10
 
@@ -17538,7 +17542,50 @@ function createImagePaintTools(controller) {
17538
17542
  return { color, foreground: controller.getState().paint.foreground };
17539
17543
  }
17540
17544
  };
17541
- return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool, setPaint, getPaint, sampleColor2];
17545
+ const sampleGradient = {
17546
+ name: "sample_gradient",
17547
+ title: "Sample a gradient along a line",
17548
+ description: "Reads the composite colours along a line from start to end (document pixels), smooths away grain and fits the fewest gradient stops that reproduce them within tolerance. Changes nothing. Returns a linear gradient (CSS angle of the line, oklab interpolation, stops with offsets along the line), its CSS, and how many points could be read. Pass the gradient to a gradient tool (set_gradient) or its stops as paint (set_paint).",
17549
+ inputSchema: {
17550
+ type: "object",
17551
+ properties: {
17552
+ start: { type: "object", properties: { x: { type: "number", description: "Document x in pixels." }, y: { type: "number", description: "Document y in pixels." } }, required: ["x", "y"], additionalProperties: false, description: "Where the gradient starts (offset 0)." },
17553
+ end: { type: "object", properties: { x: { type: "number", description: "Document x in pixels." }, y: { type: "number", description: "Document y in pixels." } }, required: ["x", "y"], additionalProperties: false, description: "Where it ends (offset 1)." },
17554
+ sampleCount: { type: "integer", minimum: 2, maximum: 1024, description: "Points read along the line, ends included (default 32)." },
17555
+ sampleSize: { type: "integer", minimum: 1, maximum: 101, description: "Square averaged at each point, in pixels (default 3)." },
17556
+ tolerance: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Largest error left, OKLab \u0394E (default 0.06; 0.02 is a just-noticeable step)." },
17557
+ maxStops: { type: "integer", minimum: 2, maximum: 64, description: "Most stops (default 5)." },
17558
+ snapStep: { type: "number", exclusiveMinimum: 0, maximum: 180, description: "Snap the line to multiples of this many degrees, keeping its length (default no snapping)." }
17559
+ },
17560
+ required: ["start", "end"],
17561
+ additionalProperties: false
17562
+ },
17563
+ annotations: { title: "Sample a gradient along a line", ...READ4 },
17564
+ handler: (args) => {
17565
+ const start = args.start;
17566
+ const end = args.end;
17567
+ const size = _nullishCoalesce(args.sampleSize, () => ( 3));
17568
+ let read = 0;
17569
+ let gradient;
17570
+ try {
17571
+ gradient = _chunkS6OT6CTScjs.sampleGradientFromLine.call(void 0,
17572
+ (p) => {
17573
+ const c = controller.sampleColor(p, { size, setForeground: false });
17574
+ if (c) read++;
17575
+ return c;
17576
+ },
17577
+ start,
17578
+ end,
17579
+ { sampleCount: args.sampleCount, tolerance: args.tolerance, maxStops: args.maxStops, snapStep: args.snapStep }
17580
+ );
17581
+ } catch (e) {
17582
+ throw new (0, _chunkBBUE3UJLcjs.LayerOperationError)(e.message);
17583
+ }
17584
+ if (!gradient) throw new (0, _chunkBBUE3UJLcjs.LayerOperationError)(`Fewer than two points along the line could be read: it runs outside the canvas or over transparent pixels. Read the canvas size with get_image_info and draw the line across visible pixels.`);
17585
+ return { gradient, css: _chunkS6OT6CTScjs.gradientToCss.call(void 0, gradient), pointsRead: read };
17586
+ }
17587
+ };
17588
+ return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool, setPaint, getPaint, sampleColor2, sampleGradient];
17542
17589
  }
17543
17590
 
17544
17591
  // src/image-editor/color-tools.ts
@@ -1,3 +1,7 @@
1
+ import {
2
+ gradientToCss,
3
+ sampleGradientFromLine
4
+ } from "../chunk-KMPM5JM5.js";
1
5
  import {
2
6
  bendSegment,
3
7
  loadOpenTypeFont,
@@ -17538,7 +17542,50 @@ function createImagePaintTools(controller) {
17538
17542
  return { color, foreground: controller.getState().paint.foreground };
17539
17543
  }
17540
17544
  };
17541
- return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool, setPaint, getPaint, sampleColor2];
17545
+ const sampleGradient = {
17546
+ name: "sample_gradient",
17547
+ title: "Sample a gradient along a line",
17548
+ description: "Reads the composite colours along a line from start to end (document pixels), smooths away grain and fits the fewest gradient stops that reproduce them within tolerance. Changes nothing. Returns a linear gradient (CSS angle of the line, oklab interpolation, stops with offsets along the line), its CSS, and how many points could be read. Pass the gradient to a gradient tool (set_gradient) or its stops as paint (set_paint).",
17549
+ inputSchema: {
17550
+ type: "object",
17551
+ properties: {
17552
+ start: { type: "object", properties: { x: { type: "number", description: "Document x in pixels." }, y: { type: "number", description: "Document y in pixels." } }, required: ["x", "y"], additionalProperties: false, description: "Where the gradient starts (offset 0)." },
17553
+ end: { type: "object", properties: { x: { type: "number", description: "Document x in pixels." }, y: { type: "number", description: "Document y in pixels." } }, required: ["x", "y"], additionalProperties: false, description: "Where it ends (offset 1)." },
17554
+ sampleCount: { type: "integer", minimum: 2, maximum: 1024, description: "Points read along the line, ends included (default 32)." },
17555
+ sampleSize: { type: "integer", minimum: 1, maximum: 101, description: "Square averaged at each point, in pixels (default 3)." },
17556
+ tolerance: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Largest error left, OKLab \u0394E (default 0.06; 0.02 is a just-noticeable step)." },
17557
+ maxStops: { type: "integer", minimum: 2, maximum: 64, description: "Most stops (default 5)." },
17558
+ snapStep: { type: "number", exclusiveMinimum: 0, maximum: 180, description: "Snap the line to multiples of this many degrees, keeping its length (default no snapping)." }
17559
+ },
17560
+ required: ["start", "end"],
17561
+ additionalProperties: false
17562
+ },
17563
+ annotations: { title: "Sample a gradient along a line", ...READ4 },
17564
+ handler: (args) => {
17565
+ const start = args.start;
17566
+ const end = args.end;
17567
+ const size = args.sampleSize ?? 3;
17568
+ let read = 0;
17569
+ let gradient;
17570
+ try {
17571
+ gradient = sampleGradientFromLine(
17572
+ (p) => {
17573
+ const c = controller.sampleColor(p, { size, setForeground: false });
17574
+ if (c) read++;
17575
+ return c;
17576
+ },
17577
+ start,
17578
+ end,
17579
+ { sampleCount: args.sampleCount, tolerance: args.tolerance, maxStops: args.maxStops, snapStep: args.snapStep }
17580
+ );
17581
+ } catch (e) {
17582
+ throw new LayerOperationError(e.message);
17583
+ }
17584
+ if (!gradient) throw new LayerOperationError(`Fewer than two points along the line could be read: it runs outside the canvas or over transparent pixels. Read the canvas size with get_image_info and draw the line across visible pixels.`);
17585
+ return { gradient, css: gradientToCss(gradient), pointsRead: read };
17586
+ }
17587
+ };
17588
+ return [paintStroke, fill, clear, getMixer, loadMixerTool, cleanMixerTool, setPaint, getPaint, sampleColor2, sampleGradient];
17542
17589
  }
17543
17590
 
17544
17591
  // src/image-editor/color-tools.ts
@@ -112,6 +112,8 @@ export interface PaintEditorState {
112
112
  mixerLoad: MixerLoad;
113
113
  view: PaintView;
114
114
  hand: Hand;
115
+ /** The stroke picked on a stroke layer, on the canvas or in the stroke panel (editor state, not an undo step). */
116
+ selectedStroke: string | null;
115
117
  /** The sticker picked on the canvas or in the sticker panel (editor state, not an undo step). */
116
118
  selectedSticker: string | null;
117
119
  canUndo: boolean;
@@ -134,6 +136,19 @@ export interface PaintEditorState {
134
136
  version: number;
135
137
  }
136
138
  /** A layer as tools and panels describe it. */
139
+ /** A change to a live stroke (`updateStroke`): every field is optional. */
140
+ export interface LiveStrokeUpdate {
141
+ /** Another brush preset; the stroke keeps its size. */
142
+ brushId?: string;
143
+ /** Brush settings to change (as `setBrush` takes), on top of `brushId` or the stroke's brush. */
144
+ brush?: Omit<PaintBrushInput, 'id'>;
145
+ /** A new colour #rrggbb (replaces gradient or effect paint). */
146
+ color?: string;
147
+ /** Paint: a colour, gradient paint or effect paint; `null` back to the plain colour. */
148
+ paint?: unknown;
149
+ /** Another jitter seed (particles, scatter and texture fall differently). */
150
+ seed?: number;
151
+ }
137
152
  export interface PaintLayerInfo {
138
153
  id: string;
139
154
  name: string;
@@ -148,6 +163,8 @@ export interface PaintLayerInfo {
148
163
  bounds: RasterRect | null;
149
164
  parentId: string | null;
150
165
  depth: number;
166
+ /** A live stroke layer (its pixels are drawn from its strokes, which stay editable): how many strokes it has. */
167
+ liveStrokes?: number;
151
168
  }
152
169
  export interface PaintStrokeInit {
153
170
  /** Default the active layer. */
@@ -277,6 +294,7 @@ export declare class PaintEditorController {
277
294
  private mixerLoad;
278
295
  private view;
279
296
  private hand;
297
+ private selectedStroke;
280
298
  private selectedSticker;
281
299
  /** A sticker drag in progress (`previewSticker`): the document before it, so the drop undoes in one step, and the one it showed last. */
282
300
  private stickerPreview;
@@ -290,6 +308,8 @@ export declare class PaintEditorController {
290
308
  private dirty;
291
309
  private seedCounter;
292
310
  private strokeCounter;
311
+ /** The area each stroke painted (document pixels; `null`: nothing), learnt as it is painted or drawn, so a stroke layer edit redraws only where it changes (see `drawLiveStrokes`). */
312
+ private paintedBounds;
293
313
  private guides;
294
314
  private swatches;
295
315
  private readonly pad;
@@ -403,9 +423,10 @@ export declare class PaintEditorController {
403
423
  private paintTarget;
404
424
  setActiveLayer(id: string): void;
405
425
  /** Adds an empty pixel layer or group above `aboveId` (default: the active layer) and makes it active. */
426
+ /** Adds a layer above the active one: `pixel` (default), `group`, or `strokes`, a live stroke layer whose strokes stay editable. */
406
427
  addLayer(options?: {
407
428
  name?: string;
408
- type?: 'pixel' | 'group';
429
+ type?: 'pixel' | 'group' | 'strokes';
409
430
  aboveId?: string;
410
431
  parentId?: string | null;
411
432
  }): PaintLayerInfo;
@@ -1000,6 +1021,75 @@ export declare class PaintEditorController {
1000
1021
  removeStickers(layerId: string | undefined, ids?: readonly string[]): string[];
1001
1022
  /** Sets a layer's stickers (replays). */
1002
1023
  replaceStickers(layerId: string, stickers: readonly Sticker[]): void;
1024
+ /** The document's `liveStrokes` without these layers (as a spread: `{}` when unchanged). */
1025
+ private liveStrokesWithout;
1026
+ private liveStrokeLayer;
1027
+ /** Whether a layer is a live stroke layer. */
1028
+ isLiveStrokeLayer(layerId: string): boolean;
1029
+ /** Makes an empty pixel layer a live stroke layer (one undo step). */
1030
+ convertToLiveStrokes(layerId?: string): PaintLayerInfo;
1031
+ /** A stroke layer's strokes, bottom first. */
1032
+ listLiveStrokes(layerId?: string): RecordedStroke[];
1033
+ /** The stroke layer holding a live stroke, and where. */
1034
+ private findLiveStroke;
1035
+ /**
1036
+ * Changes a live stroke's brush (a preset by `brushId`, keeping the stroke's size, and/or brush
1037
+ * settings), colour, paint or seed, and redraws its layer (one undo step). A new colour replaces
1038
+ * gradient or effect paint. Returns the stroke and the area that changed.
1039
+ */
1040
+ updateStroke(id: string, update: LiveStrokeUpdate): {
1041
+ stroke: RecordedStroke;
1042
+ bounds: RasterRect;
1043
+ };
1044
+ /**
1045
+ * Replaces a live stroke's points, or moves, scales, rotates or flips them (`transform`, about
1046
+ * the stroke's centre unless an origin is given; the brush size scales too), and redraws its
1047
+ * layer (one undo step). Returns the stroke and the area that changed.
1048
+ */
1049
+ editStrokePoints(id: string, edit: {
1050
+ points?: readonly PenPoint[];
1051
+ transform?: StrokeVariation;
1052
+ }): {
1053
+ stroke: RecordedStroke;
1054
+ bounds: RasterRect;
1055
+ };
1056
+ /** Deletes live strokes (one undo step). Returns the ids removed and the area that changed. */
1057
+ removeStrokes(ids: readonly string[]): {
1058
+ removed: string[];
1059
+ bounds: RasterRect | null;
1060
+ };
1061
+ /** Moves a live stroke to `index` in its layer (0 = bottom) and redraws (one undo step). Returns the new order. */
1062
+ reorderStroke(id: string, index: number): string[];
1063
+ /** Turns a stroke layer into a plain pixel layer with the same pixels (one undo step). */
1064
+ rasterizeLiveStrokes(layerId?: string): PaintLayerInfo;
1065
+ /** Sets a layer's live strokes and redraws it (replays). `null` makes it a plain pixel layer. */
1066
+ replaceLiveStrokes(layerId: string, strokes: readonly RecordedStroke[] | null): void;
1067
+ /**
1068
+ * The top-most stroke of a stroke layer (default the active layer) that painted the pixel at
1069
+ * (x, y) document pixels, or `null`. Strokes whose area holds the point are drawn alone, top
1070
+ * first, until one covers it.
1071
+ */
1072
+ liveStrokeAt(layerId: string | undefined, x: number, y: number): RecordedStroke | null;
1073
+ /** Picks a stroke for the stroke panel and canvas (`null` for none). Editor state, not an undo step. */
1074
+ selectStroke(id: string | null): void;
1075
+ /** Where a live stroke painted (document pixels), or `null`. */
1076
+ liveStrokeArea(id: string): RasterRect | null;
1077
+ /** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
1078
+ private commitLiveStrokes;
1079
+ private withLiveStrokes;
1080
+ /** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
1081
+ private strokeArea;
1082
+ /** The areas strokes painted, or `null` when one is not known (a full redraw then). */
1083
+ private strokeAreas;
1084
+ /** Draws one stroke onto `surface` of a plain copy of the layer; returns the result and the area it painted. */
1085
+ private drawLiveStroke;
1086
+ /**
1087
+ * A stroke layer's pixels drawn from its strokes, bottom first. With `dirty` (the areas the old
1088
+ * strokes that changed had painted), only the tiles there and under any stroke whose area isn't
1089
+ * known yet are drawn again, from the strokes that reach them (a pixel depends only on the
1090
+ * strokes over it); the other tiles are kept. Without it, or with `null`, every stroke is drawn.
1091
+ */
1092
+ private drawLiveStrokes;
1003
1093
  private stencilCache;
1004
1094
  private nextStencilId;
1005
1095
  /** The mask stencils' coverage over the canvas (cached), or `null` when none masks. */
@@ -112,6 +112,8 @@ export interface PaintEditorState {
112
112
  mixerLoad: MixerLoad;
113
113
  view: PaintView;
114
114
  hand: Hand;
115
+ /** The stroke picked on a stroke layer, on the canvas or in the stroke panel (editor state, not an undo step). */
116
+ selectedStroke: string | null;
115
117
  /** The sticker picked on the canvas or in the sticker panel (editor state, not an undo step). */
116
118
  selectedSticker: string | null;
117
119
  canUndo: boolean;
@@ -134,6 +136,19 @@ export interface PaintEditorState {
134
136
  version: number;
135
137
  }
136
138
  /** A layer as tools and panels describe it. */
139
+ /** A change to a live stroke (`updateStroke`): every field is optional. */
140
+ export interface LiveStrokeUpdate {
141
+ /** Another brush preset; the stroke keeps its size. */
142
+ brushId?: string;
143
+ /** Brush settings to change (as `setBrush` takes), on top of `brushId` or the stroke's brush. */
144
+ brush?: Omit<PaintBrushInput, 'id'>;
145
+ /** A new colour #rrggbb (replaces gradient or effect paint). */
146
+ color?: string;
147
+ /** Paint: a colour, gradient paint or effect paint; `null` back to the plain colour. */
148
+ paint?: unknown;
149
+ /** Another jitter seed (particles, scatter and texture fall differently). */
150
+ seed?: number;
151
+ }
137
152
  export interface PaintLayerInfo {
138
153
  id: string;
139
154
  name: string;
@@ -148,6 +163,8 @@ export interface PaintLayerInfo {
148
163
  bounds: RasterRect | null;
149
164
  parentId: string | null;
150
165
  depth: number;
166
+ /** A live stroke layer (its pixels are drawn from its strokes, which stay editable): how many strokes it has. */
167
+ liveStrokes?: number;
151
168
  }
152
169
  export interface PaintStrokeInit {
153
170
  /** Default the active layer. */
@@ -277,6 +294,7 @@ export declare class PaintEditorController {
277
294
  private mixerLoad;
278
295
  private view;
279
296
  private hand;
297
+ private selectedStroke;
280
298
  private selectedSticker;
281
299
  /** A sticker drag in progress (`previewSticker`): the document before it, so the drop undoes in one step, and the one it showed last. */
282
300
  private stickerPreview;
@@ -290,6 +308,8 @@ export declare class PaintEditorController {
290
308
  private dirty;
291
309
  private seedCounter;
292
310
  private strokeCounter;
311
+ /** The area each stroke painted (document pixels; `null`: nothing), learnt as it is painted or drawn, so a stroke layer edit redraws only where it changes (see `drawLiveStrokes`). */
312
+ private paintedBounds;
293
313
  private guides;
294
314
  private swatches;
295
315
  private readonly pad;
@@ -403,9 +423,10 @@ export declare class PaintEditorController {
403
423
  private paintTarget;
404
424
  setActiveLayer(id: string): void;
405
425
  /** Adds an empty pixel layer or group above `aboveId` (default: the active layer) and makes it active. */
426
+ /** Adds a layer above the active one: `pixel` (default), `group`, or `strokes`, a live stroke layer whose strokes stay editable. */
406
427
  addLayer(options?: {
407
428
  name?: string;
408
- type?: 'pixel' | 'group';
429
+ type?: 'pixel' | 'group' | 'strokes';
409
430
  aboveId?: string;
410
431
  parentId?: string | null;
411
432
  }): PaintLayerInfo;
@@ -1000,6 +1021,75 @@ export declare class PaintEditorController {
1000
1021
  removeStickers(layerId: string | undefined, ids?: readonly string[]): string[];
1001
1022
  /** Sets a layer's stickers (replays). */
1002
1023
  replaceStickers(layerId: string, stickers: readonly Sticker[]): void;
1024
+ /** The document's `liveStrokes` without these layers (as a spread: `{}` when unchanged). */
1025
+ private liveStrokesWithout;
1026
+ private liveStrokeLayer;
1027
+ /** Whether a layer is a live stroke layer. */
1028
+ isLiveStrokeLayer(layerId: string): boolean;
1029
+ /** Makes an empty pixel layer a live stroke layer (one undo step). */
1030
+ convertToLiveStrokes(layerId?: string): PaintLayerInfo;
1031
+ /** A stroke layer's strokes, bottom first. */
1032
+ listLiveStrokes(layerId?: string): RecordedStroke[];
1033
+ /** The stroke layer holding a live stroke, and where. */
1034
+ private findLiveStroke;
1035
+ /**
1036
+ * Changes a live stroke's brush (a preset by `brushId`, keeping the stroke's size, and/or brush
1037
+ * settings), colour, paint or seed, and redraws its layer (one undo step). A new colour replaces
1038
+ * gradient or effect paint. Returns the stroke and the area that changed.
1039
+ */
1040
+ updateStroke(id: string, update: LiveStrokeUpdate): {
1041
+ stroke: RecordedStroke;
1042
+ bounds: RasterRect;
1043
+ };
1044
+ /**
1045
+ * Replaces a live stroke's points, or moves, scales, rotates or flips them (`transform`, about
1046
+ * the stroke's centre unless an origin is given; the brush size scales too), and redraws its
1047
+ * layer (one undo step). Returns the stroke and the area that changed.
1048
+ */
1049
+ editStrokePoints(id: string, edit: {
1050
+ points?: readonly PenPoint[];
1051
+ transform?: StrokeVariation;
1052
+ }): {
1053
+ stroke: RecordedStroke;
1054
+ bounds: RasterRect;
1055
+ };
1056
+ /** Deletes live strokes (one undo step). Returns the ids removed and the area that changed. */
1057
+ removeStrokes(ids: readonly string[]): {
1058
+ removed: string[];
1059
+ bounds: RasterRect | null;
1060
+ };
1061
+ /** Moves a live stroke to `index` in its layer (0 = bottom) and redraws (one undo step). Returns the new order. */
1062
+ reorderStroke(id: string, index: number): string[];
1063
+ /** Turns a stroke layer into a plain pixel layer with the same pixels (one undo step). */
1064
+ rasterizeLiveStrokes(layerId?: string): PaintLayerInfo;
1065
+ /** Sets a layer's live strokes and redraws it (replays). `null` makes it a plain pixel layer. */
1066
+ replaceLiveStrokes(layerId: string, strokes: readonly RecordedStroke[] | null): void;
1067
+ /**
1068
+ * The top-most stroke of a stroke layer (default the active layer) that painted the pixel at
1069
+ * (x, y) document pixels, or `null`. Strokes whose area holds the point are drawn alone, top
1070
+ * first, until one covers it.
1071
+ */
1072
+ liveStrokeAt(layerId: string | undefined, x: number, y: number): RecordedStroke | null;
1073
+ /** Picks a stroke for the stroke panel and canvas (`null` for none). Editor state, not an undo step. */
1074
+ selectStroke(id: string | null): void;
1075
+ /** Where a live stroke painted (document pixels), or `null`. */
1076
+ liveStrokeArea(id: string): RasterRect | null;
1077
+ /** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
1078
+ private commitLiveStrokes;
1079
+ private withLiveStrokes;
1080
+ /** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
1081
+ private strokeArea;
1082
+ /** The areas strokes painted, or `null` when one is not known (a full redraw then). */
1083
+ private strokeAreas;
1084
+ /** Draws one stroke onto `surface` of a plain copy of the layer; returns the result and the area it painted. */
1085
+ private drawLiveStroke;
1086
+ /**
1087
+ * A stroke layer's pixels drawn from its strokes, bottom first. With `dirty` (the areas the old
1088
+ * strokes that changed had painted), only the tiles there and under any stroke whose area isn't
1089
+ * known yet are drawn again, from the strokes that reach them (a pixel depends only on the
1090
+ * strokes over it); the other tiles are kept. Without it, or with `null`, every stroke is drawn.
1091
+ */
1092
+ private drawLiveStrokes;
1003
1093
  private stencilCache;
1004
1094
  private nextStencilId;
1005
1095
  /** The mask stencils' coverage over the canvas (cached), or `null` when none masks. */
@@ -43,6 +43,11 @@ export interface PaintDocumentState {
43
43
  paintEvents?: PaintEvent[];
44
44
  /** Sticker layers: each layer's stickers, bottom first (its pixels are drawn from them). */
45
45
  stickers?: Readonly<Record<string, Sticker[]>>;
46
+ /**
47
+ * Live stroke layers: each layer's strokes, bottom first. Its pixels are drawn from them, so each
48
+ * stroke's brush, colour and points stay editable (`updateStroke`).
49
+ */
50
+ liveStrokes?: Readonly<Record<string, RecordedStroke[]>>;
46
51
  /** Thick paint: each layer's height and material channels (layers without relief are missing). */
47
52
  thick?: Readonly<Record<string, ThickChannels>>;
48
53
  /** Effect layers: the provider effect each one is drawn from (layers that are gone are ignored). */
@@ -61,9 +66,9 @@ export interface PaintDocumentState {
61
66
  /** A change a replay repeats besides strokes, recorded with how many strokes came before it. */
62
67
  export interface PaintEvent {
63
68
  at: number;
64
- kind: 'fill' | 'clear' | 'flatten' | 'stickers';
69
+ kind: 'fill' | 'clear' | 'flatten' | 'stickers' | 'live-strokes';
65
70
  layerId: string;
66
- /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers after an edit. */
71
+ /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers or live strokes after an edit. */
67
72
  data?: Record<string, unknown>;
68
73
  }
69
74
  /** A new layer id, unique in this session. */
@@ -43,6 +43,11 @@ export interface PaintDocumentState {
43
43
  paintEvents?: PaintEvent[];
44
44
  /** Sticker layers: each layer's stickers, bottom first (its pixels are drawn from them). */
45
45
  stickers?: Readonly<Record<string, Sticker[]>>;
46
+ /**
47
+ * Live stroke layers: each layer's strokes, bottom first. Its pixels are drawn from them, so each
48
+ * stroke's brush, colour and points stay editable (`updateStroke`).
49
+ */
50
+ liveStrokes?: Readonly<Record<string, RecordedStroke[]>>;
46
51
  /** Thick paint: each layer's height and material channels (layers without relief are missing). */
47
52
  thick?: Readonly<Record<string, ThickChannels>>;
48
53
  /** Effect layers: the provider effect each one is drawn from (layers that are gone are ignored). */
@@ -61,9 +66,9 @@ export interface PaintDocumentState {
61
66
  /** A change a replay repeats besides strokes, recorded with how many strokes came before it. */
62
67
  export interface PaintEvent {
63
68
  at: number;
64
- kind: 'fill' | 'clear' | 'flatten' | 'stickers';
69
+ kind: 'fill' | 'clear' | 'flatten' | 'stickers' | 'live-strokes';
65
70
  layerId: string;
66
- /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers after an edit. */
71
+ /** The fill's options (colour, opacity, grain, pressure, blend), or the layer's stickers or live strokes after an edit. */
67
72
  data?: Record<string, unknown>;
68
73
  }
69
74
  /** A new layer id, unique in this session. */