@pixerate/editor 0.251.0 → 0.253.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brushes/index.cjs +240 -1344
- package/dist/brushes/index.d.cts +10 -1
- package/dist/brushes/index.d.ts +10 -1
- package/dist/brushes/index.js +250 -1354
- package/dist/brushes/library.d.cts +104 -0
- package/dist/brushes/library.d.ts +104 -0
- package/dist/brushes/patch.d.cts +23 -0
- package/dist/brushes/patch.d.ts +23 -0
- package/dist/brushes/profiles.d.cts +85 -0
- package/dist/brushes/profiles.d.ts +85 -0
- package/dist/brushes/tools.d.cts +37 -0
- package/dist/brushes/tools.d.ts +37 -0
- package/dist/brushes/ui.d.cts +105 -0
- package/dist/brushes/ui.d.ts +105 -0
- package/dist/chunk-34KLWUH4.cjs +2284 -0
- package/dist/chunk-QPHNVZHO.js +2284 -0
- package/dist/paint-editor/brush-controls.d.cts +57 -0
- package/dist/paint-editor/brush-controls.d.ts +57 -0
- package/dist/paint-editor/brush-library-tools.d.cts +15 -0
- package/dist/paint-editor/brush-library-tools.d.ts +15 -0
- package/dist/paint-editor/brush-library.d.cts +29 -0
- package/dist/paint-editor/brush-library.d.ts +29 -0
- package/dist/paint-editor/controller.d.cts +76 -17
- package/dist/paint-editor/controller.d.ts +76 -17
- package/dist/paint-editor/index.cjs +537 -852
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +327 -642
- package/dist/paint-editor/serialize.d.cts +7 -4
- package/dist/paint-editor/serialize.d.ts +7 -4
- package/dist/paint-editor/sticker-tools.d.cts +1 -1
- package/dist/paint-editor/sticker-tools.d.ts +1 -1
- package/dist/paint-editor/strokes.d.cts +10 -1
- package/dist/paint-editor/strokes.d.ts +10 -1
- package/dist/paint-editor/tools.d.cts +1 -1
- package/dist/paint-editor/tools.d.ts +1 -1
- package/dist/paint-editor/ui.d.cts +3 -51
- package/dist/paint-editor/ui.d.ts +3 -51
- package/dist/paint-editor/wet-tools.d.cts +1 -1
- package/dist/paint-editor/wet-tools.d.ts +1 -1
- package/dist/tools/skill-evals.d.cts +8 -1
- package/dist/tools/skill-evals.d.ts +8 -1
- package/package.json +1 -1
- package/skills/README.md +2 -0
- package/skills/pixerate-brushes/SKILL.md +106 -0
- package/skills/pixerate-brushes-design/SKILL.md +88 -0
- package/skills/pixerate-brushes-design/evals/canvas-chalk.json +15 -0
- package/skills/pixerate-brushes-design/evals/pink-highlighter.json +15 -0
- package/skills/pixerate-brushes-design/evals/port-to-vector.json +17 -0
- package/skills/pixerate-paint-editor/SKILL.md +26 -24
- package/skills/pixerate-paint-editor/references/brush-facets.md +30 -0
- package/skills/pixerate-paint-editor/references/gradient-paint.md +1 -1
- package/skills/pixerate-paint-editor/references/light-paint.md +2 -2
- package/skills/pixerate-paint-editor/references/oils.md +1 -1
- package/skills/pixerate-paint-editor/references/particle-brushes.md +8 -5
- package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -1
- package/skills/pixerate-stickers/SKILL.md +1 -1
- package/skills/pixerate-video-editor/SKILL.md +1 -1
|
@@ -4,6 +4,24 @@ import {
|
|
|
4
4
|
describeStickerRef,
|
|
5
5
|
editorStickers
|
|
6
6
|
} from "../chunk-WZOGDKCQ.js";
|
|
7
|
+
import {
|
|
8
|
+
BRUSH_PATCH_PROPERTIES,
|
|
9
|
+
BRUSH_PRESETS,
|
|
10
|
+
BRUSH_SLIDERS,
|
|
11
|
+
BRUSH_UNSET_PROPERTY,
|
|
12
|
+
BrushLibraryError,
|
|
13
|
+
PAINT_PROFILE,
|
|
14
|
+
brushEngineControls,
|
|
15
|
+
closest,
|
|
16
|
+
createBrushLibrary,
|
|
17
|
+
createBrushLibraryToolProvider,
|
|
18
|
+
mergeBrushPatch,
|
|
19
|
+
migrateBrushV1,
|
|
20
|
+
multiColorSliders,
|
|
21
|
+
normalizeBrushSpec,
|
|
22
|
+
patchBrushValue,
|
|
23
|
+
resolveBrush
|
|
24
|
+
} from "../chunk-QPHNVZHO.js";
|
|
7
25
|
import {
|
|
8
26
|
toVideoFfmpeg
|
|
9
27
|
} from "../chunk-5E2Q7ZHI.js";
|
|
@@ -154,7 +172,6 @@ import {
|
|
|
154
172
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
155
173
|
EMPTY_MIXER_LOAD,
|
|
156
174
|
MAX_BRUSH_SIZE,
|
|
157
|
-
MIXING_MODELS,
|
|
158
175
|
PAINT_COLOR_JSON_SCHEMA,
|
|
159
176
|
PAINT_GRADIENT_FULL_SPEED,
|
|
160
177
|
PAINT_GRADIENT_JSON_SCHEMA,
|
|
@@ -3326,6 +3343,19 @@ function readLoad(raw) {
|
|
|
3326
3343
|
if (!reservoir && !pickup) return null;
|
|
3327
3344
|
return { reservoir, amount: reservoir && finite2(r.amount) ? Math.min(1, Math.max(0, r.amount)) : 0, pickup };
|
|
3328
3345
|
}
|
|
3346
|
+
function readStrokeBrush(raw) {
|
|
3347
|
+
if (raw && typeof raw === "object" && "kind" in raw && !("engine" in raw)) {
|
|
3348
|
+
try {
|
|
3349
|
+
return resolveBrush(normalizeBrushSpec(raw));
|
|
3350
|
+
} catch {
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
return normalizePaintBrush(raw ?? {});
|
|
3354
|
+
}
|
|
3355
|
+
function strokeToJSON(stroke) {
|
|
3356
|
+
const { brush, ...rest } = stroke;
|
|
3357
|
+
return { ...JSON.parse(JSON.stringify(rest)), brush: migrateBrushV1(brush) };
|
|
3358
|
+
}
|
|
3329
3359
|
function normalizeBrushStroke(raw) {
|
|
3330
3360
|
if (!raw || typeof raw !== "object") return null;
|
|
3331
3361
|
const r = raw;
|
|
@@ -3335,7 +3365,7 @@ function normalizeBrushStroke(raw) {
|
|
|
3335
3365
|
const out = {
|
|
3336
3366
|
id: r.id,
|
|
3337
3367
|
layerId: r.layerId,
|
|
3338
|
-
brush:
|
|
3368
|
+
brush: readStrokeBrush(r.brush),
|
|
3339
3369
|
color: typeof r.color === "string" && /^#[0-9a-f]{6}$/i.test(r.color) ? r.color.toLowerCase() : "#000000",
|
|
3340
3370
|
points,
|
|
3341
3371
|
seed: finite2(r.seed) ? r.seed | 0 : 0
|
|
@@ -3515,7 +3545,7 @@ function downsampleRgba(data, width, height, tw, th) {
|
|
|
3515
3545
|
|
|
3516
3546
|
// src/paint-editor/serialize.ts
|
|
3517
3547
|
var PAINT_FORMAT = "pixerate-paint";
|
|
3518
|
-
var PAINT_SCHEMA_VERSION =
|
|
3548
|
+
var PAINT_SCHEMA_VERSION = 2;
|
|
3519
3549
|
var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches", "mixingPad", "wet", "wetEvents", "paintEvents", "origin"]);
|
|
3520
3550
|
function documentExtensions(raw) {
|
|
3521
3551
|
const out = {};
|
|
@@ -3533,7 +3563,7 @@ function header(state, options) {
|
|
|
3533
3563
|
paper: { ...state.paper, dry: { ...state.paper.dry }, wet: { ...state.paper.wet } },
|
|
3534
3564
|
activeLayerId: state.activeLayerId
|
|
3535
3565
|
};
|
|
3536
|
-
if (options.strokes !== false && state.strokes.length) out.strokes =
|
|
3566
|
+
if (options.strokes !== false && state.strokes.length) out.strokes = state.strokes.map(strokeToJSON);
|
|
3537
3567
|
if (options.guides !== void 0) out.guides = JSON.parse(JSON.stringify(options.guides));
|
|
3538
3568
|
if (options.swatches !== void 0) out.swatches = JSON.parse(JSON.stringify(options.swatches));
|
|
3539
3569
|
if (options.mixingPad !== void 0) out.mixingPad = JSON.parse(JSON.stringify(options.mixingPad));
|
|
@@ -4921,10 +4951,10 @@ var MixingPad = class {
|
|
|
4921
4951
|
* mixed colour's scattering is re-estimated from its luminance, so repeated small mixes favour
|
|
4922
4952
|
* the lighter paint (see GOTCHAS).
|
|
4923
4953
|
*/
|
|
4924
|
-
mix(
|
|
4925
|
-
if (!
|
|
4926
|
-
const total =
|
|
4927
|
-
|
|
4954
|
+
mix(paints2, options = {}) {
|
|
4955
|
+
if (!paints2.length || paints2.length > 8) throw new Error(`Give 1 to 8 paints to mix; got ${paints2.length}.`);
|
|
4956
|
+
const total = paints2.reduce((n, p) => n + p.parts, 0);
|
|
4957
|
+
paints2.forEach((p, i) => {
|
|
4928
4958
|
if (!(Number.isFinite(p.parts) && p.parts >= 0)) throw new Error(`paints[${i}].parts is ${p.parts}; use a number \u2265 0.`);
|
|
4929
4959
|
});
|
|
4930
4960
|
if (!(total > 0)) throw new Error("Every paint has 0 parts; give at least one paint parts above 0.");
|
|
@@ -4939,7 +4969,7 @@ var MixingPad = class {
|
|
|
4939
4969
|
}
|
|
4940
4970
|
const bounds = { x: Math.floor(cx - R), y: Math.floor(cy - R), width: Math.ceil(2 * R), height: Math.ceil(2 * R) };
|
|
4941
4971
|
if (options.clear !== false) this.surface = writeRegion(this.surface, bounds, new Uint8ClampedArray(bounds.width * bounds.height * 4));
|
|
4942
|
-
const used =
|
|
4972
|
+
const used = paints2.filter((p) => p.parts > 0);
|
|
4943
4973
|
const area = Math.PI * (0.7 * R) ** 2;
|
|
4944
4974
|
used.forEach((p, i) => {
|
|
4945
4975
|
const r = Math.sqrt(area * (p.parts / total) / Math.PI);
|
|
@@ -4962,8 +4992,8 @@ var MixingPad = class {
|
|
|
4962
4992
|
this.run(knife, stir, "#000000");
|
|
4963
4993
|
}
|
|
4964
4994
|
const mixed = mixPigments(
|
|
4965
|
-
|
|
4966
|
-
|
|
4995
|
+
paints2.map((p) => hexToRgb8(p.color).map((v) => v / 255)),
|
|
4996
|
+
paints2.map((p) => p.parts)
|
|
4967
4997
|
);
|
|
4968
4998
|
const core = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: R * 1.1, hardness: 0.55 } }, PAD_BLOB);
|
|
4969
4999
|
const hex = "#" + mixed.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("");
|
|
@@ -5001,6 +5031,79 @@ var MixingPad = class {
|
|
|
5001
5031
|
}
|
|
5002
5032
|
};
|
|
5003
5033
|
|
|
5034
|
+
// src/paint-editor/brush-library.ts
|
|
5035
|
+
var copyJSON = (v) => JSON.parse(JSON.stringify(v));
|
|
5036
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5037
|
+
function libraryBrush(spec, builtin, resolved) {
|
|
5038
|
+
const brush = resolved ? normalizePaintBrush(resolved, resolved) : resolveBrush(spec);
|
|
5039
|
+
if (builtin) brush.builtin = true;
|
|
5040
|
+
else delete brush.builtin;
|
|
5041
|
+
return { spec, brush, builtin };
|
|
5042
|
+
}
|
|
5043
|
+
var builtins = null;
|
|
5044
|
+
function builtinLibraryBrushes() {
|
|
5045
|
+
return builtins ??= BRUSH_PRESETS.map((spec) => {
|
|
5046
|
+
const resolved = BUILTIN_BRUSHES.find((b) => b.id === spec.id);
|
|
5047
|
+
return libraryBrush(spec, true, resolved);
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
5050
|
+
function seededStorage(brushes) {
|
|
5051
|
+
if (!brushes.length) return void 0;
|
|
5052
|
+
let stored = JSON.stringify({ format: "pixerate-brushes", version: 1, brushes: brushes.map(brushToJSON) });
|
|
5053
|
+
return {
|
|
5054
|
+
get: async () => stored,
|
|
5055
|
+
set: async (_key, value) => {
|
|
5056
|
+
stored = value;
|
|
5057
|
+
}
|
|
5058
|
+
};
|
|
5059
|
+
}
|
|
5060
|
+
var paints = (b) => {
|
|
5061
|
+
const { category: _c, builtin: _b, ...rest } = b;
|
|
5062
|
+
return JSON.stringify(rest);
|
|
5063
|
+
};
|
|
5064
|
+
function brushSpecOf(next, from) {
|
|
5065
|
+
const plain = migrateBrushV1(next);
|
|
5066
|
+
const out = copyJSON(plain);
|
|
5067
|
+
if (from.description) out.description = from.description;
|
|
5068
|
+
if (from.tags?.length) out.tags = [...from.tags];
|
|
5069
|
+
if (out.kind === "mark" && from.kind === "mark") {
|
|
5070
|
+
if (from.medium) out.medium = from.medium;
|
|
5071
|
+
if (from.variety) out.variety = from.variety;
|
|
5072
|
+
}
|
|
5073
|
+
const colour = { ...from.colour ?? {} };
|
|
5074
|
+
if (plain.colour?.default !== void 0) colour.default = plain.colour.default;
|
|
5075
|
+
else if (isObject(colour.default) && colour.default.type === "gradient") delete colour.default;
|
|
5076
|
+
if (Object.keys(colour).length) out.colour = copyJSON(colour);
|
|
5077
|
+
else delete out.colour;
|
|
5078
|
+
if ((out.light?.amount ?? 0) > 0 && (from.light?.spill || from.light?.glow !== void 0)) {
|
|
5079
|
+
out.light = { ...out.light, ...from.light.spill ? { spill: { ...from.light.spill } } : {}, ...from.light.glow !== void 0 ? { glow: from.light.glow } : {} };
|
|
5080
|
+
}
|
|
5081
|
+
if (from.motion?.live !== void 0 || from.motion?.fps !== void 0) {
|
|
5082
|
+
out.motion = { ...out.motion ?? {}, ...from.motion.live !== void 0 ? { live: from.motion.live } : {}, ...from.motion.fps !== void 0 ? { fps: from.motion.fps } : {} };
|
|
5083
|
+
}
|
|
5084
|
+
try {
|
|
5085
|
+
const hinted = normalizeBrushSpec(out, []);
|
|
5086
|
+
if (paints(resolveBrush(hinted)) === paints(resolveBrush(plain))) return hinted;
|
|
5087
|
+
} catch {
|
|
5088
|
+
}
|
|
5089
|
+
return plain;
|
|
5090
|
+
}
|
|
5091
|
+
function engineChanges(base, next) {
|
|
5092
|
+
const diff = (a, b) => {
|
|
5093
|
+
if (isObject(a) && isObject(b)) {
|
|
5094
|
+
const out = {};
|
|
5095
|
+
for (const k of Object.keys(b)) {
|
|
5096
|
+
const d = diff(a[k], b[k]);
|
|
5097
|
+
if (d !== void 0) out[k] = d;
|
|
5098
|
+
}
|
|
5099
|
+
return Object.keys(out).length ? out : void 0;
|
|
5100
|
+
}
|
|
5101
|
+
return JSON.stringify(a) === JSON.stringify(b) ? void 0 : copyJSON(b);
|
|
5102
|
+
};
|
|
5103
|
+
const { id: _i, builtin: _b, ...rest } = diff(base, next) ?? {};
|
|
5104
|
+
return rest;
|
|
5105
|
+
}
|
|
5106
|
+
|
|
5004
5107
|
// src/paint-editor/live-strokes.ts
|
|
5005
5108
|
var LIVE_STROKE_ENGINES = ["dab", "particle", "bristle", "mixer"];
|
|
5006
5109
|
function liveStrokeRefusal(brush) {
|
|
@@ -5583,6 +5686,13 @@ var PaintEditorError = class extends Error {
|
|
|
5583
5686
|
this.name = "PaintEditorError";
|
|
5584
5687
|
}
|
|
5585
5688
|
};
|
|
5689
|
+
async function libraryCall(call) {
|
|
5690
|
+
try {
|
|
5691
|
+
return await call();
|
|
5692
|
+
} catch (e) {
|
|
5693
|
+
throw e instanceof BrushLibraryError ? new PaintEditorError(e.message) : e;
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5586
5696
|
var HEX3 = /^#[0-9a-f]{6}$/i;
|
|
5587
5697
|
var MAX_ADD_STICKERS = 500;
|
|
5588
5698
|
function parseColor(value, what = "color") {
|
|
@@ -5610,8 +5720,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5610
5720
|
undoStack = [];
|
|
5611
5721
|
redoStack = [];
|
|
5612
5722
|
historyLimit;
|
|
5723
|
+
/** The brush library this editor lists, saves into and paints from (shared with other editors when the host passes one). */
|
|
5724
|
+
brushLibrary;
|
|
5613
5725
|
brush;
|
|
5614
|
-
|
|
5726
|
+
/** The active brush as facets (what `getBrushSpec` and saving use). */
|
|
5727
|
+
brushSpec;
|
|
5728
|
+
/** The library's brushes as last read, each as stored (facets) and as the engine paints it. */
|
|
5729
|
+
brushEntries;
|
|
5730
|
+
/** The brush id asked for at construction, while the library had not listed it yet. */
|
|
5731
|
+
pendingBrushId = null;
|
|
5732
|
+
brushesLoaded = Promise.resolve();
|
|
5733
|
+
unsubscribeLibrary;
|
|
5615
5734
|
/** The library list in the state (the same array until the user brushes change, so thumbnails are not redrawn). */
|
|
5616
5735
|
library = null;
|
|
5617
5736
|
color;
|
|
@@ -5693,9 +5812,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5693
5812
|
this.rasterizeSticker = options.rasterizeSticker ?? defaultStickerRasterizer;
|
|
5694
5813
|
if (options.stickers) this.stickers.set(options.stickers);
|
|
5695
5814
|
this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
this.
|
|
5815
|
+
const seed = (options.brushes ?? []).map((b) => normalizePaintBrush({ ...b, builtin: false }));
|
|
5816
|
+
this.brushLibrary = options.brushLibrary ?? createBrushLibrary({ storage: seededStorage(seed) });
|
|
5817
|
+
this.brushEntries = [...builtinLibraryBrushes(), ...seed.map((b) => libraryBrush(migrateBrushV1(b), false, b))];
|
|
5818
|
+
const wanted = options.brushId ?? DEFAULT_BRUSH_ID;
|
|
5819
|
+
const start = this.findEntry(wanted) ?? this.findEntry(DEFAULT_BRUSH_ID);
|
|
5820
|
+
this.brush = normalizePaintBrush(start.brush);
|
|
5821
|
+
this.brushSpec = copyJSON(start.spec);
|
|
5822
|
+
this.unsubscribeLibrary = this.brushLibrary.subscribe(() => void this.refreshBrushes().catch(() => void 0));
|
|
5823
|
+
if (options.brushLibrary && seed.length) void this.brushLibrary.importFile(seed.map(brushToJSON)).catch(() => void 0);
|
|
5824
|
+
void this.refreshBrushes().catch(() => void 0);
|
|
5825
|
+
this.pendingBrushId = start.brush.id === wanted ? null : wanted;
|
|
5699
5826
|
this.color = options.color ? parseColor(options.color) : "#1f2937";
|
|
5700
5827
|
this.hand = options.hand === "left" ? "left" : "right";
|
|
5701
5828
|
this.encodeImage = options.encodeImage;
|
|
@@ -5748,7 +5875,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5748
5875
|
this.cached = {
|
|
5749
5876
|
document: this.doc,
|
|
5750
5877
|
brush: this.brush,
|
|
5751
|
-
brushes: this.library ??=
|
|
5878
|
+
brushes: this.library ??= this.brushEntries.map((e) => e.brush),
|
|
5752
5879
|
color: this.color,
|
|
5753
5880
|
secondaryColor: this.secondaryColor,
|
|
5754
5881
|
paint: this.gradient ?? { type: "color", color: this.color },
|
|
@@ -6730,9 +6857,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6730
6857
|
* Renders effect paints that are not rendered yet (strokes and replays need them rendered
|
|
6731
6858
|
* first, since strokes are synchronous). Throws naming the effect and provider when one cannot be.
|
|
6732
6859
|
*/
|
|
6733
|
-
async prepareEffectPaints(
|
|
6860
|
+
async prepareEffectPaints(paints2) {
|
|
6734
6861
|
const seen = /* @__PURE__ */ new Set();
|
|
6735
|
-
for (const p of
|
|
6862
|
+
for (const p of paints2) {
|
|
6736
6863
|
const key = effectPaintKey(p);
|
|
6737
6864
|
if (seen.has(key)) continue;
|
|
6738
6865
|
seen.add(key);
|
|
@@ -6742,8 +6869,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6742
6869
|
/** Renders the effect paints of recorded strokes (default all of them) before a replay. */
|
|
6743
6870
|
async prepareStrokeEffects(strokeIds) {
|
|
6744
6871
|
const ids = strokeIds ? new Set(strokeIds) : null;
|
|
6745
|
-
const
|
|
6746
|
-
await this.prepareEffectPaints(
|
|
6872
|
+
const paints2 = this.doc.strokes.filter((s) => (!ids || ids.has(s.id)) && s.paint?.type === "effect").map((s) => s.paint);
|
|
6873
|
+
await this.prepareEffectPaints(paints2);
|
|
6747
6874
|
}
|
|
6748
6875
|
/** A small preview of the effect paint (a PNG data URL `size` px wide), or `null` without a rendered one. */
|
|
6749
6876
|
effectPaintThumbnail(size = 48) {
|
|
@@ -6824,100 +6951,162 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6824
6951
|
return { layerId: now.id, source: this.sourceOf(d, values, options.time), amount };
|
|
6825
6952
|
}
|
|
6826
6953
|
// --- Brushes -------------------------------------------------------------------
|
|
6954
|
+
findEntry(id) {
|
|
6955
|
+
return this.brushEntries.find((e) => e.brush.id === id);
|
|
6956
|
+
}
|
|
6827
6957
|
findBrush(id) {
|
|
6828
|
-
return
|
|
6958
|
+
return this.findEntry(id)?.brush;
|
|
6959
|
+
}
|
|
6960
|
+
requireEntry(id) {
|
|
6961
|
+
const e = this.findEntry(id);
|
|
6962
|
+
if (!e) throw new PaintEditorError(`Unknown brush "${id}". ${closest(id, this.brushEntries.map((x) => x.brush.id))}Call list_brush_presets for the ids.`);
|
|
6963
|
+
return e;
|
|
6964
|
+
}
|
|
6965
|
+
requireBrush(id) {
|
|
6966
|
+
return this.requireEntry(id).brush;
|
|
6829
6967
|
}
|
|
6830
|
-
/**
|
|
6968
|
+
/** Reads the library again (after a change anywhere it is shared). */
|
|
6969
|
+
refreshBrushes() {
|
|
6970
|
+
const run = this.brushLibrary.list().then((entries) => {
|
|
6971
|
+
this.brushEntries = entries.map((e) => libraryBrush(e.brush, e.builtin));
|
|
6972
|
+
this.library = null;
|
|
6973
|
+
const pending = this.pendingBrushId;
|
|
6974
|
+
this.pendingBrushId = null;
|
|
6975
|
+
if (pending && this.findEntry(pending)) this.setBrush({ id: pending });
|
|
6976
|
+
else this.emit([]);
|
|
6977
|
+
});
|
|
6978
|
+
this.brushesLoaded = run.catch(() => void 0);
|
|
6979
|
+
return run;
|
|
6980
|
+
}
|
|
6981
|
+
/** Resolves once the brush library has been read (user brushes kept in storage are listed from then on). */
|
|
6982
|
+
whenBrushesLoaded() {
|
|
6983
|
+
return this.brushesLoaded;
|
|
6984
|
+
}
|
|
6985
|
+
/** A library brush by id, as the engine paints it (`undefined` when unknown). */
|
|
6831
6986
|
getBrush(id) {
|
|
6832
6987
|
const b = this.findBrush(id);
|
|
6833
6988
|
return b ? normalizePaintBrush(b, b) : void 0;
|
|
6834
6989
|
}
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
return
|
|
6990
|
+
/** A library brush by id as stored (its facets), or without an id the active brush as edited. */
|
|
6991
|
+
getBrushSpec(id) {
|
|
6992
|
+
const spec = id === void 0 ? this.brushSpec : this.findEntry(id)?.spec;
|
|
6993
|
+
return spec ? copyJSON(spec) : void 0;
|
|
6839
6994
|
}
|
|
6840
6995
|
/**
|
|
6841
|
-
* Chooses the active brush (`id`: a library brush) and / or changes its settings. Editor
|
|
6842
|
-
* not an undo step. Returns the active brush.
|
|
6996
|
+
* Chooses the active brush (`id`: a library brush) and / or changes its engine settings. Editor
|
|
6997
|
+
* state, not an undo step. Returns the active brush. `setBrushFacets` changes it by facets.
|
|
6843
6998
|
*/
|
|
6844
6999
|
setBrush(update = {}) {
|
|
6845
7000
|
const { id, ...changes } = update;
|
|
6846
|
-
const
|
|
7001
|
+
const entry = id !== void 0 ? this.requireEntry(id) : null;
|
|
7002
|
+
const base = entry ? entry.brush : this.brush;
|
|
7003
|
+
const baseSpec = entry ? entry.spec : this.brushSpec;
|
|
6847
7004
|
if (changes.paint !== void 0 && changes.paint !== null) this.readStrokePaint(changes.paint);
|
|
7005
|
+
const unchanged = !Object.keys(changes).length;
|
|
6848
7006
|
const next = normalizePaintBrush({ ...changes, id: base.id }, base);
|
|
6849
|
-
|
|
7007
|
+
this.useBrush(next, unchanged ? baseSpec : brushSpecOf(next, baseSpec), unchanged && base.builtin === true);
|
|
7008
|
+
return this.brush;
|
|
7009
|
+
}
|
|
7010
|
+
/**
|
|
7011
|
+
* Chooses the active brush (`id`: a library brush) and / or lays a facet patch over it (objects
|
|
7012
|
+
* merge, lists and values replace, as the library's `update_brush` does). Editor state, not an
|
|
7013
|
+
* undo step. `unset` removes whole facets first. Returns the active brush as the engine paints
|
|
7014
|
+
* it, its facets, and what was clamped or dropped.
|
|
7015
|
+
*/
|
|
7016
|
+
setBrushFacets(patch = {}, id, unset = []) {
|
|
7017
|
+
const entry = id !== void 0 ? this.requireEntry(id) : null;
|
|
7018
|
+
const unchanged = !Object.keys(patch).length && !unset.length;
|
|
7019
|
+
const problems = [];
|
|
7020
|
+
const base = copyJSON(entry ? entry.spec : this.brushSpec);
|
|
7021
|
+
for (const k of unset) delete base[k];
|
|
7022
|
+
const spec = this.readBrushPatch(base, patch, problems);
|
|
7023
|
+
this.useBrush(unchanged && entry ? entry.brush : resolveBrush(spec), spec, unchanged && entry?.builtin === true);
|
|
7024
|
+
return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
|
|
7025
|
+
}
|
|
7026
|
+
/**
|
|
7027
|
+
* Makes `spec` (a whole facet brush, as the brush editor edits it) the active brush, keeping the
|
|
7028
|
+
* active brush's id. Editor state, not an undo step. Returns it as the engine paints it, its
|
|
7029
|
+
* facets, and what was clamped or dropped.
|
|
7030
|
+
*/
|
|
7031
|
+
setBrushSpec(spec) {
|
|
7032
|
+
const problems = [];
|
|
7033
|
+
const paint = spec.colour?.default;
|
|
7034
|
+
if (paint && typeof paint === "object" && "type" in paint && paint.type === "gradient") this.readStrokePaint(paint);
|
|
7035
|
+
let next;
|
|
7036
|
+
try {
|
|
7037
|
+
next = normalizeBrushSpec({ ...spec, id: this.brush.id }, problems);
|
|
7038
|
+
} catch (e) {
|
|
7039
|
+
throw new PaintEditorError(e.message);
|
|
7040
|
+
}
|
|
7041
|
+
this.useBrush(resolveBrush(next), next, false);
|
|
7042
|
+
return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
|
|
7043
|
+
}
|
|
7044
|
+
/**
|
|
7045
|
+
* The engine settings a facet patch changes on a brush (`from`: a library brush id or a brush as
|
|
7046
|
+
* painted; default the active brush), as `paintStroke`'s and `updateStroke`'s `brush` take them.
|
|
7047
|
+
*/
|
|
7048
|
+
brushChanges(patch, from) {
|
|
7049
|
+
const entry = typeof from === "string" ? this.requireEntry(from) : null;
|
|
7050
|
+
const base = entry ? entry.brush : typeof from === "object" ? normalizePaintBrush(from, from) : this.brush;
|
|
7051
|
+
const spec = entry ? entry.spec : typeof from === "object" ? migrateBrushV1(base) : this.brushSpec;
|
|
7052
|
+
return engineChanges(base, resolveBrush(this.readBrushPatch(spec, patch, [])));
|
|
7053
|
+
}
|
|
7054
|
+
readBrushPatch(base, patch, problems) {
|
|
7055
|
+
const { id: _id, name, ...rest } = patch;
|
|
7056
|
+
const paint = rest.colour?.default;
|
|
7057
|
+
if (paint && typeof paint === "object" && "type" in paint && paint.type === "gradient") this.readStrokePaint(paint);
|
|
7058
|
+
try {
|
|
7059
|
+
return normalizeBrushSpec(mergeBrushPatch(base, { ...rest, ...name !== void 0 ? { name } : {} }), problems);
|
|
7060
|
+
} catch (e) {
|
|
7061
|
+
throw new PaintEditorError(e.message);
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
useBrush(next, spec, builtin) {
|
|
7065
|
+
next = normalizePaintBrush(next, next);
|
|
7066
|
+
if (builtin) next.builtin = true;
|
|
6850
7067
|
else delete next.builtin;
|
|
6851
7068
|
this.brush = next;
|
|
7069
|
+
this.brushSpec = { ...copyJSON(spec), id: next.id, name: next.name };
|
|
7070
|
+
this.pendingBrushId = null;
|
|
6852
7071
|
this.emit([]);
|
|
6853
7072
|
if (next.engine === "sticker" && next.sticker.stamps.some((r) => !editorStampOf(r))) void this.prepareStickers(next.sticker.stamps).catch(() => this.emit([]));
|
|
6854
|
-
return this.brush;
|
|
6855
7073
|
}
|
|
6856
|
-
/**
|
|
6857
|
-
|
|
7074
|
+
/**
|
|
7075
|
+
* Saves the active brush (or `brush`, engine settings on top of it) into the brush library under
|
|
7076
|
+
* `id` / `name`, replacing a user brush with that id. Built-in ids are refused. Resolves with the
|
|
7077
|
+
* saved brush.
|
|
7078
|
+
*/
|
|
7079
|
+
async saveBrush(options) {
|
|
6858
7080
|
if (!BRUSH_ID.test(options.id)) throw new PaintEditorError(`Brush id "${options.id}" is invalid; use lowercase letters, digits and dashes (e.g. "my-pencil").`);
|
|
6859
|
-
if (
|
|
6860
|
-
const
|
|
6861
|
-
const
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
this.
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
const i = this.userBrushes.findIndex((b) => b.id === id);
|
|
6873
|
-
if (i < 0) throw new PaintEditorError(`Unknown brush "${id}". Call list_brushes for the ids.`);
|
|
6874
|
-
this.userBrushes.splice(i, 1);
|
|
6875
|
-
this.library = null;
|
|
6876
|
-
this.emit([]);
|
|
6877
|
-
}
|
|
6878
|
-
/** The user's saved brushes. */
|
|
7081
|
+
if (this.brushLibrary.isBuiltin(options.id)) throw new PaintEditorError(`"${options.id}" is a built-in brush; save under another id.`);
|
|
7082
|
+
const spec = options.brush ? brushSpecOf(normalizePaintBrush(options.brush, this.brush), this.brushSpec) : this.brushSpec;
|
|
7083
|
+
const { brush } = await libraryCall(() => this.brushLibrary.save({ ...spec, id: options.id, name: options.name ?? spec.name }, { overwrite: true }));
|
|
7084
|
+
await this.refreshBrushes();
|
|
7085
|
+
return this.requireBrush(brush.id);
|
|
7086
|
+
}
|
|
7087
|
+
/** Removes a user brush from the brush library (built-in brushes can't be removed). */
|
|
7088
|
+
async removeBrush(id) {
|
|
7089
|
+
if (this.brushLibrary.isBuiltin(id)) throw new PaintEditorError(`"${id}" is a built-in brush and cannot be removed.`);
|
|
7090
|
+
if (!await libraryCall(() => this.brushLibrary.remove(id))) throw new PaintEditorError(`Unknown brush "${id}". Call list_brush_presets for the ids.`);
|
|
7091
|
+
await this.refreshBrushes();
|
|
7092
|
+
}
|
|
7093
|
+
/** The user's saved brushes, as the engine paints them. */
|
|
6879
7094
|
getUserBrushes() {
|
|
6880
|
-
return this.
|
|
7095
|
+
return this.brushEntries.filter((e) => !e.builtin).map((e) => normalizePaintBrush(e.brush, e.brush));
|
|
6881
7096
|
}
|
|
6882
|
-
/** A brush library file of the given brushes (default: the user's brushes). */
|
|
7097
|
+
/** A brush library file (version 2) of the given brushes (default: the user's brushes). */
|
|
6883
7098
|
exportBrushes(ids) {
|
|
6884
|
-
|
|
6885
|
-
return { format: "pixerate-brushes", version: 1, brushes: list.map(brushToJSON) };
|
|
7099
|
+
return libraryCall(() => this.brushLibrary.exportFile(ids));
|
|
6886
7100
|
}
|
|
6887
7101
|
/**
|
|
6888
|
-
* Adds the brushes of a library file to the
|
|
6889
|
-
* unless `replace` (built-in ids are always skipped).
|
|
7102
|
+
* Adds the brushes of a library file (version 1 or 2) to the brush library. A brush whose id is
|
|
7103
|
+
* taken is skipped unless `replace` (built-in ids are always skipped). Resolves with the ids
|
|
7104
|
+
* added or replaced, the ids skipped and the problems.
|
|
6890
7105
|
*/
|
|
6891
|
-
importBrushes(file, options = {}) {
|
|
6892
|
-
const
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
list = parseBrushLibrary(file, problems);
|
|
6896
|
-
} catch (e) {
|
|
6897
|
-
throw new PaintEditorError(e.message);
|
|
6898
|
-
}
|
|
6899
|
-
const added = [];
|
|
6900
|
-
const skipped = [];
|
|
6901
|
-
for (const b of list) {
|
|
6902
|
-
if (BUILTIN_BRUSHES.some((x) => x.id === b.id)) {
|
|
6903
|
-
skipped.push(b.id);
|
|
6904
|
-
problems.push(`"${b.id}" is a built-in brush id; rename it to import it.`);
|
|
6905
|
-
continue;
|
|
6906
|
-
}
|
|
6907
|
-
const i = this.userBrushes.findIndex((x) => x.id === b.id);
|
|
6908
|
-
if (i >= 0 && !options.replace) {
|
|
6909
|
-
skipped.push(b.id);
|
|
6910
|
-
continue;
|
|
6911
|
-
}
|
|
6912
|
-
if (i >= 0) this.userBrushes[i] = b;
|
|
6913
|
-
else this.userBrushes.push(b);
|
|
6914
|
-
added.push(b.id);
|
|
6915
|
-
}
|
|
6916
|
-
if (added.length) {
|
|
6917
|
-
this.library = null;
|
|
6918
|
-
this.emit([]);
|
|
6919
|
-
}
|
|
6920
|
-
return { added, skipped, problems };
|
|
7106
|
+
async importBrushes(file, options = {}) {
|
|
7107
|
+
const r = await libraryCall(() => this.brushLibrary.importFile(file, { overwrite: !!options.replace }));
|
|
7108
|
+
await this.refreshBrushes();
|
|
7109
|
+
return { added: [...r.added, ...r.replaced], skipped: r.skipped, problems: r.problems };
|
|
6921
7110
|
}
|
|
6922
7111
|
/** A thumbnail of a brush: a test stroke on the current paper. */
|
|
6923
7112
|
brushThumbnail(id, options = {}) {
|
|
@@ -7076,7 +7265,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7076
7265
|
const brush = this.strokeBrush(init);
|
|
7077
7266
|
const effect = this.strokeEffect(init, brush);
|
|
7078
7267
|
if (brush.light.amount > 0) {
|
|
7079
|
-
if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (
|
|
7268
|
+
if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (glow-gel, neon-marker; list_brush_presets lists them), or set light.amount to 0 (set_brush).`);
|
|
7080
7269
|
}
|
|
7081
7270
|
if (doc.liveStrokes?.[layer.id]) {
|
|
7082
7271
|
const why = liveStrokeRefusal(brush);
|
|
@@ -7507,7 +7696,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7507
7696
|
const w = Math.round(width * scale);
|
|
7508
7697
|
const h = Math.round(height * scale);
|
|
7509
7698
|
if (w > 16384 || h > 16384) throw new PaintEditorError(`At scale ${scale} the canvas would be ${w}\xD7${h}; the limit is 16384 px per side. Use a smaller scale.`);
|
|
7510
|
-
const target = new _PaintEditorController({ width: w, height: h, dpi: dpi * scale,
|
|
7699
|
+
const target = new _PaintEditorController({ width: w, height: h, dpi: dpi * scale, brushLibrary: this.brushLibrary, wet: this.wetOptions });
|
|
7700
|
+
target.brushEntries = this.brushEntries;
|
|
7511
7701
|
const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };
|
|
7512
7702
|
target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });
|
|
7513
7703
|
target.lighting = { ...this.lighting };
|
|
@@ -7562,6 +7752,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7562
7752
|
}
|
|
7563
7753
|
/** Stops the wet simulation's worker threads (call when the editor goes away). */
|
|
7564
7754
|
dispose() {
|
|
7755
|
+
this.unsubscribeLibrary();
|
|
7565
7756
|
this.wet.destroy();
|
|
7566
7757
|
this.effectsRenderer.dispose();
|
|
7567
7758
|
this.effectPaintPixels.clear();
|
|
@@ -7942,7 +8133,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7942
8133
|
if (Object.keys(stickers).length) ext.stickers = stickers;
|
|
7943
8134
|
else delete ext.stickers;
|
|
7944
8135
|
const liveStrokes = {};
|
|
7945
|
-
for (const [id, list] of Object.entries(this.doc.liveStrokes ?? {})) if (findLayer(this.doc.layers, id)) liveStrokes[id] = list.map(
|
|
8136
|
+
for (const [id, list] of Object.entries(this.doc.liveStrokes ?? {})) if (findLayer(this.doc.layers, id)) liveStrokes[id] = list.map(strokeToJSON);
|
|
7946
8137
|
if (Object.keys(liveStrokes).length) ext.liveStrokes = liveStrokes;
|
|
7947
8138
|
else delete ext.liveStrokes;
|
|
7948
8139
|
const light = {};
|
|
@@ -8415,7 +8606,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8415
8606
|
stickerLayer(layerId) {
|
|
8416
8607
|
const l = this.wetLayer(layerId, "edit the stickers of");
|
|
8417
8608
|
const stickers = this.doc.stickers?.[l.id];
|
|
8418
|
-
if (!stickers) throw new PaintEditorError(`Layer "${l.name}" has no stickers; spray some with a sticker brush (
|
|
8609
|
+
if (!stickers) throw new PaintEditorError(`Layer "${l.name}" has no stickers; spray some with a sticker brush (sticker-stars; list_brush_presets lists them).`);
|
|
8419
8610
|
return { layer: l, stickers };
|
|
8420
8611
|
}
|
|
8421
8612
|
// --- The sticker library ---------------------------------------------------------
|
|
@@ -9145,8 +9336,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9145
9336
|
* and a fully mixed core of the Kubelka-Munk mix (see `MixingPad.mix`). With `setColor` the
|
|
9146
9337
|
* mix becomes the paint colour. Returns the mixed colour and the pile's bounds.
|
|
9147
9338
|
*/
|
|
9148
|
-
mixOnPad(
|
|
9149
|
-
const parsed = (
|
|
9339
|
+
mixOnPad(paints2, options = {}) {
|
|
9340
|
+
const parsed = (paints2 ?? []).map((p, i) => ({ color: parseColor(p?.color, `paints[${i}].color`), parts: p?.parts }));
|
|
9150
9341
|
let r;
|
|
9151
9342
|
try {
|
|
9152
9343
|
r = this.pad.mix(parsed, options);
|
|
@@ -10140,7 +10331,7 @@ function createPaintGuideTools(c) {
|
|
|
10140
10331
|
additionalProperties: false
|
|
10141
10332
|
},
|
|
10142
10333
|
annotations: { title: "Mix paints on the pad", ...WRITE2 },
|
|
10143
|
-
handler: ({ paints, x, y, radius, passes, clear, setColor }) => c.mixOnPad(
|
|
10334
|
+
handler: ({ paints: paints2, x, y, radius, passes, clear, setColor }) => c.mixOnPad(paints2, { x, y, radius, passes, clear, setColor })
|
|
10144
10335
|
},
|
|
10145
10336
|
{
|
|
10146
10337
|
name: "save_mixing_pad",
|
|
@@ -10571,254 +10762,6 @@ var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint:
|
|
|
10571
10762
|
var EMPTY3 = { type: "object", properties: {}, additionalProperties: false };
|
|
10572
10763
|
var COLOR3 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
|
|
10573
10764
|
var LAYER_ID4 = { type: "string", description: "Layer id from list_layers (default the active layer)." };
|
|
10574
|
-
var TIP = {
|
|
10575
|
-
type: "object",
|
|
10576
|
-
description: "The tip (the shared brush engine). Missing fields keep their current values.",
|
|
10577
|
-
properties: {
|
|
10578
|
-
size: { type: "number", minimum: 1, maximum: MAX_BRUSH_SIZE, description: "Diameter in pixels." },
|
|
10579
|
-
hardness: { type: "number", minimum: 0, maximum: 1, description: "Edge hardness, 0 (soft) .. 1 (hard)." },
|
|
10580
|
-
spacing: { type: "number", minimum: 0.01, maximum: 10, description: "Dab spacing as a fraction of the diameter (0.05 is smooth, 1 is dotted)." },
|
|
10581
|
-
angle: { type: "number", minimum: -360, maximum: 360, description: "Tip angle in degrees, counter-clockwise; with rotation stroke or barrel it is added to the stroke direction or pen rotation." },
|
|
10582
|
-
roundness: { type: "number", minimum: 0.01, maximum: 1, description: "Tip roundness: 1 round, lower is a flat ellipse (a pastel or chisel tip)." },
|
|
10583
|
-
opacity: { type: "number", minimum: 0, maximum: 1, description: "Stroke opacity ceiling: overlapping dabs never exceed it." },
|
|
10584
|
-
flow: { type: "number", minimum: 0, maximum: 1, description: "Build-up per dab towards the opacity (low flow builds up softly, like an airbrush)." },
|
|
10585
|
-
smoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Position smoothing of the input, 0..0.95." },
|
|
10586
|
-
pressureSize: { type: "boolean", description: "Pressure scales the size (down to dynamics.minSize)." },
|
|
10587
|
-
pressureOpacity: { type: "boolean", description: "Pressure scales the opacity." },
|
|
10588
|
-
blend: {
|
|
10589
|
-
type: "string",
|
|
10590
|
-
enum: BLEND_MODES.map((m) => m.id),
|
|
10591
|
-
description: "How paint combines with the layer. pigment mixes it in like real paint (blue into yellow gives green); multiply darkens like marker ink; watercolor glazes like a transparent wash; tint, highlight and shadow recolour, light and shade (see list_blend_modes)."
|
|
10592
|
-
}
|
|
10593
|
-
},
|
|
10594
|
-
additionalProperties: false
|
|
10595
|
-
};
|
|
10596
|
-
var DYNAMICS = {
|
|
10597
|
-
type: "object",
|
|
10598
|
-
description: "Pen dynamics and jitter. Missing fields keep their current values.",
|
|
10599
|
-
properties: {
|
|
10600
|
-
minSize: { type: "number", minimum: 0, maximum: 1, description: "Size at zero pressure as a fraction of the full size (when pressureSize)." },
|
|
10601
|
-
pressureFlow: { type: "number", minimum: 0, maximum: 1, description: "How much pressure scales flow." },
|
|
10602
|
-
tiltSize: { type: "number", minimum: 0, maximum: 1, description: "Laying the pen down widens the tip (up to 2.5\xD7)." },
|
|
10603
|
-
tiltRoundness: { type: "number", minimum: 0, maximum: 1, description: "Laying the pen down flattens the tip along the tilt (side-of-the-pencil shading)." },
|
|
10604
|
-
tiltOpacity: { type: "number", minimum: 0, maximum: 1, description: "Laying the pen down lightens the mark." },
|
|
10605
|
-
velocitySize: { type: "number", minimum: -1, maximum: 1, description: "Positive grows the tip on fast strokes, negative thins it (needs point times t)." },
|
|
10606
|
-
velocityOpacity: { type: "number", minimum: -1, maximum: 1, description: "Positive darkens fast marks, negative lightens them (needs point times t)." },
|
|
10607
|
-
sizeJitter: { type: "number", minimum: 0, maximum: 1, description: "Random size change per dab." },
|
|
10608
|
-
angleJitter: { type: "number", minimum: 0, maximum: 1, description: "Random angle per dab, as a fraction of \xB1180\xB0." },
|
|
10609
|
-
opacityJitter: { type: "number", minimum: 0, maximum: 1, description: "Random opacity drop per dab." },
|
|
10610
|
-
scatter: { type: "number", minimum: 0, maximum: 4, description: "Random offset across the stroke, in diameters." }
|
|
10611
|
-
},
|
|
10612
|
-
additionalProperties: false
|
|
10613
|
-
};
|
|
10614
|
-
var MIXER = {
|
|
10615
|
-
type: "object",
|
|
10616
|
-
description: "Mixer engine settings (the Mixer Brush). Missing fields keep their current values.",
|
|
10617
|
-
properties: {
|
|
10618
|
-
wet: { type: "number", minimum: 0, maximum: 1, description: "Pick-up ratio: how much canvas colour each dab picks up, 0 (dry) .. 1." },
|
|
10619
|
-
load: { type: "number", minimum: 0, maximum: 1, description: "How much paint the reservoir holds; below 1 it drains per dab, so long strokes run dry." },
|
|
10620
|
-
mix: { type: "number", minimum: 0, maximum: 1, description: "Mix ratio: how far each dab's paint moves from the reservoir towards the picked-up colour." },
|
|
10621
|
-
loadEachStroke: { type: "boolean", description: "Refill the reservoir with the colour before each stroke." },
|
|
10622
|
-
cleanEachStroke: { type: "boolean", description: "Empty the pickup well after each stroke." },
|
|
10623
|
-
mixing: { type: "string", enum: [...MIXING_MODELS], description: "rgb: RGB averages. pigment: mixes like paint (Kubelka-Munk)." }
|
|
10624
|
-
},
|
|
10625
|
-
additionalProperties: false
|
|
10626
|
-
};
|
|
10627
|
-
var MIXING = { type: "string", enum: [...MIXING_MODELS], description: "pigment: mixes like paint (Kubelka-Munk, blue and yellow make green); rgb: straight RGB averages." };
|
|
10628
|
-
var BEHAVIOR = {
|
|
10629
|
-
type: "object",
|
|
10630
|
-
description: "How the brush handles paint on the canvas (dab and mixer engines; on a mixer brush pickup, mix, dirty and mixing are its mixer wet, mix, !cleanEachStroke and mixing). Missing fields keep their current values.",
|
|
10631
|
-
properties: {
|
|
10632
|
-
pickup: { type: "number", minimum: 0, maximum: 1, description: "Pick-up ratio 0..1: how much of the paint under each dab the brush lifts and carries along (0: none, a dry brush)." },
|
|
10633
|
-
mix: { type: "number", minimum: 0, maximum: 1, description: "Mix ratio 0..1: how much of the carried paint goes into what the brush lays down, against the brush's own colour (scaled by pickup)." },
|
|
10634
|
-
dirty: { type: "boolean", description: "Dirty brush: keep the carried paint after the stroke, so the next stroke starts with the last colour it touched (needs pickup above 0). clean_mixer_brush wipes it." },
|
|
10635
|
-
mixing: MIXING,
|
|
10636
|
-
multicolor: {
|
|
10637
|
-
type: "object",
|
|
10638
|
-
description: "Multi-coloured brush (the older form of paint, the brush's own gradient: tip is across-tip, dab is distance in dabs or random). Read back from paint when it has a multi-colour equivalent. Missing fields keep their current values.",
|
|
10639
|
-
properties: {
|
|
10640
|
-
mode: { type: "string", enum: [...MULTI_COLOR_MODES], description: "off; tip: the colours lie side by side across the tip (stripes along the stroke, like a brush loaded with several paints); dab: the colour changes along the stroke." },
|
|
10641
|
-
colors: { type: "array", maxItems: 8, items: { ...COLOR3 }, description: "2..8 colours #rrggbb; fewer than 2 uses the paint colour and the secondary colour." },
|
|
10642
|
-
order: { type: "string", enum: ["sequence", "random"], description: "dab mode: sequence runs through the colours in order, blending between them; random picks one per run of dabs (seeded)." },
|
|
10643
|
-
length: { type: "integer", minimum: 1, maximum: 500, description: "dab mode: dabs per colour (default 12)." },
|
|
10644
|
-
softness: { type: "number", minimum: 0, maximum: 1, description: "How softly neighbouring colours blend: 0 hard stripes or steps, 1 a smooth gradient." }
|
|
10645
|
-
},
|
|
10646
|
-
additionalProperties: false
|
|
10647
|
-
}
|
|
10648
|
-
},
|
|
10649
|
-
additionalProperties: false
|
|
10650
|
-
};
|
|
10651
|
-
var BRUSH_PAINT = {
|
|
10652
|
-
description: `The brush's own gradient paint, used instead of the editor's paint (null: the editor's paint). It is what the multi-colour settings became; stops may be "paint" / "secondary" for the editor's two colours.`,
|
|
10653
|
-
anyOf: [PAINT_GRADIENT_JSON_SCHEMA, { type: "null", description: "No gradient of its own: the editor's paint." }]
|
|
10654
|
-
};
|
|
10655
|
-
var BLENDER = {
|
|
10656
|
-
type: "object",
|
|
10657
|
-
description: "Blender engine settings. Missing fields keep their current values.",
|
|
10658
|
-
properties: {
|
|
10659
|
-
strength: { type: "number", minimum: 0, maximum: 1, description: "How far each pixel under the tip moves towards the mix of the paint near it, per dab." },
|
|
10660
|
-
distance: { type: "number", minimum: 0, maximum: 1, description: "Blend distance: how far away colours are mixed in, as a fraction of the tip diameter (0.35 default; higher blends wider and softer)." },
|
|
10661
|
-
mixing: MIXING
|
|
10662
|
-
},
|
|
10663
|
-
additionalProperties: false
|
|
10664
|
-
};
|
|
10665
|
-
var SMUDGE = {
|
|
10666
|
-
type: "object",
|
|
10667
|
-
description: "Smudge engine settings. Missing fields keep their current values.",
|
|
10668
|
-
properties: {
|
|
10669
|
-
strength: { type: "number", minimum: 0, maximum: 1, description: "How strongly the dragged paint covers what is under the tip, per dab." },
|
|
10670
|
-
length: { type: "number", minimum: 0, maximum: 1, description: "How far paint is dragged: the share of the carried paint kept from dab to dab (0.85 default; 1 drags it the whole stroke)." },
|
|
10671
|
-
mixing: { ...MIXING, description: "rgb (default): smeared colours average; pigment: they mix like paint." }
|
|
10672
|
-
},
|
|
10673
|
-
additionalProperties: false
|
|
10674
|
-
};
|
|
10675
|
-
var WET_BRUSH = {
|
|
10676
|
-
type: "object",
|
|
10677
|
-
description: "Wet engine settings (watercolour, gouache and the wet tools). Missing fields keep their current values.",
|
|
10678
|
-
properties: {
|
|
10679
|
-
tool: {
|
|
10680
|
-
type: "string",
|
|
10681
|
-
enum: [...WET_TOOLS],
|
|
10682
|
-
description: "paint: water and pigment (picks up and mixes wet paint by behavior.pickup / mix); water: clear water (with rewet it lifts dried paint); dry: a hair dryer, takes water; blow: pushes the water along the stroke; blend: evens out wet pigment; smudge: drags wet paint; blot: a tissue, takes wet paint up (erase wet)."
|
|
10683
|
-
},
|
|
10684
|
-
medium: { type: "string", enum: [...WET_MEDIA], description: "The medium: watercolor, gouache or oil. A layer takes the medium's wet settings the first time it holds wet paint (oil: thick, barely flows, stays wet long and blends into the wet paint it meets)." },
|
|
10685
|
-
load: { type: "number", minimum: 0, maximum: 1, description: "Paint loading: 1 never runs out; lower, the brush runs dry along a long stroke (each stroke reloads)." },
|
|
10686
|
-
thinner: { type: "number", minimum: 0, maximum: 1, description: "Thinner (water or solvent): more transparent, runnier paint." },
|
|
10687
|
-
water: { type: "number", minimum: 0, maximum: 1, description: "Wetness: water laid per stroke, 0.2 a damp brush .. 1 a dripping one." },
|
|
10688
|
-
pigment: { type: "number", minimum: 0, maximum: 1, description: "Pigment concentration: 0 clear water .. 1 straight from the tube." },
|
|
10689
|
-
body: { type: "number", minimum: 0, maximum: 1, description: "Body: 0 a transparent watercolour (glazes) .. 1 opaque gouache (hides what is under it)." },
|
|
10690
|
-
strength: { type: "number", minimum: 0, maximum: 1, description: "Strength of dry, blow, blend, smudge and blot." },
|
|
10691
|
-
rewet: { type: "number", minimum: 0, maximum: 1, description: "How much dried paint the water lifts again: 0 none (staining) .. 1." }
|
|
10692
|
-
},
|
|
10693
|
-
additionalProperties: false
|
|
10694
|
-
};
|
|
10695
|
-
var IMPASTO_BRUSH = {
|
|
10696
|
-
type: "object",
|
|
10697
|
-
description: "Impasto engine settings (thick paint with relief, lit by set_lighting). Missing fields keep their current values.",
|
|
10698
|
-
properties: {
|
|
10699
|
-
tool: {
|
|
10700
|
-
type: "string",
|
|
10701
|
-
enum: [...IMPASTO_TOOLS],
|
|
10702
|
-
description: "brush: thick paint with bristle ridges along the stroke; tube: blobs squeezed from the tube; knife: a palette knife that scrapes the relief flat and smears the paint; roller: a flat stippled band; gloop: a thick, glossy, rounded bead."
|
|
10703
|
-
},
|
|
10704
|
-
thickness: { type: "number", minimum: 0, maximum: 1, description: "How far the paint stands up." },
|
|
10705
|
-
gloss: { type: "number", minimum: 0, maximum: 1, description: "Shine: 0 matte .. 1 wet-looking gloss." },
|
|
10706
|
-
metallic: { type: "number", minimum: 0, maximum: 1, description: "Metallic paint: highlights take the paint colour (gold, copper, silver)." },
|
|
10707
|
-
load: { type: "number", minimum: 0, maximum: 1, description: "Paint loading: 1 never runs out; lower, the stroke thins and fades as the brush empties." },
|
|
10708
|
-
thinner: { type: "number", minimum: 0, maximum: 1, description: "Thinner: flatter, more transparent paint." },
|
|
10709
|
-
drag: { type: "number", minimum: 0, maximum: 1, description: "Knife and brush: how much of the paint under the tool is dragged along." },
|
|
10710
|
-
rim: { type: "number", minimum: 0, maximum: 1, description: "Light paint (with light.amount): the bead's light is stronger where its surface turns away, as on backlit glass (default 0)." },
|
|
10711
|
-
clarity: { type: "number", minimum: 0, maximum: 1, description: "Glassy paint: the middle of a bead lets the layers below show through while its edges stay solid (default 0). flatten_relief clears it." }
|
|
10712
|
-
},
|
|
10713
|
-
additionalProperties: false
|
|
10714
|
-
};
|
|
10715
|
-
var LIGHT_BRUSH = {
|
|
10716
|
-
type: "object",
|
|
10717
|
-
description: "Light paint: the light the brush lays with its colour, glowing in the paint's own colour and spilling onto its surroundings (set_light_spill). Dab, particle, bristle, Mixer Brush, impasto and wet paint brushes take it (wet light flows, pools, granulates and blooms with the pigment until the paint dries, then is ordinary light paint); wet tools other than paint lay none, and sticker brushes refuse light above 0. Missing fields keep their current values.",
|
|
10718
|
-
properties: {
|
|
10719
|
-
amount: { type: "number", minimum: 0, maximum: 1, description: "Light each dab lays, scaled by its coverage and flow (0 none, default; 1 full)." },
|
|
10720
|
-
pressure: { type: "boolean", description: "Pressure scales the light (default false)." }
|
|
10721
|
-
},
|
|
10722
|
-
additionalProperties: false
|
|
10723
|
-
};
|
|
10724
|
-
var PARTICLE_BRUSH = {
|
|
10725
|
-
type: "object",
|
|
10726
|
-
description: "Particle engine settings (procedural brushes). Missing fields keep their current values.",
|
|
10727
|
-
properties: {
|
|
10728
|
-
kind: {
|
|
10729
|
-
type: "string",
|
|
10730
|
-
enum: [...PARTICLE_KINDS],
|
|
10731
|
-
description: "string: strands of masses on springs dragged behind the pen (hair, fur, wet string); spring: particles on springs to the pen, overshooting and wobbling; orbit: particles circling the pen (curls); flow: particles thrown off the pen into a swirling flow until they fade; elastic: strands whose links keep a rest length, stretching when the pen is fast and snapping back; fuzzy: winding strings, each strand waving out of step with the others (always waves by at least 0.12 of the tip size)."
|
|
10732
|
-
},
|
|
10733
|
-
render: {
|
|
10734
|
-
type: "string",
|
|
10735
|
-
enum: [...PARTICLE_RENDERS],
|
|
10736
|
-
description: "dot: a mark at each step (round, or dotTextures); line: each particle draws a continuous line; sequence: beads spaced along each path (round, or dotTextures); ribbon: the band between neighbouring particles' paths filled (ribbonFill), with the paths drawn as its edges at the mark size (flame, silk, smoke)."
|
|
10737
|
-
},
|
|
10738
|
-
count: { type: "integer", minimum: 1, maximum: 64, description: "Particles (flow: how many are thrown per tip of travel)." },
|
|
10739
|
-
size: { type: "number", minimum: 0, maximum: 1, description: "Mark size as a fraction of the tip size (ribbon: the width of its edges)." },
|
|
10740
|
-
spread: { type: "number", minimum: 0, maximum: 1, description: "How far apart the particles start, as a fraction of the tip size (ribbon: how wide it is)." },
|
|
10741
|
-
stiffness: { type: "number", minimum: 0, maximum: 1, description: "Spring, string and elastic stiffness: stiff follows closely (elastic: snaps back faster), soft swings wide." },
|
|
10742
|
-
damping: { type: "number", minimum: 0, maximum: 1, description: "How quickly motion dies down." },
|
|
10743
|
-
speed: { type: "number", minimum: 0, maximum: 1, description: "Orbit and flow speed." },
|
|
10744
|
-
gravity: { type: "number", minimum: -1, maximum: 1, description: "Pull downwards (negative: upwards)." },
|
|
10745
|
-
life: { type: "number", minimum: 0, maximum: 1, description: "Flow particles: how long they live." },
|
|
10746
|
-
interval: { type: "number", minimum: 0, maximum: 1, description: "Sequence: spacing of the beads." },
|
|
10747
|
-
ribbonFill: { type: "number", minimum: 0, maximum: 1, description: "Ribbon: opacity of the fill between the paths, relative to their edges (default 0.5; 0 draws only the edges)." },
|
|
10748
|
-
ribbonPairs: { type: "string", enum: [...RIBBON_PAIRS], description: "Ribbon: adjacent (default) joins each particle to its neighbour around the pen; first-last makes one band between the outermost two." },
|
|
10749
|
-
waveAmplitude: { type: "number", minimum: 0, maximum: 1, description: "Wave: how far each particle's drawn place swings sideways to the stroke, as a fraction of the tip size (default 0: no wave). Works with every kind and render." },
|
|
10750
|
-
waveLength: { type: "number", minimum: 4, maximum: 2e3, description: "Wave: wavelength in px of pen travel (default 60)." },
|
|
10751
|
-
waveSpeed: { type: "number", minimum: 0, maximum: 1, description: "Wave: how fast the waves run, by stroke time (0 default: they stand still on the path; 1: about four a second, so slow strokes wave more). Strokes without times run at a nominal 0.5 px/ms." },
|
|
10752
|
-
wavePhaseJitter: { type: "number", minimum: 0, maximum: 1, description: "Wave: how far out of step the particles' waves are (default 0: all in step, a wavy band; 1: random phases, tangled strands)." },
|
|
10753
|
-
dotTextures: {
|
|
10754
|
-
type: "array",
|
|
10755
|
-
maxItems: MAX_DOT_TEXTURES,
|
|
10756
|
-
items: { type: "string", enum: [...STAMP_KINDS], description: "A stamp." },
|
|
10757
|
-
description: `Dot and sequence renders: up to ${MAX_DOT_TEXTURES} sticker stamps (list_stickers) drawn instead of round marks; repeats are allowed (they weight the order). Empty (default): round marks.`
|
|
10758
|
-
},
|
|
10759
|
-
dotTextureOrder: { type: "string", enum: [...DOT_TEXTURE_ORDERS], description: "Textured dots: sequence (default) uses the stamps in turn; random picks one per dot (seeded, so replays are exact)." },
|
|
10760
|
-
dotRotation: { type: "string", enum: [...DOT_ROTATIONS], description: "Textured dots: follow turns each with its particle's motion; random (default) turns each at random; fixed uses the tip angle." }
|
|
10761
|
-
},
|
|
10762
|
-
additionalProperties: false
|
|
10763
|
-
};
|
|
10764
|
-
var BRISTLE_BRUSH = {
|
|
10765
|
-
type: "object",
|
|
10766
|
-
description: "Bristle engine settings: a tuft of bristles, each its own line. Missing fields keep their current values.",
|
|
10767
|
-
properties: {
|
|
10768
|
-
count: { type: "integer", minimum: 3, maximum: 128, description: "Bristles in the tuft." },
|
|
10769
|
-
stiffness: { type: "number", minimum: 0, maximum: 1, description: "Stiff bristles follow the tip; soft ones lag and bend into curves at turns." },
|
|
10770
|
-
splay: { type: "number", minimum: 0, maximum: 1, description: "How far pressure spreads the tuft." },
|
|
10771
|
-
thickness: { type: "number", minimum: 0, maximum: 1, description: "Each bristle's line width as a fraction of the tip size." },
|
|
10772
|
-
load: { type: "number", minimum: 0, maximum: 1, description: "Paint per bristle: 1 never runs dry; lower, bristles run out along the stroke (dry brush)." },
|
|
10773
|
-
jitter: { type: "number", minimum: 0, maximum: 1, description: "Random variation between bristles." }
|
|
10774
|
-
},
|
|
10775
|
-
additionalProperties: false
|
|
10776
|
-
};
|
|
10777
|
-
var STICKER_BRUSH = {
|
|
10778
|
-
type: "object",
|
|
10779
|
-
description: "Sticker engine settings: stamps sprayed along the stroke as editable stickers. Missing fields keep their current values.",
|
|
10780
|
-
properties: {
|
|
10781
|
-
stamps: {
|
|
10782
|
-
type: "array",
|
|
10783
|
-
minItems: 1,
|
|
10784
|
-
maxItems: MAX_STICKER_STAMPS,
|
|
10785
|
-
items: { type: "string", minLength: 1, description: "A sticker ref provider:id from find_stickers (e.g. editor:star); a bare editor stamp (star) means editor:star." },
|
|
10786
|
-
description: `The stickers to spray, one picked at random per sticker: refs from find_stickers (the editor's own: ${STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}). Each is checked against the sticker library and an unknown ref is refused with the closest ones. A provider's sticker is drawn from its art (tinted to the paint colour when tintable), loaded before the stroke.`
|
|
10787
|
-
},
|
|
10788
|
-
density: { type: "number", minimum: 0, maximum: 1, description: "How closely stickers follow each other: about density \xD7 4 stickers per tip diameter of travel (0.25 \u2248 one per diameter, 1 \u2248 four). Default 0.4." },
|
|
10789
|
-
sizeJitter: { type: "number", minimum: 0, maximum: 1, description: "Random size change." },
|
|
10790
|
-
rotationJitter: { type: "number", minimum: 0, maximum: 1, description: "Random turn as a fraction of \xB1180\xB0." },
|
|
10791
|
-
scatter: { type: "number", minimum: 0, maximum: 2, description: "How far from the path stickers land, in tip diameters." },
|
|
10792
|
-
colorJitter: { type: "number", minimum: 0, maximum: 1, description: "Random colour change around the paint colour: at 1, up to \xB140\xB0 of hue and \xB10.15 OKLab lightness, kept in gamut. Default 0.2." }
|
|
10793
|
-
},
|
|
10794
|
-
additionalProperties: false
|
|
10795
|
-
};
|
|
10796
|
-
var BRUSH_FIELDS = {
|
|
10797
|
-
name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." },
|
|
10798
|
-
category: { type: "string", enum: [...PAINT_BRUSH_CATEGORIES], description: "Library category." },
|
|
10799
|
-
engine: {
|
|
10800
|
-
type: "string",
|
|
10801
|
-
enum: [...PAINT_BRUSH_ENGINES],
|
|
10802
|
-
description: "dab: paints dabs (with behavior.pickup or gradient paint it paints dab by dab, lifting and mixing paint); eraser: removes paint; mixer: the Mixer Brush (a reservoir plus picked-up paint); blender: mixes the colours under and near the tip; smudge: drags paint along the stroke; wet: watercolour, gouache, oil and the wet tools on a simulated wet layer (see wet); impasto: thick paint with relief (see impasto); particle: procedural particle brushes (see particle); bristle: a tuft of bristles that bend and splay (see bristle); sticker: sprays editable sticker stamps (see sticker)."
|
|
10803
|
-
},
|
|
10804
|
-
tip: TIP,
|
|
10805
|
-
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response: 0 ignores the grain, 1 catches only its peaks at light pressure (pencil, pastel, charcoal). Multiplied by the paper depth." },
|
|
10806
|
-
pressureSmoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Pressure smoothing, separate from the tip position smoothing." },
|
|
10807
|
-
rotation: { type: "string", enum: [...BRUSH_ROTATIONS], description: "What turns the tip: fixed (tip.angle), stroke (follows the direction of travel) or barrel (the pen barrel rotation, twist)." },
|
|
10808
|
-
dynamics: DYNAMICS,
|
|
10809
|
-
mixer: MIXER,
|
|
10810
|
-
loadsColor: { type: "boolean", description: "Mixer engine: load the colour each stroke (false for a mixer that only moves paint)." },
|
|
10811
|
-
behavior: BEHAVIOR,
|
|
10812
|
-
blender: BLENDER,
|
|
10813
|
-
smudge: SMUDGE,
|
|
10814
|
-
wet: WET_BRUSH,
|
|
10815
|
-
impasto: IMPASTO_BRUSH,
|
|
10816
|
-
particle: PARTICLE_BRUSH,
|
|
10817
|
-
bristle: BRISTLE_BRUSH,
|
|
10818
|
-
sticker: STICKER_BRUSH,
|
|
10819
|
-
light: LIGHT_BRUSH,
|
|
10820
|
-
paint: BRUSH_PAINT
|
|
10821
|
-
};
|
|
10822
10765
|
var TIME = { type: "number", minimum: 0, description: "Draw live effect paint (strokes on stroke layers painted with live effect paint) this many ms into its animation (default: as painted, its own time)." };
|
|
10823
10766
|
var POINTS = {
|
|
10824
10767
|
type: "array",
|
|
@@ -10846,26 +10789,6 @@ function loadResult(load) {
|
|
|
10846
10789
|
const hex = (c) => c ? "#" + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("") : null;
|
|
10847
10790
|
return { reservoir: hex(load.reservoir), amount: Math.round(load.amount * 1e3) / 1e3, pickup: hex(load.pickup) };
|
|
10848
10791
|
}
|
|
10849
|
-
function brushSummary(b) {
|
|
10850
|
-
return {
|
|
10851
|
-
gradient: b.paint !== null,
|
|
10852
|
-
id: b.id,
|
|
10853
|
-
name: b.name,
|
|
10854
|
-
category: b.category,
|
|
10855
|
-
engine: b.engine,
|
|
10856
|
-
size: b.tip.size,
|
|
10857
|
-
grain: b.grain,
|
|
10858
|
-
rotation: b.rotation,
|
|
10859
|
-
wetCapable: isWetCapable(b),
|
|
10860
|
-
pickup: b.behavior.pickup,
|
|
10861
|
-
dirty: b.behavior.dirty,
|
|
10862
|
-
multicolor: b.behavior.multicolor.mode,
|
|
10863
|
-
...b.engine === "wet" ? { wetTool: b.wet.tool, medium: b.wet.medium } : {},
|
|
10864
|
-
...b.engine === "impasto" ? { impastoTool: b.impasto.tool } : {},
|
|
10865
|
-
...b.light.amount > 0 ? { light: b.light.amount } : {},
|
|
10866
|
-
builtin: Boolean(b.builtin)
|
|
10867
|
-
};
|
|
10868
|
-
}
|
|
10869
10792
|
function createPaintEditorToolProvider(controller, options = {}) {
|
|
10870
10793
|
const c = typeof controller === "function" ? lazyController(controller) : controller;
|
|
10871
10794
|
const tools = [];
|
|
@@ -11180,121 +11103,31 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11180
11103
|
coordinates: { out: ["bounds"] },
|
|
11181
11104
|
handler: ({ layerId }) => c.clearLayer(layerId)
|
|
11182
11105
|
});
|
|
11183
|
-
add({
|
|
11184
|
-
name: "list_brushes",
|
|
11185
|
-
title: "List brushes",
|
|
11186
|
-
description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, light ribbon, flame ribbon, neon strand, smoke ribbon, bokeh dots, twinkle sparks, glow wash, highlighter; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
|
|
11187
|
-
inputSchema: EMPTY3,
|
|
11188
|
-
annotations: { title: "List brushes", ...READ6 },
|
|
11189
|
-
handler: () => {
|
|
11190
|
-
const s = c.getState();
|
|
11191
|
-
return { activeBrushId: s.brush.id, activeEdited: !s.brush.builtin && Boolean(c.getBrush(s.brush.id)?.builtin), brushes: s.brushes.map(brushSummary) };
|
|
11192
|
-
}
|
|
11193
|
-
});
|
|
11194
11106
|
add({
|
|
11195
11107
|
name: "get_brush",
|
|
11196
11108
|
title: "Get a brush",
|
|
11197
|
-
description: "Reads
|
|
11198
|
-
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from
|
|
11109
|
+
description: "Reads a brush as facets (kind, medium, variety or tool, tip, marks, dynamics, behaviour, light, motion, colour: the brush library's form, as describe_brush returns it), with the engine it paints with and whether it is an unchanged built-in preset. Without id, the active brush as edited. Also returns strokePaint: what a stroke with it would paint now (its gradient colour, else the editor's paint from set_paint). Changes nothing.",
|
|
11110
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." } }, additionalProperties: false },
|
|
11199
11111
|
annotations: { title: "Get a brush", ...READ6 },
|
|
11200
11112
|
handler: ({ id }) => {
|
|
11201
11113
|
const st = c.getState();
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
return {
|
|
11114
|
+
const b = id === void 0 ? st.brush : c.getBrush(id);
|
|
11115
|
+
if (!b) c.setBrushFacets({}, id);
|
|
11116
|
+
const brush = b;
|
|
11117
|
+
return { brush: c.getBrushSpec(id), engine: brush.engine, category: brush.category, builtin: Boolean(brush.builtin), strokePaint: brush.paint ?? st.paint };
|
|
11206
11118
|
}
|
|
11207
11119
|
});
|
|
11208
11120
|
add({
|
|
11209
11121
|
name: "set_brush",
|
|
11210
11122
|
title: "Set the brush",
|
|
11211
|
-
description: "Chooses the active brush (id) and / or changes
|
|
11212
|
-
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from
|
|
11123
|
+
description: "Chooses the active brush (id, from list_brush_presets) and / or changes it with facets laid over it (objects merge, lists and values replace, marks of another kind replace the marks; the brush library's create_brush fields); unset removes whole facets first (unset colour drops a brush's own gradient). Strokes without a brush use it. Editor state, not an undo step: the changes stay on the active brush until the library's create_brush or update_brush stores a brush. Returns the active brush as facets, the engine it paints with, and problems (values clamped or dropped).",
|
|
11124
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets to start from (default the active brush)." }, name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." }, ...BRUSH_PATCH_PROPERTIES, unset: BRUSH_UNSET_PROPERTY }, additionalProperties: false },
|
|
11213
11125
|
annotations: { title: "Set the brush", ...SET4 },
|
|
11214
|
-
handler: async (
|
|
11215
|
-
const stamps =
|
|
11126
|
+
handler: async ({ id, unset, ...patch }) => {
|
|
11127
|
+
const stamps = patch.tip?.stamps;
|
|
11216
11128
|
if (stamps) await c.prepareStickers(stamps);
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
});
|
|
11220
|
-
add({
|
|
11221
|
-
name: "save_brush",
|
|
11222
|
-
title: "Save a brush",
|
|
11223
|
-
description: "Saves the active brush (with its changes) into the user library under a new or existing user id. Built-in ids are refused. Library state, not an undo step. Returns the saved brush.",
|
|
11224
|
-
inputSchema: {
|
|
11225
|
-
type: "object",
|
|
11226
|
-
properties: {
|
|
11227
|
-
id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,63}$", description: "Library id: lowercase letters, digits and dashes." },
|
|
11228
|
-
name: { type: "string", minLength: 1, maxLength: 80, description: "Display name (default the brush name)." }
|
|
11229
|
-
},
|
|
11230
|
-
required: ["id"],
|
|
11231
|
-
additionalProperties: false
|
|
11232
|
-
},
|
|
11233
|
-
annotations: { title: "Save a brush", ...SET4 },
|
|
11234
|
-
handler: ({ id, name }) => brushToJSON(c.saveBrush({ id, name }))
|
|
11235
|
-
});
|
|
11236
|
-
add({
|
|
11237
|
-
name: "remove_brush",
|
|
11238
|
-
title: "Remove a saved brush",
|
|
11239
|
-
description: "Removes a user brush from the library (built-in brushes cannot be removed). Library state, not an undo step. Returns the removed id.",
|
|
11240
|
-
inputSchema: { type: "object", properties: { id: { type: "string", description: "User brush id from list_brushes." } }, required: ["id"], additionalProperties: false },
|
|
11241
|
-
annotations: { title: "Remove a saved brush", ...DESTRUCTIVE3 },
|
|
11242
|
-
handler: ({ id }) => {
|
|
11243
|
-
c.removeBrush(id);
|
|
11244
|
-
return { removed: id };
|
|
11245
|
-
}
|
|
11246
|
-
});
|
|
11247
|
-
add({
|
|
11248
|
-
name: "export_brushes",
|
|
11249
|
-
title: "Export brushes",
|
|
11250
|
-
description: "Returns a brush library file (JSON: format pixerate-brushes, version 1, brushes) of the given brushes, or of every user brush. Changes nothing.",
|
|
11251
|
-
inputSchema: {
|
|
11252
|
-
type: "object",
|
|
11253
|
-
properties: { ids: { type: "array", items: { type: "string", description: "Brush id." }, description: "Brush ids from list_brushes (default every user brush)." } },
|
|
11254
|
-
additionalProperties: false
|
|
11255
|
-
},
|
|
11256
|
-
annotations: { title: "Export brushes", ...READ6 },
|
|
11257
|
-
handler: ({ ids }) => c.exportBrushes(ids)
|
|
11258
|
-
});
|
|
11259
|
-
add({
|
|
11260
|
-
name: "import_brushes",
|
|
11261
|
-
title: "Import brushes",
|
|
11262
|
-
description: "Adds the brushes of a brush library file (as export_brushes returns) to the user library. Taken ids are skipped unless replace is true; built-in ids are always skipped. Library state, not an undo step. Returns the ids added and skipped and any problems.",
|
|
11263
|
-
inputSchema: {
|
|
11264
|
-
type: "object",
|
|
11265
|
-
properties: {
|
|
11266
|
-
library: { type: "object", description: 'The library file: {format: "pixerate-brushes", version: 1, brushes: [...]}.' },
|
|
11267
|
-
replace: { type: "boolean", description: "Replace user brushes with the same id (default false)." }
|
|
11268
|
-
},
|
|
11269
|
-
required: ["library"],
|
|
11270
|
-
additionalProperties: false
|
|
11271
|
-
},
|
|
11272
|
-
annotations: { title: "Import brushes", ...WRITE5 },
|
|
11273
|
-
handler: ({ library, replace }) => c.importBrushes(library, { replace })
|
|
11274
|
-
});
|
|
11275
|
-
add({
|
|
11276
|
-
name: "render_brush_thumbnail",
|
|
11277
|
-
title: "Render a brush thumbnail",
|
|
11278
|
-
description: "Renders a brush as a PNG: a test stroke (an S-curve swelling and fading in pressure) on the current paper, so you can see what it does before painting. Changes nothing.",
|
|
11279
|
-
inputSchema: {
|
|
11280
|
-
type: "object",
|
|
11281
|
-
properties: {
|
|
11282
|
-
id: { type: "string", description: "Brush id from list_brushes (default the active brush)." },
|
|
11283
|
-
width: { type: "integer", minimum: 16, maximum: 512, description: "Width in pixels (default 160)." },
|
|
11284
|
-
height: { type: "integer", minimum: 16, maximum: 512, description: "Height in pixels (default 48)." },
|
|
11285
|
-
color: { ...COLOR3, description: "Stroke colour (default the paint colour)." }
|
|
11286
|
-
},
|
|
11287
|
-
additionalProperties: false
|
|
11288
|
-
},
|
|
11289
|
-
annotations: { title: "Render a brush thumbnail", ...READ6 },
|
|
11290
|
-
handler: ({ id, width, height, color }) => {
|
|
11291
|
-
const t = c.brushThumbnail(id ?? c.getState().brush.id, { width, height, color });
|
|
11292
|
-
return {
|
|
11293
|
-
content: [
|
|
11294
|
-
{ type: "image", data: bytesToBase64(encodePng(t.width, t.height, t.data)), mimeType: "image/png" },
|
|
11295
|
-
{ type: "text", text: `Brush ${id ?? c.getState().brush.id} ${t.width}\xD7${t.height}` }
|
|
11296
|
-
]
|
|
11297
|
-
};
|
|
11129
|
+
const r = c.setBrushFacets(patch, id, unset);
|
|
11130
|
+
return { brush: r.spec, engine: r.brush.engine, problems: r.problems };
|
|
11298
11131
|
}
|
|
11299
11132
|
});
|
|
11300
11133
|
add({
|
|
@@ -11371,14 +11204,14 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11371
11204
|
add({
|
|
11372
11205
|
name: "paint_stroke",
|
|
11373
11206
|
title: "Paint a brush stroke",
|
|
11374
|
-
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer\u2019s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet; with light.amount, as glow-wash has, their light flows with the paint), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
|
|
11207
|
+
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer of the canvas with a library brush or the active one, optionally changed (by facets) for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer\u2019s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet; with light.amount, as glow-wash has, their light flows with the paint), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
|
|
11375
11208
|
inputSchema: {
|
|
11376
11209
|
type: "object",
|
|
11377
11210
|
properties: {
|
|
11378
11211
|
points: POINTS,
|
|
11379
11212
|
layerId: LAYER_ID4,
|
|
11380
|
-
brushId: { type: "string", description: "Brush id from
|
|
11381
|
-
brush: { type: "object", description: "
|
|
11213
|
+
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
11214
|
+
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 },
|
|
11382
11215
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
11383
11216
|
paint: { ...PAINT_SOURCE_JSON_SCHEMA, description: "What this stroke paints with instead of the editor's paint: a colour, or gradient paint that changes colour as the stroke goes (by distance, pressure, tilt, speed, direction, time, across the tip or at random). Wins over the brush's own gradient." },
|
|
11384
11217
|
seed: { type: "integer", minimum: 0, description: "Jitter seed (default a new one); the same seed and points give the same marks." },
|
|
@@ -11392,9 +11225,9 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11392
11225
|
handler: async (a) => {
|
|
11393
11226
|
await c.wetIdle();
|
|
11394
11227
|
const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
|
|
11395
|
-
const
|
|
11396
|
-
if ((
|
|
11397
|
-
const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush
|
|
11228
|
+
const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
|
|
11229
|
+
if ((brush?.engine ?? base?.engine) === "sticker") await c.prepareStickers(brush?.sticker?.stamps ?? base?.sticker.stamps ?? []);
|
|
11230
|
+
const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
|
|
11398
11231
|
const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
|
|
11399
11232
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11400
11233
|
}
|
|
@@ -11457,8 +11290,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11457
11290
|
type: "object",
|
|
11458
11291
|
properties: {
|
|
11459
11292
|
id: { type: "string", description: "Stroke id from list_strokes {live: true}." },
|
|
11460
|
-
brushId: { type: "string", description: "Brush
|
|
11461
|
-
brush: { type: "object", properties:
|
|
11293
|
+
brushId: { type: "string", description: "Brush id from list_brush_presets; the stroke keeps its size." },
|
|
11294
|
+
brush: { type: "object", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false, description: "Facets to change on top of brushId or the stroke\u2019s brush (as set_brush takes them); missing fields keep their values." },
|
|
11462
11295
|
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "New colour #rrggbb (replaces gradient paint)." },
|
|
11463
11296
|
paint: { anyOf: [PAINT_SOURCE_JSON_SCHEMA, { type: "null", description: "Back to the stroke\u2019s plain colour." }], description: "A colour or gradient paint for the stroke; null drops its gradient." },
|
|
11464
11297
|
seed: { type: "integer", minimum: 0, description: "Another jitter seed: particles, scatter and texture fall differently." }
|
|
@@ -11470,6 +11303,11 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11470
11303
|
coordinates: { out: ["bounds", "stroke.bounds"] },
|
|
11471
11304
|
handler: ({ id, ...update }) => {
|
|
11472
11305
|
if (!Object.keys(update).length) throw new Error("Pass at least one change: brushId, brush, color, paint or seed.");
|
|
11306
|
+
if (update.brush) {
|
|
11307
|
+
const stroke = Object.values(c.document.liveStrokes ?? {}).flat().find((s) => s.id === id);
|
|
11308
|
+
if (!stroke) throw new Error(`Unknown stroke "${id}" on a stroke layer. Call list_strokes {live: true} for the ids.`);
|
|
11309
|
+
update.brush = c.brushChanges(update.brush, update.brushId ?? stroke.brush);
|
|
11310
|
+
}
|
|
11473
11311
|
const r = c.updateStroke(id, update);
|
|
11474
11312
|
return { stroke: strokeSummary(r.stroke, false, true), bounds: r.bounds };
|
|
11475
11313
|
}
|
|
@@ -11574,7 +11412,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11574
11412
|
},
|
|
11575
11413
|
pressureScale: { type: "number", minimum: 0, maximum: 4, description: "Multiply every pressure (default 1)." },
|
|
11576
11414
|
color: { ...COLOR3, description: "Recolour (default each stroke\u2019s colour)." },
|
|
11577
|
-
brushId: { type: "string", description: "Replay with this brush from
|
|
11415
|
+
brushId: { type: "string", description: "Replay with this brush from list_brush_presets (default each stroke\u2019s brush)." },
|
|
11578
11416
|
layerId: { type: "string", description: "Paint on this layer (default each stroke\u2019s layer)." },
|
|
11579
11417
|
repeat: { type: "integer", minimum: 1, maximum: 100, description: "How many copies (default 1)." }
|
|
11580
11418
|
},
|
|
@@ -11761,6 +11599,18 @@ function strokeSummary(s, withPoints, live) {
|
|
|
11761
11599
|
};
|
|
11762
11600
|
}
|
|
11763
11601
|
|
|
11602
|
+
// src/paint-editor/brush-library-tools.ts
|
|
11603
|
+
function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
11604
|
+
return createBrushLibraryToolProvider(controller.brushLibrary, {
|
|
11605
|
+
profile: PAINT_PROFILE,
|
|
11606
|
+
...options,
|
|
11607
|
+
renderPreview: (brush, { width, height, color }) => {
|
|
11608
|
+
const t = renderBrushThumbnail(resolveBrush(brush), { paper: controller.getState().document.paper, width, height, color: color.toLowerCase() });
|
|
11609
|
+
return { data: bytesToBase64(encodePng(t.width, t.height, t.data)), mimeType: "image/png" };
|
|
11610
|
+
}
|
|
11611
|
+
});
|
|
11612
|
+
}
|
|
11613
|
+
|
|
11764
11614
|
// src/paint-editor/ui.ts
|
|
11765
11615
|
var PAINT_TOOLS = [
|
|
11766
11616
|
{ id: "brush", label: "Brush", key: "b" },
|
|
@@ -12087,21 +11937,6 @@ function guideOverlay(controller, drag) {
|
|
|
12087
11937
|
if (s.guides.perspective.enabled) for (const [from, to] of perspectiveGuideLines(s.guides.perspective, size, 16)) out.push({ from, to, kind: "perspective" });
|
|
12088
11938
|
return out;
|
|
12089
11939
|
}
|
|
12090
|
-
var BRUSH_SLIDERS = [
|
|
12091
|
-
{ path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
|
|
12092
|
-
{ path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
|
|
12093
|
-
{ path: "tip.flow", label: "Flow", min: 0, max: 1, step: 0.01 },
|
|
12094
|
-
{ path: "tip.hardness", label: "Hardness", min: 0, max: 1, step: 0.01 },
|
|
12095
|
-
{ path: "tip.spacing", label: "Spacing", min: 0.01, max: 1, step: 0.01 },
|
|
12096
|
-
{ path: "tip.roundness", label: "Roundness", min: 0.05, max: 1, step: 0.01 },
|
|
12097
|
-
{ path: "tip.angle", label: "Angle", min: -180, max: 180, step: 1 },
|
|
12098
|
-
{ path: "grain", label: "Grain", min: 0, max: 1, step: 0.01 },
|
|
12099
|
-
{ path: "tip.smoothing", label: "Smoothing", min: 0, max: 0.95, step: 0.01 },
|
|
12100
|
-
{ path: "pressureSmoothing", label: "Pressure smoothing", min: 0, max: 0.95, step: 0.01 },
|
|
12101
|
-
{ path: "dynamics.minSize", label: "Min size", min: 0, max: 1, step: 0.01 },
|
|
12102
|
-
{ path: "dynamics.tiltSize", label: "Tilt size", min: 0, max: 1, step: 0.01 },
|
|
12103
|
-
{ path: "dynamics.velocitySize", label: "Speed size", min: -1, max: 1, step: 0.01 }
|
|
12104
|
-
];
|
|
12105
11940
|
var PAPER_SLIDERS = [
|
|
12106
11941
|
{ path: "depth", label: "Grain depth", min: 0, max: 1, step: 0.01 },
|
|
12107
11942
|
{ path: "scale", label: "Grain scale", min: 0.25, max: 8, step: 0.05 },
|
|
@@ -12160,158 +11995,6 @@ var BLEND_MENU_GROUPS = [
|
|
|
12160
11995
|
{ label: "Standard", modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== "pigment") },
|
|
12161
11996
|
{ label: "Paint", modes: BLEND_MODES.filter((m) => m.painterly || m.id === "pigment") }
|
|
12162
11997
|
];
|
|
12163
|
-
var LIGHT_SLIDER = { path: "light.amount", label: "Light", min: 0, max: 1, step: 0.01 };
|
|
12164
|
-
var PICKUP_SLIDERS = [
|
|
12165
|
-
{ path: "behavior.pickup", label: "Pick-up", min: 0, max: 1, step: 0.01 },
|
|
12166
|
-
{ path: "behavior.mix", label: "Mix", min: 0, max: 1, step: 0.01 }
|
|
12167
|
-
];
|
|
12168
|
-
function brushEngineControls(brush) {
|
|
12169
|
-
const c = engineControls(brush);
|
|
12170
|
-
const wetPaint = brush.engine === "wet" && brush.wet?.tool === "paint";
|
|
12171
|
-
if (!c || !(wetPaint || ["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine))) return c;
|
|
12172
|
-
const extra = brush.engine === "impasto" ? [{ path: "impasto.rim", label: "Rim", min: 0, max: 1, step: 0.01 }, { path: "impasto.clarity", label: "Clarity", min: 0, max: 1, step: 0.01 }] : [];
|
|
12173
|
-
return { ...c, sliders: [...c.sliders, LIGHT_SLIDER, ...extra], light: true };
|
|
12174
|
-
}
|
|
12175
|
-
function engineControls(brush) {
|
|
12176
|
-
switch (brush.engine) {
|
|
12177
|
-
case "particle":
|
|
12178
|
-
return {
|
|
12179
|
-
title: "Particles",
|
|
12180
|
-
sliders: [
|
|
12181
|
-
{ path: "particle.count", label: "Particles", min: 1, max: 64, step: 1 },
|
|
12182
|
-
{ path: "particle.size", label: "Mark size", min: 0, max: 1, step: 0.01 },
|
|
12183
|
-
{ path: "particle.spread", label: "Spread", min: 0, max: 1, step: 0.01 },
|
|
12184
|
-
{ path: "particle.stiffness", label: "Stiffness", min: 0, max: 1, step: 0.01 },
|
|
12185
|
-
{ path: "particle.damping", label: "Damping", min: 0, max: 1, step: 0.01 },
|
|
12186
|
-
{ path: "particle.speed", label: "Speed", min: 0, max: 1, step: 0.01 },
|
|
12187
|
-
{ path: "particle.gravity", label: "Gravity", min: -1, max: 1, step: 0.01 },
|
|
12188
|
-
{ path: "particle.life", label: "Life", min: 0, max: 1, step: 0.01 },
|
|
12189
|
-
{ path: "particle.interval", label: "Bead spacing", min: 0, max: 1, step: 0.01 },
|
|
12190
|
-
{ path: "particle.ribbonFill", label: "Ribbon fill", min: 0, max: 1, step: 0.01 },
|
|
12191
|
-
{ path: "particle.waveAmplitude", label: "Wave", min: 0, max: 1, step: 0.01 },
|
|
12192
|
-
{ path: "particle.waveLength", label: "Wavelength", min: 4, max: 400, step: 1 },
|
|
12193
|
-
{ path: "particle.waveSpeed", label: "Wave speed", min: 0, max: 1, step: 0.01 },
|
|
12194
|
-
{ path: "particle.wavePhaseJitter", label: "Wave scatter", min: 0, max: 1, step: 0.01 }
|
|
12195
|
-
],
|
|
12196
|
-
mixingPath: null,
|
|
12197
|
-
dirty: false,
|
|
12198
|
-
multicolor: false,
|
|
12199
|
-
load: false,
|
|
12200
|
-
particleMenus: true
|
|
12201
|
-
};
|
|
12202
|
-
case "bristle":
|
|
12203
|
-
return {
|
|
12204
|
-
title: "Bristles",
|
|
12205
|
-
sliders: [
|
|
12206
|
-
{ path: "bristle.count", label: "Bristles", min: 3, max: 128, step: 1 },
|
|
12207
|
-
{ path: "bristle.stiffness", label: "Stiffness", min: 0, max: 1, step: 0.01 },
|
|
12208
|
-
{ path: "bristle.splay", label: "Splay", min: 0, max: 1, step: 0.01 },
|
|
12209
|
-
{ path: "bristle.thickness", label: "Bristle width", min: 0, max: 1, step: 0.01 },
|
|
12210
|
-
{ path: "bristle.load", label: "Load", min: 0, max: 1, step: 0.01 },
|
|
12211
|
-
{ path: "bristle.jitter", label: "Variation", min: 0, max: 1, step: 0.01 }
|
|
12212
|
-
],
|
|
12213
|
-
mixingPath: null,
|
|
12214
|
-
dirty: false,
|
|
12215
|
-
multicolor: false,
|
|
12216
|
-
load: false
|
|
12217
|
-
};
|
|
12218
|
-
case "sticker":
|
|
12219
|
-
return {
|
|
12220
|
-
title: "Stickers",
|
|
12221
|
-
sliders: [
|
|
12222
|
-
{ path: "sticker.density", label: "Density", min: 0, max: 1, step: 0.01 },
|
|
12223
|
-
{ path: "sticker.sizeJitter", label: "Size jitter", min: 0, max: 1, step: 0.01 },
|
|
12224
|
-
{ path: "sticker.rotationJitter", label: "Turn jitter", min: 0, max: 1, step: 0.01 },
|
|
12225
|
-
{ path: "sticker.scatter", label: "Scatter", min: 0, max: 2, step: 0.01 },
|
|
12226
|
-
{ path: "sticker.colorJitter", label: "Colour jitter", min: 0, max: 1, step: 0.01 }
|
|
12227
|
-
],
|
|
12228
|
-
mixingPath: null,
|
|
12229
|
-
dirty: false,
|
|
12230
|
-
multicolor: false,
|
|
12231
|
-
load: false,
|
|
12232
|
-
stickerStamps: true
|
|
12233
|
-
};
|
|
12234
|
-
case "impasto":
|
|
12235
|
-
return {
|
|
12236
|
-
title: "Thick paint",
|
|
12237
|
-
sliders: [
|
|
12238
|
-
{ path: "impasto.thickness", label: "Thickness", min: 0, max: 1, step: 0.01 },
|
|
12239
|
-
{ path: "impasto.gloss", label: "Gloss", min: 0, max: 1, step: 0.01 },
|
|
12240
|
-
{ path: "impasto.metallic", label: "Metallic", min: 0, max: 1, step: 0.01 },
|
|
12241
|
-
{ path: "impasto.load", label: "Load", min: 0, max: 1, step: 0.01 },
|
|
12242
|
-
{ path: "impasto.thinner", label: "Thinner", min: 0, max: 1, step: 0.01 },
|
|
12243
|
-
{ path: "impasto.drag", label: "Drag", min: 0, max: 1, step: 0.01 },
|
|
12244
|
-
...PICKUP_SLIDERS
|
|
12245
|
-
],
|
|
12246
|
-
mixingPath: "behavior.mixing",
|
|
12247
|
-
dirty: true,
|
|
12248
|
-
multicolor: false,
|
|
12249
|
-
load: true,
|
|
12250
|
-
impastoTool: true
|
|
12251
|
-
};
|
|
12252
|
-
case "wet": {
|
|
12253
|
-
const tool = brush.wet?.tool ?? "paint";
|
|
12254
|
-
const sliders = tool === "paint" ? [
|
|
12255
|
-
{ path: "wet.water", label: "Water", min: 0, max: 1, step: 0.01 },
|
|
12256
|
-
{ path: "wet.pigment", label: "Pigment", min: 0, max: 1, step: 0.01 },
|
|
12257
|
-
{ path: "wet.body", label: "Body (opacity)", min: 0, max: 1, step: 0.01 },
|
|
12258
|
-
{ path: "wet.rewet", label: "Re-wet", min: 0, max: 1, step: 0.01 },
|
|
12259
|
-
{ path: "wet.load", label: "Load", min: 0, max: 1, step: 0.01 },
|
|
12260
|
-
{ path: "wet.thinner", label: "Thinner", min: 0, max: 1, step: 0.01 },
|
|
12261
|
-
...PICKUP_SLIDERS
|
|
12262
|
-
] : tool === "water" ? [
|
|
12263
|
-
{ path: "wet.water", label: "Water", min: 0, max: 1, step: 0.01 },
|
|
12264
|
-
{ path: "wet.rewet", label: "Re-wet", min: 0, max: 1, step: 0.01 }
|
|
12265
|
-
] : [{ path: "wet.strength", label: "Strength", min: 0, max: 1, step: 0.01 }];
|
|
12266
|
-
return { title: "Wet brush", sliders, mixingPath: tool === "paint" ? "behavior.mixing" : null, dirty: tool === "paint", multicolor: false, load: tool === "paint", wetTool: true };
|
|
12267
|
-
}
|
|
12268
|
-
case "blender":
|
|
12269
|
-
return {
|
|
12270
|
-
title: "Blender",
|
|
12271
|
-
sliders: [
|
|
12272
|
-
{ path: "blender.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
|
|
12273
|
-
{ path: "blender.distance", label: "Blend distance", min: 0, max: 1, step: 0.01 }
|
|
12274
|
-
],
|
|
12275
|
-
mixingPath: "blender.mixing",
|
|
12276
|
-
dirty: false,
|
|
12277
|
-
multicolor: false,
|
|
12278
|
-
load: false
|
|
12279
|
-
};
|
|
12280
|
-
case "smudge":
|
|
12281
|
-
return {
|
|
12282
|
-
title: "Smudge",
|
|
12283
|
-
sliders: [
|
|
12284
|
-
{ path: "smudge.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
|
|
12285
|
-
{ path: "smudge.length", label: "Length", min: 0, max: 1, step: 0.01 }
|
|
12286
|
-
],
|
|
12287
|
-
mixingPath: "smudge.mixing",
|
|
12288
|
-
dirty: false,
|
|
12289
|
-
multicolor: false,
|
|
12290
|
-
load: false
|
|
12291
|
-
};
|
|
12292
|
-
case "mixer":
|
|
12293
|
-
return { title: "Mixer Brush", sliders: [...PICKUP_SLIDERS, { path: "mixer.load", label: "Load", min: 0, max: 1, step: 0.01 }], mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
|
|
12294
|
-
case "dab":
|
|
12295
|
-
return { title: "Paint behaviour", sliders: PICKUP_SLIDERS, mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
|
|
12296
|
-
default:
|
|
12297
|
-
return null;
|
|
12298
|
-
}
|
|
12299
|
-
}
|
|
12300
|
-
function multiColorSliders(mode) {
|
|
12301
|
-
if (mode === "off") return [];
|
|
12302
|
-
const soft = { path: "behavior.multicolor.softness", label: "Colour softness", min: 0, max: 1, step: 0.01 };
|
|
12303
|
-
return mode === "dab" ? [soft, { path: "behavior.multicolor.length", label: "Dabs per colour", min: 1, max: 100, step: 1 }] : [soft];
|
|
12304
|
-
}
|
|
12305
|
-
function patchBrushValue(brush, path, value) {
|
|
12306
|
-
const set = (obj, keys) => {
|
|
12307
|
-
const [head2, ...rest2] = keys;
|
|
12308
|
-
if (!rest2.length) return { ...obj, [head2]: value };
|
|
12309
|
-
return { ...obj, [head2]: set(obj[head2] ?? {}, rest2) };
|
|
12310
|
-
};
|
|
12311
|
-
const [head, ...rest] = path.split(".");
|
|
12312
|
-
if (!rest.length) return { [head]: value };
|
|
12313
|
-
return { [head]: set({ ...brush[head] ?? {} }, rest) };
|
|
12314
|
-
}
|
|
12315
11998
|
function brushLoadColor(load) {
|
|
12316
11999
|
const c = load.pickup ?? load.reservoir;
|
|
12317
12000
|
return c ? rgbToHexColor(Math.round(c[0] * 255), Math.round(c[1] * 255), Math.round(c[2] * 255)) : null;
|
|
@@ -12881,6 +12564,7 @@ export {
|
|
|
12881
12564
|
colorHarmony2 as colorHarmony,
|
|
12882
12565
|
composeAffine,
|
|
12883
12566
|
contactSheet,
|
|
12567
|
+
createPaintBrushLibraryToolProvider,
|
|
12884
12568
|
createPaintCompositor,
|
|
12885
12569
|
createPaintDocument,
|
|
12886
12570
|
createPaintEditorDocument,
|
|
@@ -13037,6 +12721,7 @@ export {
|
|
|
13037
12721
|
strokeBounds,
|
|
13038
12722
|
strokeDuration,
|
|
13039
12723
|
strokeSchedule,
|
|
12724
|
+
strokeToJSON,
|
|
13040
12725
|
sweepPath,
|
|
13041
12726
|
symmetryAxisAngles,
|
|
13042
12727
|
symmetryAxisSegments,
|