@pixerate/editor 0.260.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.
- package/dist/paint-editor/controller.d.cts +17 -0
- package/dist/paint-editor/controller.d.ts +17 -0
- package/dist/paint-editor/index.cjs +223 -84
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +140 -1
- package/dist/paint-editor/path-samples.d.cts +19 -0
- package/dist/paint-editor/path-samples.d.ts +19 -0
- package/dist/paint-editor/ui.d.cts +6 -0
- package/dist/paint-editor/ui.d.ts +6 -0
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +2 -0
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +5 -1
|
@@ -57,4 +57,5 @@ export type { StickerProvider, StickerRef, StickerSummary } from '../stickers/ty
|
|
|
57
57
|
export * from './sticker-art.cjs';
|
|
58
58
|
export * from './collaboration.cjs';
|
|
59
59
|
export * from './collaboration-tools.cjs';
|
|
60
|
+
export { samplePath, MAX_PATH_SAMPLES, type PathSampleOptions } from './path-samples.cjs';
|
|
60
61
|
export { paintContracts, PAINT_CONTRACT_FORMAT_VERSION, PAINT_DOCUMENT_JSON_SCHEMA } from './contract.cjs';
|
|
@@ -57,4 +57,5 @@ export type { StickerProvider, StickerRef, StickerSummary } from '../stickers/ty
|
|
|
57
57
|
export * from './sticker-art.js';
|
|
58
58
|
export * from './collaboration.js';
|
|
59
59
|
export * from './collaboration-tools.js';
|
|
60
|
+
export { samplePath, MAX_PATH_SAMPLES, type PathSampleOptions } from './path-samples.js';
|
|
60
61
|
export { paintContracts, PAINT_CONTRACT_FORMAT_VERSION, PAINT_DOCUMENT_JSON_SCHEMA } from './contract.js';
|
|
@@ -150,7 +150,11 @@ import {
|
|
|
150
150
|
stickerIdFor
|
|
151
151
|
} from "../chunk-APX63ILU.js";
|
|
152
152
|
import "../chunk-M6DNZNVY.js";
|
|
153
|
-
import
|
|
153
|
+
import {
|
|
154
|
+
bezierPathToSvg,
|
|
155
|
+
flattenChain,
|
|
156
|
+
pathToContours
|
|
157
|
+
} from "../chunk-WEIK3UYB.js";
|
|
154
158
|
import "../chunk-PWBZH3FK.js";
|
|
155
159
|
import {
|
|
156
160
|
brushColourHex
|
|
@@ -5221,6 +5225,55 @@ function engineChanges(base, next) {
|
|
|
5221
5225
|
return rest;
|
|
5222
5226
|
}
|
|
5223
5227
|
|
|
5228
|
+
// src/paint-editor/path-samples.ts
|
|
5229
|
+
var FLATTEN_TOLERANCE = 0.1;
|
|
5230
|
+
var MAX_PATH_SAMPLES = 2e4;
|
|
5231
|
+
function profileAt(profile, t) {
|
|
5232
|
+
if (profile.length === 1) return profile[0];
|
|
5233
|
+
const at = t * (profile.length - 1);
|
|
5234
|
+
const i = Math.min(profile.length - 2, Math.floor(at));
|
|
5235
|
+
return profile[i] + (profile[i + 1] - profile[i]) * (at - i);
|
|
5236
|
+
}
|
|
5237
|
+
function samplePath(d, options = {}) {
|
|
5238
|
+
const spacing = options.spacing ?? 2;
|
|
5239
|
+
if (!(spacing >= 0.25 && spacing <= 100)) throw new Error(`spacing ${spacing} is out of range; use 0.25..100 document pixels.`);
|
|
5240
|
+
const profile = options.pressure;
|
|
5241
|
+
if (profile && (!profile.length || profile.some((p) => !(p >= 0 && p <= 1)))) throw new Error("pressure needs one or more values from 0 to 1, e.g. [0.2, 1, 0.2] for a taper.");
|
|
5242
|
+
let contours;
|
|
5243
|
+
try {
|
|
5244
|
+
contours = pathToContours(d);
|
|
5245
|
+
} catch (e) {
|
|
5246
|
+
throw new Error(`path is not SVG path data (${e instanceof Error ? e.message : String(e)}); give e.g. "M 10 10 C 40 0 80 60 120 30".`);
|
|
5247
|
+
}
|
|
5248
|
+
const out = [];
|
|
5249
|
+
for (const contour of contours) {
|
|
5250
|
+
if (!contour.segments.length) continue;
|
|
5251
|
+
const line = flattenChain(contour.segments, FLATTEN_TOLERANCE);
|
|
5252
|
+
if (contour.closed && line.length) line.push(line[0]);
|
|
5253
|
+
const lengths = [0];
|
|
5254
|
+
for (let i = 1; i < line.length; i++) lengths.push(lengths[i - 1] + Math.hypot(line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1]));
|
|
5255
|
+
const total = lengths[lengths.length - 1];
|
|
5256
|
+
const n = Math.max(1, Math.ceil(total / spacing));
|
|
5257
|
+
if (n + 1 > MAX_PATH_SAMPLES) throw new Error(`The path is ${Math.round(total)} px long: too many samples at spacing ${spacing}; use a larger spacing (at least ${Math.ceil(total / (MAX_PATH_SAMPLES - 1))}).`);
|
|
5258
|
+
const points = [];
|
|
5259
|
+
let j = 0;
|
|
5260
|
+
for (let k = 0; k <= n; k++) {
|
|
5261
|
+
const at = total * k / n;
|
|
5262
|
+
while (j < line.length - 2 && lengths[j + 1] < at) j++;
|
|
5263
|
+
const span = lengths[j + 1] - lengths[j] || 1;
|
|
5264
|
+
const f = line.length > 1 ? Math.min(1, Math.max(0, (at - lengths[j]) / span)) : 0;
|
|
5265
|
+
const a = line[j];
|
|
5266
|
+
const b = line[Math.min(j + 1, line.length - 1)];
|
|
5267
|
+
const x = Math.round((a[0] + (b[0] - a[0]) * f) * 1e3) / 1e3;
|
|
5268
|
+
const y = Math.round((a[1] + (b[1] - a[1]) * f) * 1e3) / 1e3;
|
|
5269
|
+
points.push(profile ? { x, y, pressure: Math.round(profileAt(profile, k / n) * 1e3) / 1e3 } : { x, y });
|
|
5270
|
+
}
|
|
5271
|
+
out.push(points);
|
|
5272
|
+
}
|
|
5273
|
+
if (!out.length) throw new Error('path has no segments to brush; give SVG path data such as "M 10 10 L 200 80".');
|
|
5274
|
+
return out;
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5224
5277
|
// src/paint-editor/live-strokes.ts
|
|
5225
5278
|
var LIVE_STROKE_ENGINES = ["dab", "particle", "bristle", "mixer"];
|
|
5226
5279
|
function liveStrokeRefusal(brush) {
|
|
@@ -7857,6 +7910,47 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7857
7910
|
else this.emit([]);
|
|
7858
7911
|
return result;
|
|
7859
7912
|
}
|
|
7913
|
+
/**
|
|
7914
|
+
* Brushes vector path data (SVG `d`, document pixels) with the active brush, or `brushId` /
|
|
7915
|
+
* `brush`, as if drawn by hand: one recorded stroke per subpath, sampled every `spacing` px
|
|
7916
|
+
* with an optional `pressure` profile along each. All of it is one undo step. A given `seed`
|
|
7917
|
+
* seeds the first subpath; the next ones take the following seeds.
|
|
7918
|
+
*/
|
|
7919
|
+
strokePath(init) {
|
|
7920
|
+
this.requireIdle("painting");
|
|
7921
|
+
if (typeof init.path !== "string" || !init.path.trim()) throw new PaintEditorError('path is empty; give SVG path data in document pixels, e.g. "M 10 10 C 40 0 80 60 120 30".');
|
|
7922
|
+
let subpaths;
|
|
7923
|
+
try {
|
|
7924
|
+
subpaths = samplePath(init.path, init);
|
|
7925
|
+
} catch (e) {
|
|
7926
|
+
throw new PaintEditorError(e instanceof Error ? e.message : String(e));
|
|
7927
|
+
}
|
|
7928
|
+
const { path: _path, spacing: _spacing, pressure: _pressure, ...stroke } = init;
|
|
7929
|
+
const before = this.doc;
|
|
7930
|
+
let doc = this.doc;
|
|
7931
|
+
let bounds = null;
|
|
7932
|
+
let dabs = 0;
|
|
7933
|
+
const strokeIds = [];
|
|
7934
|
+
let last = null;
|
|
7935
|
+
try {
|
|
7936
|
+
subpaths.forEach((points, i) => {
|
|
7937
|
+
last = this.runStroke(doc, { ...stroke, ...stroke.seed !== void 0 ? { seed: stroke.seed + i } : {}, start: points[0] }, points);
|
|
7938
|
+
doc = last.doc;
|
|
7939
|
+
bounds = unionRect(bounds, last.result.bounds);
|
|
7940
|
+
dabs += last.result.dabs;
|
|
7941
|
+
if (last.result.strokeId) strokeIds.push(last.result.strokeId);
|
|
7942
|
+
});
|
|
7943
|
+
} catch (e) {
|
|
7944
|
+
this.wetOp = null;
|
|
7945
|
+
throw e;
|
|
7946
|
+
}
|
|
7947
|
+
const run = last;
|
|
7948
|
+
if (run?.result.mixerLoad) this.keepLoad(run.brush, run.result.mixerLoad);
|
|
7949
|
+
if (doc !== before || this.wetOp) this.commit(`${run?.brush.name ?? "Brush"} path stroke`, this.withWetEvents(doc), before);
|
|
7950
|
+
else this.emit([]);
|
|
7951
|
+
const layerId = run?.result.layerId ?? stroke.layerId ?? this.doc.activeLayerId ?? "";
|
|
7952
|
+
return { layerId, bounds, dabs, strokeIds, ...run?.result.mixerLoad ? { mixerLoad: run.result.mixerLoad } : {}, ...run?.result.wet !== void 0 ? { wet: run.result.wet } : {} };
|
|
7953
|
+
}
|
|
7860
7954
|
/** Checks stroke samples (finite x and y, pressure 0..1) and returns them; throws a sentence naming the bad point. */
|
|
7861
7955
|
checkPoints(points) {
|
|
7862
7956
|
if (!Array.isArray(points) || !points.length) throw new PaintEditorError("points is empty; give at least one {x, y} in document pixels.");
|
|
@@ -11579,6 +11673,41 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11579
11673
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11580
11674
|
}
|
|
11581
11675
|
});
|
|
11676
|
+
add({
|
|
11677
|
+
name: "stroke_path",
|
|
11678
|
+
title: "Brush along a path",
|
|
11679
|
+
description: "Brushes vector path data (SVG path d in document pixels: M, L, H, V, C, S, Q, T, A and Z) on a pixel layer with a library brush or the active one, as if drawn by hand along it: the outline of a shape, a lettering guide, a curve from a vector tool. Each subpath becomes one recorded stroke, sampled every spacing px, with an optional pressure profile spread along each subpath (a taper such as [0.2, 1, 0.2]); closed subpaths end where they start. Brushes behave as in paint_stroke (grain, pick-up, wet and thick paint, guides off). One undo step for the whole path. Returns the layer id, changed bounds, dab count and the recorded strokeIds (for replay_strokes, update_stroke).",
|
|
11680
|
+
inputSchema: {
|
|
11681
|
+
type: "object",
|
|
11682
|
+
properties: {
|
|
11683
|
+
path: { type: "string", minLength: 1, description: 'SVG path data in document pixels, e.g. "M 40 200 C 120 40 280 40 360 200". Several subpaths paint several strokes.' },
|
|
11684
|
+
spacing: { type: "number", minimum: 0.25, maximum: 100, description: "Distance between samples along the path in document pixels (default 2); the brush spaces its own dabs, so this only sets how closely curves are followed." },
|
|
11685
|
+
pressure: { type: "array", minItems: 1, maxItems: 64, items: { type: "number", minimum: 0, maximum: 1 }, description: "Pen pressure along each subpath, spread evenly from start to end (default full pressure); [0.2, 1, 0.2] tapers both ends." },
|
|
11686
|
+
layerId: LAYER_ID4,
|
|
11687
|
+
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
11688
|
+
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
11689
|
+
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
11690
|
+
paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: "What the strokes paint with instead of the editor's paint: a colour, or gradient paint that changes colour as each stroke goes." },
|
|
11691
|
+
seed: { type: "integer", minimum: 0, description: "Jitter seed of the first subpath, the next ones take the following seeds (default new ones); the same seed and path give the same marks." }
|
|
11692
|
+
},
|
|
11693
|
+
required: ["path"],
|
|
11694
|
+
additionalProperties: false
|
|
11695
|
+
},
|
|
11696
|
+
annotations: { title: "Brush along a path", ...WRITE5 },
|
|
11697
|
+
coordinates: { out: ["bounds"] },
|
|
11698
|
+
handler: async (a) => {
|
|
11699
|
+
await c.wetIdle();
|
|
11700
|
+
const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
|
|
11701
|
+
const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
|
|
11702
|
+
if (base) {
|
|
11703
|
+
const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
|
|
11704
|
+
if (refs.length) await c.prepareStickers(refs);
|
|
11705
|
+
}
|
|
11706
|
+
const path = options.origin && typeof a.path === "string" && a.path.trim() ? bezierPathToSvg(a.path, { offset: { x: -options.origin.x, y: -options.origin.y } }) : a.path;
|
|
11707
|
+
const r = c.strokePath({ path, spacing: a.spacing, pressure: a.pressure, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed });
|
|
11708
|
+
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11709
|
+
}
|
|
11710
|
+
});
|
|
11582
11711
|
add({
|
|
11583
11712
|
name: "get_live_stroke",
|
|
11584
11713
|
title: "Get the stroke being painted",
|
|
@@ -12480,6 +12609,12 @@ function toggleDotTexture(textures, stamp) {
|
|
|
12480
12609
|
if (textures.includes(stamp)) return textures.filter((s) => s !== stamp);
|
|
12481
12610
|
return textures.length < MAX_DOT_TEXTURES ? [...textures, stamp] : [...textures];
|
|
12482
12611
|
}
|
|
12612
|
+
var PATH_STROKE_TAPERS = [
|
|
12613
|
+
{ id: "even", label: "Even" },
|
|
12614
|
+
{ id: "both", label: "Taper both ends", pressure: [0.2, 1, 0.2] },
|
|
12615
|
+
{ id: "in", label: "Fade in", pressure: [0.2, 1] },
|
|
12616
|
+
{ id: "out", label: "Fade out", pressure: [1, 0.2] }
|
|
12617
|
+
];
|
|
12483
12618
|
var STROKE_SLIDERS = [
|
|
12484
12619
|
{ path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
|
|
12485
12620
|
{ path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
|
|
@@ -13270,6 +13405,7 @@ function uiTables() {
|
|
|
13270
13405
|
blendMenu: BLEND_MENU_GROUPS,
|
|
13271
13406
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
13272
13407
|
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
13408
|
+
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
13273
13409
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
13274
13410
|
wetGpu: WET_GPU_OPTIONS,
|
|
13275
13411
|
labels: {
|
|
@@ -13364,6 +13500,7 @@ export {
|
|
|
13364
13500
|
MAX_DOT_TEXTURES,
|
|
13365
13501
|
MAX_IMAGE_SIDE,
|
|
13366
13502
|
MAX_LIVE_EFFECT_SOURCES,
|
|
13503
|
+
MAX_PATH_SAMPLES,
|
|
13367
13504
|
MAX_STICKER_STAMPS,
|
|
13368
13505
|
MAX_ZOOM,
|
|
13369
13506
|
MIN_CANVAS_SIDE,
|
|
@@ -13407,6 +13544,7 @@ export {
|
|
|
13407
13544
|
PARTICLE_KIND_LABELS,
|
|
13408
13545
|
PARTICLE_RENDERS,
|
|
13409
13546
|
PARTICLE_RENDER_LABELS,
|
|
13547
|
+
PATH_STROKE_TAPERS,
|
|
13410
13548
|
PaintCanvasPresenter,
|
|
13411
13549
|
PaintDabStroke,
|
|
13412
13550
|
PaintDirector,
|
|
@@ -13617,6 +13755,7 @@ export {
|
|
|
13617
13755
|
sampleArt,
|
|
13618
13756
|
sampleEffectAt,
|
|
13619
13757
|
sampleEffectField,
|
|
13758
|
+
samplePath,
|
|
13620
13759
|
samplePressure,
|
|
13621
13760
|
scalePaintGradient,
|
|
13622
13761
|
scaleStencil,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pen samples along vector path data, so a path can be brushed like a hand-drawn stroke
|
|
3
|
+
* (`strokePath`, the `stroke_path` tool): each subpath becomes one stroke, sampled at an even
|
|
4
|
+
* spacing, with an optional pressure profile spread along its length (a taper).
|
|
5
|
+
*/
|
|
6
|
+
import type { PenPoint } from './engine.cjs';
|
|
7
|
+
/** The most samples one subpath may have (the stroke tools' point limit). */
|
|
8
|
+
export declare const MAX_PATH_SAMPLES = 20000;
|
|
9
|
+
export interface PathSampleOptions {
|
|
10
|
+
/** Distance between samples in document pixels (default 2). */
|
|
11
|
+
spacing?: number;
|
|
12
|
+
/** Pressure along each subpath, spread evenly from its start to its end (default full pressure). */
|
|
13
|
+
pressure?: readonly number[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Samples SVG path data (document pixels) into pen points, one array per subpath. Closed
|
|
17
|
+
* subpaths end where they start. Throws a sentence when the path has nothing to brush.
|
|
18
|
+
*/
|
|
19
|
+
export declare function samplePath(d: string, options?: PathSampleOptions): PenPoint[][];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pen samples along vector path data, so a path can be brushed like a hand-drawn stroke
|
|
3
|
+
* (`strokePath`, the `stroke_path` tool): each subpath becomes one stroke, sampled at an even
|
|
4
|
+
* spacing, with an optional pressure profile spread along its length (a taper).
|
|
5
|
+
*/
|
|
6
|
+
import type { PenPoint } from './engine.js';
|
|
7
|
+
/** The most samples one subpath may have (the stroke tools' point limit). */
|
|
8
|
+
export declare const MAX_PATH_SAMPLES = 20000;
|
|
9
|
+
export interface PathSampleOptions {
|
|
10
|
+
/** Distance between samples in document pixels (default 2). */
|
|
11
|
+
spacing?: number;
|
|
12
|
+
/** Pressure along each subpath, spread evenly from its start to its end (default full pressure). */
|
|
13
|
+
pressure?: readonly number[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Samples SVG path data (document pixels) into pen points, one array per subpath. Closed
|
|
17
|
+
* subpaths end where they start. Throws a sentence when the path has nothing to brush.
|
|
18
|
+
*/
|
|
19
|
+
export declare function samplePath(d: string, options?: PathSampleOptions): PenPoint[][];
|
|
@@ -250,6 +250,12 @@ export declare function toggleStamp(stamps: readonly StampKind[], stamp: StampKi
|
|
|
250
250
|
* else adds it while there are fewer than `MAX_DOT_TEXTURES`. Empty means round dots.
|
|
251
251
|
*/
|
|
252
252
|
export declare function toggleDotTexture(textures: readonly StampKind[], stamp: StampKind): StampKind[];
|
|
253
|
+
/** Pressure profiles the "Brush a path" panel offers for `strokePath` (`pressure` undefined: even). */
|
|
254
|
+
export declare const PATH_STROKE_TAPERS: ReadonlyArray<{
|
|
255
|
+
id: string;
|
|
256
|
+
label: string;
|
|
257
|
+
pressure?: readonly number[];
|
|
258
|
+
}>;
|
|
253
259
|
/** The sticker panel's sliders for the selected sticker. */
|
|
254
260
|
/** The brush sliders of a picked stroke on a stroke layer (paths into its brush; see `updateStroke`). */
|
|
255
261
|
export declare const STROKE_SLIDERS: readonly SliderControl[];
|
|
@@ -250,6 +250,12 @@ export declare function toggleStamp(stamps: readonly StampKind[], stamp: StampKi
|
|
|
250
250
|
* else adds it while there are fewer than `MAX_DOT_TEXTURES`. Empty means round dots.
|
|
251
251
|
*/
|
|
252
252
|
export declare function toggleDotTexture(textures: readonly StampKind[], stamp: StampKind): StampKind[];
|
|
253
|
+
/** Pressure profiles the "Brush a path" panel offers for `strokePath` (`pressure` undefined: even). */
|
|
254
|
+
export declare const PATH_STROKE_TAPERS: ReadonlyArray<{
|
|
255
|
+
id: string;
|
|
256
|
+
label: string;
|
|
257
|
+
pressure?: readonly number[];
|
|
258
|
+
}>;
|
|
253
259
|
/** The sticker panel's sliders for the selected sticker. */
|
|
254
260
|
/** The brush sliders of a picked stroke on a stroke layer (paths into its brush; see `updateStroke`). */
|
|
255
261
|
export declare const STROKE_SLIDERS: readonly SliderControl[];
|
package/package.json
CHANGED
|
@@ -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
|