@pixerate/editor 0.267.0 → 0.268.1
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/animation-editor/index.cjs +62 -62
- package/dist/animation-editor/index.js +6 -6
- package/dist/brushes/index.cjs +198 -72
- package/dist/brushes/index.d.cts +4 -3
- package/dist/brushes/index.d.ts +4 -3
- package/dist/brushes/index.js +136 -10
- package/dist/brushes/library.d.cts +27 -1
- package/dist/brushes/library.d.ts +27 -1
- package/dist/brushes/profiles.d.cts +4 -0
- package/dist/brushes/profiles.d.ts +4 -0
- package/dist/brushes/schema.d.cts +4 -0
- package/dist/brushes/schema.d.ts +4 -0
- package/dist/brushes/sources.d.cts +114 -0
- package/dist/brushes/sources.d.ts +114 -0
- package/dist/brushes/types.d.cts +37 -5
- package/dist/brushes/types.d.ts +37 -5
- package/dist/brushes/ui.d.cts +40 -4
- package/dist/brushes/ui.d.ts +40 -4
- package/dist/{brushstrokeRender-NLFPEBGI.js → brushstrokeRender-A727TTNQ.js} +6 -6
- package/dist/{brushstrokeRender-L7JYRR2R.cjs → brushstrokeRender-WR2N5HHY.cjs} +22 -22
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-J2TNIZ55.cjs → chunk-2WOGXEDX.cjs} +1 -1
- package/dist/{chunk-JGWVZPT6.js → chunk-3XDSNIGF.js} +72 -5
- package/dist/{chunk-EXB2EGBE.cjs → chunk-62MFUHSI.cjs} +16 -16
- package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
- package/dist/{chunk-EF7LZAXQ.js → chunk-BAZMCFVL.js} +1 -1
- package/dist/{chunk-HIO6E7LC.cjs → chunk-BPJSI4NT.cjs} +2 -2
- package/dist/{chunk-XVOP3CGQ.cjs → chunk-EU2ZZA6L.cjs} +393 -62
- package/dist/{chunk-YIC6MAJ2.js → chunk-HFY5LG5J.js} +1 -1
- package/dist/{chunk-7UKFXXWL.js → chunk-HVO6XCI4.js} +1 -1
- package/dist/{chunk-LHPEG5AU.cjs → chunk-J4GXP53R.cjs} +140 -91
- package/dist/{chunk-OKHKA43L.cjs → chunk-JBB3GDBB.cjs} +330 -216
- package/dist/{chunk-AGNUD5BI.js → chunk-KZI3YWKW.js} +351 -20
- package/dist/{chunk-FGJHJIMN.js → chunk-LDOD7VCT.js} +59 -10
- package/dist/{chunk-EYSHTYLG.js → chunk-LETF75CA.js} +1 -1
- package/dist/{chunk-FQPUF2W2.cjs → chunk-NIBFKXMD.cjs} +217 -35
- package/dist/{chunk-G7NHE2VD.cjs → chunk-NPWWSCWT.cjs} +35 -35
- package/dist/{chunk-OSEXT7JL.js → chunk-QNY7BDFP.js} +1 -1
- package/dist/{chunk-PXDIAYB6.cjs → chunk-STLTOISW.cjs} +34 -214
- package/dist/{chunk-4IDUYB45.js → chunk-UVTO3D4E.js} +130 -16
- package/dist/{chunk-TXFWZ5ZJ.js → chunk-W6JDUXP3.js} +4 -184
- package/dist/{chunk-LA5Y4ETA.cjs → chunk-YOVXQHA3.cjs} +23 -23
- package/dist/{chunk-DT4YQUGT.cjs → chunk-YXP6X3SY.cjs} +101 -34
- package/dist/image-editor/adjustment-layers.d.cts +8 -0
- package/dist/image-editor/adjustment-layers.d.ts +8 -0
- package/dist/image-editor/index.cjs +857 -853
- package/dist/image-editor/index.js +30 -26
- package/dist/paint/index.cjs +5 -5
- package/dist/paint/index.js +11 -11
- package/dist/paint-editor/brush-library-tools.d.cts +12 -2
- package/dist/paint-editor/brush-library-tools.d.ts +12 -2
- package/dist/paint-editor/brush-model.d.cts +29 -1
- package/dist/paint-editor/brush-model.d.ts +29 -1
- package/dist/paint-editor/brush-source-art.d.cts +32 -0
- package/dist/paint-editor/brush-source-art.d.ts +32 -0
- package/dist/paint-editor/controller.d.cts +6 -1
- package/dist/paint-editor/controller.d.ts +6 -1
- package/dist/paint-editor/dab-stroke.d.cts +2 -0
- package/dist/paint-editor/dab-stroke.d.ts +2 -0
- package/dist/paint-editor/engine.d.cts +11 -2
- package/dist/paint-editor/engine.d.ts +11 -2
- package/dist/paint-editor/index.cjs +576 -481
- package/dist/paint-editor/index.d.cts +3 -1
- package/dist/paint-editor/index.d.ts +3 -1
- package/dist/paint-editor/index.js +127 -32
- package/dist/paint-editor/stamp-tip.d.cts +8 -5
- package/dist/paint-editor/stamp-tip.d.ts +8 -5
- package/dist/paint-editor/thumbnails.d.cts +6 -0
- package/dist/paint-editor/thumbnails.d.ts +6 -0
- package/dist/paint-editor/tip-grain.d.cts +34 -0
- package/dist/paint-editor/tip-grain.d.ts +34 -0
- package/dist/raster/index.cjs +2 -2
- package/dist/raster/index.js +9 -9
- package/dist/vector/index.cjs +1002 -1002
- package/dist/vector/index.js +7 -7
- package/package.json +110 -1
- package/skills/pixerate-brushes/SKILL.md +35 -8
- package/skills/pixerate-brushes-design/SKILL.md +8 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +6 -0
- package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
DEFAULT_BRUSH,
|
|
12
12
|
DEFAULT_MIXER,
|
|
13
13
|
DEFAULT_SMUDGE,
|
|
14
|
+
GRAIN_PATTERNS,
|
|
14
15
|
MAX_BRUSH_SIZE,
|
|
15
16
|
PAINT_GRADIENT_JSON_SCHEMA,
|
|
16
17
|
normalizeBlender,
|
|
@@ -18,10 +19,15 @@ import {
|
|
|
18
19
|
normalizeMixer,
|
|
19
20
|
normalizeSmudge,
|
|
20
21
|
readPaintSource
|
|
21
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-ACTHSQOI.js";
|
|
22
23
|
import {
|
|
23
24
|
BLEND_MODES
|
|
24
25
|
} from "./chunk-IRSTHLQQ.js";
|
|
26
|
+
import {
|
|
27
|
+
decodePng,
|
|
28
|
+
parseDataUrl,
|
|
29
|
+
pngDataUrl
|
|
30
|
+
} from "./chunk-OTTT5N3K.js";
|
|
25
31
|
|
|
26
32
|
// src/brushes/presets.ts
|
|
27
33
|
var BRUSH_PRESETS = [
|
|
@@ -908,6 +914,9 @@ var MULTI_COLOR_MODES = ["off", "tip", "dab"];
|
|
|
908
914
|
var DEFAULT_MULTI_COLOR = { mode: "off", colors: [], order: "sequence", length: 12, softness: 1 };
|
|
909
915
|
var DEFAULT_BEHAVIOR = { pickup: 0, mix: 0.5, dirty: false, mixing: "pigment", multicolor: DEFAULT_MULTI_COLOR };
|
|
910
916
|
var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
|
|
917
|
+
var DAB_FLIPS = ["none", "x", "y", "xy"];
|
|
918
|
+
var TIP_GRAIN_MODES = ["moving", "texturised"];
|
|
919
|
+
var DEFAULT_TIP_GRAIN = { mode: "texturised", scale: 1, depth: 0.6, contrast: 1, invert: false };
|
|
911
920
|
var DEFAULT_DYNAMICS = {
|
|
912
921
|
minSize: 0,
|
|
913
922
|
pressureFlow: 0,
|
|
@@ -935,7 +944,8 @@ function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
|
|
|
935
944
|
sizeJitter: unit2(raw.sizeJitter, base.sizeJitter),
|
|
936
945
|
angleJitter: unit2(raw.angleJitter, base.angleJitter),
|
|
937
946
|
opacityJitter: unit2(raw.opacityJitter, base.opacityJitter),
|
|
938
|
-
scatter: range(raw.scatter, 0, 4, base.scatter)
|
|
947
|
+
scatter: range(raw.scatter, 0, 4, base.scatter),
|
|
948
|
+
...countAndFlip(raw, base)
|
|
939
949
|
};
|
|
940
950
|
}
|
|
941
951
|
var BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
@@ -959,6 +969,28 @@ function normalizeBehavior(raw = {}, base = DEFAULT_BEHAVIOR) {
|
|
|
959
969
|
multicolor: normalizeMultiColor(raw.multicolor ?? {}, base.multicolor)
|
|
960
970
|
};
|
|
961
971
|
}
|
|
972
|
+
function countAndFlip(raw, base) {
|
|
973
|
+
const n = typeof raw.count === "number" && Number.isFinite(raw.count) ? Math.min(16, Math.max(1, Math.round(raw.count))) : base.count ?? 1;
|
|
974
|
+
const flip = DAB_FLIPS.includes(raw.flip) ? raw.flip : base.flip ?? "none";
|
|
975
|
+
return { ...n > 1 ? { count: n } : {}, ...flip !== "none" ? { flip } : {} };
|
|
976
|
+
}
|
|
977
|
+
var withTipGrain = (g) => g ? { tipGrain: g } : {};
|
|
978
|
+
function readTipGrain(raw, base) {
|
|
979
|
+
if (raw === null) return null;
|
|
980
|
+
if (!raw || typeof raw !== "object") return base;
|
|
981
|
+
const r = raw;
|
|
982
|
+
const source = typeof r.source === "string" && /^\S{1,200}$/.test(r.source) ? r.source : base?.source;
|
|
983
|
+
if (!source) return base;
|
|
984
|
+
const b = base ?? { source, ...DEFAULT_TIP_GRAIN };
|
|
985
|
+
return {
|
|
986
|
+
source,
|
|
987
|
+
mode: TIP_GRAIN_MODES.includes(r.mode) ? r.mode : b.mode,
|
|
988
|
+
scale: range(r.scale, 0.05, 20, b.scale),
|
|
989
|
+
depth: unit2(r.depth, b.depth),
|
|
990
|
+
contrast: range(r.contrast, 0, 3, b.contrast),
|
|
991
|
+
invert: typeof r.invert === "boolean" ? r.invert : b.invert
|
|
992
|
+
};
|
|
993
|
+
}
|
|
962
994
|
var DEFAULT_SPRAY = { density: 0.5, size: 0.12 };
|
|
963
995
|
function readStamp(raw, base) {
|
|
964
996
|
if (raw === null) return null;
|
|
@@ -1076,6 +1108,7 @@ function normalizePaintBrush(raw = {}, base) {
|
|
|
1076
1108
|
light: normalizeLight(raw.light ?? {}, b.light ?? DEFAULT_LIGHT),
|
|
1077
1109
|
stamp: readStamp(raw.stamp, b.stamp ?? null),
|
|
1078
1110
|
spray: readSpray(raw.spray, b.spray ?? null),
|
|
1111
|
+
...withTipGrain(readTipGrain(raw.tipGrain, b.tipGrain ?? null)),
|
|
1079
1112
|
paint,
|
|
1080
1113
|
...raw.builtin ? { builtin: true } : {}
|
|
1081
1114
|
};
|
|
@@ -1109,6 +1142,7 @@ var BASE_BRUSH = {
|
|
|
1109
1142
|
// src/brushes/resolve.ts
|
|
1110
1143
|
var ALL = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
|
|
1111
1144
|
var EXCHANGE = ["dab", "wet", "impasto"];
|
|
1145
|
+
var DABS = ["dab", "eraser", "mixer", "blender", "smudge"];
|
|
1112
1146
|
var NOT = { toEngine: (v) => !v, toFacet: (v) => !v };
|
|
1113
1147
|
var EDITOR_STAMPS = {
|
|
1114
1148
|
toEngine: (v) => v.flatMap((ref) => {
|
|
@@ -1133,6 +1167,9 @@ var BRUSH_FIELDS = [
|
|
|
1133
1167
|
f("dynamics.pressureSmoothing", "pressureSmoothing"),
|
|
1134
1168
|
f("dynamics.rotation", "rotation"),
|
|
1135
1169
|
...each("dynamics", "dynamics", ["minSize", "pressureFlow", "tiltSize", "tiltRoundness", "tiltOpacity", "velocitySize", "velocityOpacity", "sizeJitter", "angleJitter", "opacityJitter", "scatter"]),
|
|
1170
|
+
f("dynamics.count", "dynamics.count", DABS),
|
|
1171
|
+
f("dynamics.flip", "dynamics.flip", DABS),
|
|
1172
|
+
f("tip.grain", "tipGrain", DABS),
|
|
1136
1173
|
f("behaviour.grain", "grain"),
|
|
1137
1174
|
...each("light", "light", ["amount", "pressure"]),
|
|
1138
1175
|
// Paint exchange: the dab, wet and impasto engines' behaviour, or the Mixer Brush's settings.
|
|
@@ -1355,6 +1392,8 @@ var BRUSH_TOOLS = ["eraser", "blender", "smudge", "water", "dryer", "blow", "blo
|
|
|
1355
1392
|
var BRUSH_TIP_SHAPES = ["round", "flat", "chisel", "bristle", "stamp"];
|
|
1356
1393
|
var BRUSH_MARKS_KINDS = ["continuous", "scatter", "particles", "objects"];
|
|
1357
1394
|
var BRUSH_SURFACES = ["dry", "wet", "thick"];
|
|
1395
|
+
var BRUSH_GRAIN_MODES = ["moving", "texturised"];
|
|
1396
|
+
var BRUSH_FLIPS = ["none", "x", "y", "xy"];
|
|
1358
1397
|
|
|
1359
1398
|
// src/brushes/schema.ts
|
|
1360
1399
|
var MAX_BRUSH_PALETTE = 16;
|
|
@@ -1376,8 +1415,22 @@ var BRUSH_STAMP_PATTERN = `^(?:[^:\\s]+:\\S+|${STAMP_KINDS.join("|")})$`;
|
|
|
1376
1415
|
var STAMP = {
|
|
1377
1416
|
type: "string",
|
|
1378
1417
|
pattern: BRUSH_STAMP_PATTERN,
|
|
1379
|
-
description: `A sticker ref provider:id (find_stickers lists them; the editor's own are ${STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}). A bare editor stamp (star) means editor:star.`
|
|
1418
|
+
description: `A sticker ref provider:id (find_stickers lists them; the editor's own are ${STAMP_KINDS.map((k) => `editor:${k}`).join(", ")}), or an image source source:<id> (list_brush_sources lists them; add_brush_source makes one from an image). A bare editor stamp (star) means editor:star.`
|
|
1380
1419
|
};
|
|
1420
|
+
var TIP_GRAIN_PATTERNS = GRAIN_PATTERNS.filter((p) => p !== "smooth");
|
|
1421
|
+
var BRUSH_GRAIN_SOURCE_PATTERN = `^(?:source:[a-z0-9][a-z0-9-]{0,63}|${TIP_GRAIN_PATTERNS.join("|")})$`;
|
|
1422
|
+
var TIP_GRAIN = object(
|
|
1423
|
+
"Texture inside the tip (a grain source), separate from the paper's grain: each dab is multiplied by it. Continuous and scatter marks of round and stamp tips.",
|
|
1424
|
+
{
|
|
1425
|
+
source: { type: "string", pattern: BRUSH_GRAIN_SOURCE_PATTERN, description: `The texture: a built-in pattern (${TIP_GRAIN_PATTERNS.join(", ")}) or an image source source:<id> (list_brush_sources); light parts are peaks, which paint.` },
|
|
1426
|
+
mode: oneOf2(BRUSH_GRAIN_MODES, "texturised (default): the grain stays put on the canvas, a texture under the paint; moving: it rolls along with the tip, turning with it."),
|
|
1427
|
+
scale: num(0.05, 20, "Grain size: 1 is the pattern's or image's own size, 2 twice as coarse (default 1)."),
|
|
1428
|
+
depth: unit3("How much the grain takes out of each dab: 0 none, 1 only the peaks paint (default 0.6)."),
|
|
1429
|
+
contrast: num(0, 3, "Height contrast around the middle: below 1 softer, above 1 harsher (default 1)."),
|
|
1430
|
+
invert: bool("Swap peaks and valleys (default false).")
|
|
1431
|
+
},
|
|
1432
|
+
["source"]
|
|
1433
|
+
);
|
|
1381
1434
|
var BRISTLES = object("Bristle tips: the tuft. Missing fields take their defaults.", {
|
|
1382
1435
|
count: int(3, 128, "Bristles in the tuft (default 32)."),
|
|
1383
1436
|
stiffness: unit3("Stiff bristles follow the tip; soft ones lag and bend into curves at turns (default 0.5)."),
|
|
@@ -1389,9 +1442,9 @@ var BRISTLES = object("Bristle tips: the tuft. Missing fields take their default
|
|
|
1389
1442
|
var TIP = object(
|
|
1390
1443
|
"The tip: its shape and the dab it lays.",
|
|
1391
1444
|
{
|
|
1392
|
-
shape: oneOf2(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp:
|
|
1445
|
+
shape: oneOf2(BRUSH_TIP_SHAPES, "round, flat, chisel: an ellipse set by roundness and angle (the name is for people); bristle: a tuft of bristles (see bristle); stamp: dabs shaped like a stamp, a sticker or an image source (see stamps), also laid as sprays, objects or particles."),
|
|
1393
1446
|
size: num(1, MAX_BRUSH_SIZE, "Diameter in px (default 20)."),
|
|
1394
|
-
hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1)."),
|
|
1447
|
+
hardness: unit3("Edge hardness, 0 soft .. 1 hard (default 1). Image-source stamps fade towards their edges below 1."),
|
|
1395
1448
|
spacing: num(0.01, 10, "Dab spacing as a fraction of the diameter: 0.05 smooth, 1 dotted (default 0.1)."),
|
|
1396
1449
|
angle: num(-360, 360, "Tip angle in degrees, counter-clockwise; added to the stroke direction or barrel with dynamics.rotation (default 0)."),
|
|
1397
1450
|
roundness: num(0.01, 1, "Roundness: 1 round (default), lower a flat ellipse (pastel, chisel)."),
|
|
@@ -1407,8 +1460,9 @@ var TIP = object(
|
|
|
1407
1460
|
minItems: 1,
|
|
1408
1461
|
maxItems: MAX_STICKER_STAMPS,
|
|
1409
1462
|
items: STAMP,
|
|
1410
|
-
description: `Stamp tips:
|
|
1411
|
-
}
|
|
1463
|
+
description: `Stamp tips: stickers or image sources (source:<id>). Continuous dabs and sprays lay the first; objects take only stickers and pick one at random per mark (default editor:star; repeats count once); particles use the first ${MAX_DOT_TEXTURES} in marks.stampOrder, and repeats weight the order.`
|
|
1464
|
+
},
|
|
1465
|
+
grain: TIP_GRAIN
|
|
1412
1466
|
},
|
|
1413
1467
|
["shape"]
|
|
1414
1468
|
);
|
|
@@ -1478,7 +1532,9 @@ var DYNAMICS = object("Pen feel: pressure, tilt, velocity, jitter, rotation and
|
|
|
1478
1532
|
sizeJitter: unit3("Random size change per dab (default 0)."),
|
|
1479
1533
|
angleJitter: unit3("Random angle per dab, as a fraction of \xB1180\xB0 (default 0)."),
|
|
1480
1534
|
opacityJitter: unit3("Random opacity drop per dab (default 0)."),
|
|
1481
|
-
scatter: num(0, 4, "Random offset across the stroke, in diameters (default 0).")
|
|
1535
|
+
scatter: num(0, 4, "Random offset across the stroke, in diameters (default 0)."),
|
|
1536
|
+
count: int(1, 16, "Dabs laid at each spacing step, each with its own jitter and scatter (default 1). Dab brushes."),
|
|
1537
|
+
flip: oneOf2(BRUSH_FLIPS, "Mirror each dab at random across x, y or both (default none): variety for image and stamp tips.")
|
|
1482
1538
|
});
|
|
1483
1539
|
var EXCHANGE2 = object("Paint exchange with the canvas: picking up paint and mixing it into what the brush lays. Paint brushes on a dry, wet or thick surface and Mixer Brushes.", {
|
|
1484
1540
|
pickup: unit3("Pick-up ratio: how much of the paint under each dab the brush lifts and carries (default 0; with a reservoir 0.5)."),
|
|
@@ -1600,6 +1656,7 @@ var BRUSH_JSON_SCHEMA = object(
|
|
|
1600
1656
|
);
|
|
1601
1657
|
var BRUSH_FACET_SCHEMAS = {
|
|
1602
1658
|
tip: TIP,
|
|
1659
|
+
tipGrain: TIP_GRAIN,
|
|
1603
1660
|
marks: { continuous: CONTINUOUS, scatter: SCATTER, particles: PARTICLES, objects: OBJECTS },
|
|
1604
1661
|
dynamics: DYNAMICS,
|
|
1605
1662
|
behaviour: BEHAVIOUR,
|
|
@@ -1609,6 +1666,139 @@ var BRUSH_FACET_SCHEMAS = {
|
|
|
1609
1666
|
paint: PAINT
|
|
1610
1667
|
};
|
|
1611
1668
|
|
|
1669
|
+
// src/brushes/sources.ts
|
|
1670
|
+
var BRUSH_SOURCE_PROVIDER = "source";
|
|
1671
|
+
var MAX_BRUSH_SOURCE_SIZE = 512;
|
|
1672
|
+
var MAX_BRUSH_SOURCE_INPUT_PIXELS = 4096 * 4096;
|
|
1673
|
+
var MAX_BRUSH_SOURCES = 500;
|
|
1674
|
+
var BRUSH_SOURCE_CHANNELS = ["auto", "alpha", "darkness", "lightness"];
|
|
1675
|
+
var BrushSourceError = class extends Error {
|
|
1676
|
+
constructor(message) {
|
|
1677
|
+
super(message);
|
|
1678
|
+
this.name = "BrushSourceError";
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
function brushSourceRef(id) {
|
|
1682
|
+
return `${BRUSH_SOURCE_PROVIDER}:${id}`;
|
|
1683
|
+
}
|
|
1684
|
+
function isBrushSourceRef(ref) {
|
|
1685
|
+
return typeof ref === "string" && ref.startsWith(`${BRUSH_SOURCE_PROVIDER}:`);
|
|
1686
|
+
}
|
|
1687
|
+
function brushSourceIdOf(ref) {
|
|
1688
|
+
return isBrushSourceRef(ref) ? ref.slice(BRUSH_SOURCE_PROVIDER.length + 1) : null;
|
|
1689
|
+
}
|
|
1690
|
+
function brushSourceIds(brush) {
|
|
1691
|
+
const refs = [...brush.tip.stamps ?? [], ...brush.tip.grain ? [brush.tip.grain.source] : []];
|
|
1692
|
+
return [...new Set(refs.map(brushSourceIdOf).filter((id) => id !== null))];
|
|
1693
|
+
}
|
|
1694
|
+
function summarizeBrushSource(s) {
|
|
1695
|
+
const { image: _i, ...rest } = s;
|
|
1696
|
+
return { ...rest, ref: brushSourceRef(s.id) };
|
|
1697
|
+
}
|
|
1698
|
+
function brushSourceIdFromName(name) {
|
|
1699
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "source";
|
|
1700
|
+
}
|
|
1701
|
+
var text = (v, max) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : void 0;
|
|
1702
|
+
function fit(width, height, mask, max) {
|
|
1703
|
+
const scale = Math.min(1, max / Math.max(width, height));
|
|
1704
|
+
if (scale >= 1) return { width, height, mask: Uint8Array.from(mask) };
|
|
1705
|
+
const w = Math.max(1, Math.round(width * scale));
|
|
1706
|
+
const h = Math.max(1, Math.round(height * scale));
|
|
1707
|
+
const out = new Uint8Array(w * h);
|
|
1708
|
+
for (let y = 0; y < h; y++) {
|
|
1709
|
+
const y0 = Math.floor(y * height / h);
|
|
1710
|
+
const y1 = Math.max(y0 + 1, Math.floor((y + 1) * height / h));
|
|
1711
|
+
for (let x = 0; x < w; x++) {
|
|
1712
|
+
const x0 = Math.floor(x * width / w);
|
|
1713
|
+
const x1 = Math.max(x0 + 1, Math.floor((x + 1) * width / w));
|
|
1714
|
+
let sum = 0;
|
|
1715
|
+
for (let yy = y0; yy < y1; yy++) for (let xx = x0; xx < x1; xx++) sum += mask[yy * width + xx];
|
|
1716
|
+
out[y * w + x] = Math.round(sum / ((y1 - y0) * (x1 - x0)));
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return { width: w, height: h, mask: out };
|
|
1720
|
+
}
|
|
1721
|
+
function brushSourceFromMask(meta, width, height, mask) {
|
|
1722
|
+
if (!BRUSH_ID.test(meta.id)) throw new BrushSourceError(`Source id ${JSON.stringify(meta.id)} is not valid: use lowercase letters, digits and dashes (up to 64).`);
|
|
1723
|
+
if (!(Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0) || mask.length !== width * height) throw new BrushSourceError("The mask is not width \xD7 height bytes.");
|
|
1724
|
+
let any = false;
|
|
1725
|
+
for (let i = 0; i < mask.length && !any; i++) any = mask[i] > 0;
|
|
1726
|
+
if (!any) throw new BrushSourceError(`"${meta.name}" has no marks: every pixel is empty. Pick another channel (alpha, darkness or lightness) or set invert.`);
|
|
1727
|
+
const f2 = fit(width, height, mask, MAX_BRUSH_SOURCE_SIZE);
|
|
1728
|
+
const name = text(meta.name, 80) ?? meta.id;
|
|
1729
|
+
const licence = text(meta.licence, 2e3);
|
|
1730
|
+
const origin = text(meta.origin, 200);
|
|
1731
|
+
return { id: meta.id, name, width: f2.width, height: f2.height, image: pngDataUrl(f2.width, f2.height, f2.mask, 1), ...licence ? { licence } : {}, ...origin ? { origin } : {} };
|
|
1732
|
+
}
|
|
1733
|
+
function makeBrushSource(input, problems = []) {
|
|
1734
|
+
const name = text(input.name, 80);
|
|
1735
|
+
if (!name) throw new BrushSourceError("A brush source needs a name.");
|
|
1736
|
+
const id = input.id ?? brushSourceIdFromName(name);
|
|
1737
|
+
if (!BRUSH_ID.test(id)) throw new BrushSourceError(`Source id ${JSON.stringify(id)} is not valid: use lowercase letters, digits and dashes (up to 64), starting with a letter or digit.`);
|
|
1738
|
+
let bytes;
|
|
1739
|
+
if (typeof input.image === "string") {
|
|
1740
|
+
const parsed = parseDataUrl(input.image);
|
|
1741
|
+
if (!parsed) throw new BrushSourceError("image must be a data: URL (data:image/png;base64,\u2026) or PNG bytes.");
|
|
1742
|
+
bytes = parsed.bytes;
|
|
1743
|
+
} else bytes = input.image;
|
|
1744
|
+
let png;
|
|
1745
|
+
try {
|
|
1746
|
+
png = decodePng(bytes, { maxPixels: MAX_BRUSH_SOURCE_INPUT_PIXELS });
|
|
1747
|
+
} catch (e) {
|
|
1748
|
+
throw new BrushSourceError(`The image can't be read as a PNG (${e.message}); convert it to PNG, at most ${MAX_BRUSH_SOURCE_INPUT_PIXELS} pixels.`);
|
|
1749
|
+
}
|
|
1750
|
+
const { width, height, data } = png;
|
|
1751
|
+
const n = width * height;
|
|
1752
|
+
let channel = input.channel ?? "auto";
|
|
1753
|
+
if (channel === "auto") {
|
|
1754
|
+
let transparent = false;
|
|
1755
|
+
for (let i = 0; i < n && !transparent; i++) transparent = data[i * 4 + 3] < 250;
|
|
1756
|
+
channel = transparent ? "alpha" : "darkness";
|
|
1757
|
+
problems.push(`channel auto read the marks from ${channel === "alpha" ? "the transparency" : "the dark parts (dark marks on a light background)"}.`);
|
|
1758
|
+
}
|
|
1759
|
+
const mask = new Uint8Array(n);
|
|
1760
|
+
for (let i = 0; i < n; i++) {
|
|
1761
|
+
const a = data[i * 4 + 3];
|
|
1762
|
+
const lum = (0.2126 * data[i * 4] + 0.7152 * data[i * 4 + 1] + 0.0722 * data[i * 4 + 2]) * (a / 255) + 255 * (1 - a / 255);
|
|
1763
|
+
const v = channel === "alpha" ? a : channel === "darkness" ? 255 - lum : lum;
|
|
1764
|
+
mask[i] = Math.round(input.invert ? 255 - v : v);
|
|
1765
|
+
}
|
|
1766
|
+
const source = brushSourceFromMask({ id, name, licence: input.licence, origin: input.origin }, width, height, mask);
|
|
1767
|
+
if (source.width !== width) problems.push(`The image was scaled from ${width}\xD7${height} to ${source.width}\xD7${source.height} (sources are kept at most ${MAX_BRUSH_SOURCE_SIZE} px).`);
|
|
1768
|
+
return source;
|
|
1769
|
+
}
|
|
1770
|
+
var GREY_PNG = /^data:image\/png;base64,[A-Za-z0-9+/]+=*$/;
|
|
1771
|
+
function readBrushSource(raw, at, problems) {
|
|
1772
|
+
const r = raw;
|
|
1773
|
+
if (!r || typeof r !== "object" || Array.isArray(r)) return void problems.push(`${at} is not a brush source; skipped.`), null;
|
|
1774
|
+
if (typeof r.id !== "string" || !BRUSH_ID.test(r.id)) return void problems.push(`${at} has no valid id; skipped.`), null;
|
|
1775
|
+
const ok = (v) => Number.isInteger(v) && v >= 1 && v <= MAX_BRUSH_SOURCE_SIZE;
|
|
1776
|
+
if (!ok(r.width) || !ok(r.height)) return void problems.push(`${at} ("${r.id}") has no valid width and height (1..${MAX_BRUSH_SOURCE_SIZE}); skipped.`), null;
|
|
1777
|
+
if (typeof r.image !== "string" || !GREY_PNG.test(r.image)) return void problems.push(`${at} ("${r.id}") has no PNG data: URL image; skipped.`), null;
|
|
1778
|
+
const name = text(r.name, 80) ?? r.id;
|
|
1779
|
+
const licence = text(r.licence, 2e3);
|
|
1780
|
+
const origin = text(r.origin, 200);
|
|
1781
|
+
return { id: r.id, name, width: r.width, height: r.height, image: r.image, ...licence ? { licence } : {}, ...origin ? { origin } : {} };
|
|
1782
|
+
}
|
|
1783
|
+
var decoded = /* @__PURE__ */ new Map();
|
|
1784
|
+
function decodeBrushSource(source) {
|
|
1785
|
+
const hit = decoded.get(source.image);
|
|
1786
|
+
if (hit) return hit;
|
|
1787
|
+
const parsed = parseDataUrl(source.image);
|
|
1788
|
+
if (!parsed) throw new BrushSourceError("The source image is not a data: URL.");
|
|
1789
|
+
const png = decodePng(parsed.bytes, { keepGray: true, maxPixels: MAX_BRUSH_SOURCE_SIZE * MAX_BRUSH_SOURCE_SIZE });
|
|
1790
|
+
let mask;
|
|
1791
|
+
if (png.channels === 1) mask = png.data;
|
|
1792
|
+
else {
|
|
1793
|
+
mask = new Uint8ClampedArray(png.width * png.height);
|
|
1794
|
+
for (let i = 0; i < mask.length; i++) mask[i] = Math.round(png.data[i * 4] * png.data[i * 4 + 3] / 255);
|
|
1795
|
+
}
|
|
1796
|
+
const out = { width: png.width, height: png.height, mask };
|
|
1797
|
+
if (decoded.size > 256) decoded.delete(decoded.keys().next().value);
|
|
1798
|
+
decoded.set(source.image, out);
|
|
1799
|
+
return out;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1612
1802
|
// src/brushes/normalize.ts
|
|
1613
1803
|
var WET_BRUSH_TOOLS = ["water", "dryer", "blow", "blot", "wet-blend", "wet-drag"];
|
|
1614
1804
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -1787,13 +1977,26 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
1787
1977
|
delete brush.tip.stamps;
|
|
1788
1978
|
}
|
|
1789
1979
|
if (brush.tip.stamps) {
|
|
1790
|
-
|
|
1980
|
+
let stamps = brush.tip.stamps.map((s) => normalizeStickerRef(s));
|
|
1981
|
+
if (brush.marks?.kind === "objects" && stamps.some(isBrushSourceRef)) {
|
|
1982
|
+
problems.push("tip.stamps: image sources (source:\u2026) dropped: objects marks lay stickers; use continuous, scatter or particles marks for an image tip.");
|
|
1983
|
+
stamps = stamps.filter((s) => !isBrushSourceRef(s));
|
|
1984
|
+
}
|
|
1791
1985
|
if (stamps.length) brush.tip.stamps = stamps;
|
|
1792
1986
|
else {
|
|
1793
1987
|
drop("tip.stamps", "none of the stamps can be laid");
|
|
1794
1988
|
delete brush.tip.stamps;
|
|
1795
1989
|
}
|
|
1796
1990
|
}
|
|
1991
|
+
if (brush.tip.grain) {
|
|
1992
|
+
const surface = brush.behaviour?.surface;
|
|
1993
|
+
const marks = brush.marks?.kind;
|
|
1994
|
+
const why = !brush.tip.grain.source ? "it needs a source (a grain pattern or source:<id>)" : brush.tip.shape === "bristle" ? "bristle tips lay their own marks" : marks === "particles" || marks === "objects" ? `${marks} marks are not dabs` : surface === "wet" || surface === "thick" ? `${surface} paint lays its own texture` : kind === "tool" && WET_BRUSH_TOOLS.includes(brush.tool) ? "the wet tools work on the wet layer" : null;
|
|
1995
|
+
if (why) {
|
|
1996
|
+
drop("tip.grain", why);
|
|
1997
|
+
delete brush.tip.grain;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1797
2000
|
const b = brush.behaviour;
|
|
1798
2001
|
if (b) {
|
|
1799
2002
|
const wetTool = kind === "tool" && WET_BRUSH_TOOLS.includes(brush.tool);
|
|
@@ -1850,7 +2053,9 @@ var PAINT_PROFILE = {
|
|
|
1850
2053
|
motion: "full",
|
|
1851
2054
|
colours: ALL_COLOURS,
|
|
1852
2055
|
velocity: "full",
|
|
1853
|
-
tilt: "full"
|
|
2056
|
+
tilt: "full",
|
|
2057
|
+
tipGrain: "full",
|
|
2058
|
+
dabCount: "full"
|
|
1854
2059
|
};
|
|
1855
2060
|
var IMAGE_PROFILE = {
|
|
1856
2061
|
id: "image",
|
|
@@ -1867,7 +2072,9 @@ var IMAGE_PROFILE = {
|
|
|
1867
2072
|
motion: "none",
|
|
1868
2073
|
colours: ["solid", "dual", "gradient"],
|
|
1869
2074
|
velocity: "ignored",
|
|
1870
|
-
tilt: "ignored"
|
|
2075
|
+
tilt: "ignored",
|
|
2076
|
+
tipGrain: "ignored",
|
|
2077
|
+
dabCount: "ignored"
|
|
1871
2078
|
};
|
|
1872
2079
|
var CANVAS_PROFILE = {
|
|
1873
2080
|
id: "canvas",
|
|
@@ -1884,7 +2091,9 @@ var CANVAS_PROFILE = {
|
|
|
1884
2091
|
motion: "none",
|
|
1885
2092
|
colours: ["solid", "dual", "gradient"],
|
|
1886
2093
|
velocity: "ignored",
|
|
1887
|
-
tilt: "full"
|
|
2094
|
+
tilt: "full",
|
|
2095
|
+
tipGrain: "ignored",
|
|
2096
|
+
dabCount: "ignored"
|
|
1888
2097
|
};
|
|
1889
2098
|
var VECTOR_PROFILE = { ...CANVAS_PROFILE, id: "vector", name: "Vector editor", kinds: ["mark", "tool"], tools: ["blender", "smudge"], surfaces: ["dry", "wet"], reservoir: "full", light: "ignored" };
|
|
1890
2099
|
var BRUSH_HOST_PROFILES = { paint: PAINT_PROFILE, image: IMAGE_PROFILE, canvas: CANVAS_PROFILE, vector: VECTOR_PROFILE };
|
|
@@ -1908,7 +2117,10 @@ function checkBrush(brush, profile) {
|
|
|
1908
2117
|
else if (brush.kind === "tool" && brush.tool && !profile.tools.includes(brush.tool)) {
|
|
1909
2118
|
add("tool", "error", `${host} has no ${brush.tool} tool.`, profile.tools.length ? `Use one of: ${list(profile.tools)}.` : `${host} has no tools; use a mark brush.`);
|
|
1910
2119
|
}
|
|
1911
|
-
if (!profile.tipShapes.includes(brush.tip.shape))
|
|
2120
|
+
if (!profile.tipShapes.includes(brush.tip.shape)) {
|
|
2121
|
+
const image = brush.tip.shape === "stamp" && (brush.tip.stamps ?? []).some(isBrushSourceRef);
|
|
2122
|
+
add("tip.shape", "error", `${host} can't lay a ${image ? "stamp tip (image tips are stamps)" : `${brush.tip.shape} tip`}.`, `Set tip.shape to one of: ${list(profile.tipShapes)}.`);
|
|
2123
|
+
}
|
|
1912
2124
|
const marks = brush.marks?.kind ?? "continuous";
|
|
1913
2125
|
if (!profile.marks.includes(marks)) add("marks", "error", `${host} can't lay ${marks} marks.`, `Use marks ${list(profile.marks)} (remove marks for continuous dabs).`);
|
|
1914
2126
|
const surface = brush.behaviour?.surface ?? "dry";
|
|
@@ -1921,6 +2133,8 @@ function checkBrush(brush, profile) {
|
|
|
1921
2133
|
const m = brush.motion;
|
|
1922
2134
|
feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
|
|
1923
2135
|
const d = brush.dynamics ?? {};
|
|
2136
|
+
feature(profile.tipGrain ?? "none", !!brush.tip.grain, "tip.grain", "tip grain", "Remove tip.grain (behaviour.grain textures with the paper instead).");
|
|
2137
|
+
feature(profile.dabCount ?? "none", (d.count ?? 1) > 1, "dynamics.count", "several dabs per step", "Set dynamics.count to 1.");
|
|
1924
2138
|
feature(profile.velocity, nonZero(d.velocitySize) || nonZero(d.velocityOpacity), "dynamics.velocity", "velocity dynamics (its strokes carry no timing)", "Set dynamics.velocitySize and velocityOpacity to 0.");
|
|
1925
2139
|
feature(profile.tilt, nonZero(d.tiltSize) || nonZero(d.tiltRoundness) || nonZero(d.tiltOpacity), "dynamics.tilt", "tilt dynamics", "Set dynamics.tiltSize, tiltRoundness and tiltOpacity to 0.");
|
|
1926
2140
|
const colour = brush.colour;
|
|
@@ -1976,6 +2190,12 @@ function degrade(brush, profile, options = {}) {
|
|
|
1976
2190
|
case "behaviour.grain":
|
|
1977
2191
|
delete b.behaviour.grain;
|
|
1978
2192
|
break;
|
|
2193
|
+
case "tip.grain":
|
|
2194
|
+
delete b.tip.grain;
|
|
2195
|
+
break;
|
|
2196
|
+
case "dynamics.count":
|
|
2197
|
+
delete b.dynamics.count;
|
|
2198
|
+
break;
|
|
1979
2199
|
case "light":
|
|
1980
2200
|
delete b.light;
|
|
1981
2201
|
break;
|
|
@@ -2024,6 +2244,15 @@ function isBrushLibrary(library) {
|
|
|
2024
2244
|
}
|
|
2025
2245
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2026
2246
|
var isV1 = (item) => "engine" in item || "category" in item || !("kind" in item);
|
|
2247
|
+
function parseBrushLibrarySources(raw, problems = []) {
|
|
2248
|
+
const file = isObject2(raw) ? raw : null;
|
|
2249
|
+
if (!file || file.sources === void 0) return [];
|
|
2250
|
+
if (!Array.isArray(file.sources)) {
|
|
2251
|
+
problems.push("sources is not a list; skipped.");
|
|
2252
|
+
return [];
|
|
2253
|
+
}
|
|
2254
|
+
return file.sources.map((s, i) => readBrushSource(s, `sources[${i}]`, problems)).filter((s) => s !== null);
|
|
2255
|
+
}
|
|
2027
2256
|
function parseBrushLibrary2(raw, problems = []) {
|
|
2028
2257
|
if (typeof raw === "string") {
|
|
2029
2258
|
try {
|
|
@@ -2066,8 +2295,8 @@ function matches(entry, f2) {
|
|
|
2066
2295
|
if (f2.tool && b.tool !== f2.tool) return false;
|
|
2067
2296
|
if (f2.tag && !(b.tags ?? []).includes(f2.tag)) return false;
|
|
2068
2297
|
if (f2.text) {
|
|
2069
|
-
const
|
|
2070
|
-
if (!words(f2.text).every((w) =>
|
|
2298
|
+
const text2 = words([b.id, b.name, b.description ?? "", b.variety ?? "", b.medium ?? "", b.tool ?? "", ...b.tags ?? []].join(" "));
|
|
2299
|
+
if (!words(f2.text).every((w) => text2.some((t) => t.startsWith(w)))) return false;
|
|
2071
2300
|
}
|
|
2072
2301
|
if (f2.profile && !checkBrush(b, f2.profile).ok) return false;
|
|
2073
2302
|
return true;
|
|
@@ -2077,8 +2306,10 @@ function createBrushLibrary(options = {}) {
|
|
|
2077
2306
|
const presets = (options.presets ?? BRUSH_PRESETS).map(copy3);
|
|
2078
2307
|
const builtinIds = new Set(presets.map((b) => b.id));
|
|
2079
2308
|
const key = `${options.namespace ?? "pixerate.brushes"}:user`;
|
|
2309
|
+
const sourcesKey = `${options.namespace ?? "pixerate.brushes"}:sources`;
|
|
2080
2310
|
const listeners = /* @__PURE__ */ new Set();
|
|
2081
2311
|
let user = null;
|
|
2312
|
+
let sources = null;
|
|
2082
2313
|
let queue = Promise.resolve();
|
|
2083
2314
|
const run = (task) => {
|
|
2084
2315
|
const result = queue.then(task, task);
|
|
@@ -2104,6 +2335,44 @@ function createBrushLibrary(options = {}) {
|
|
|
2104
2335
|
if (options.storage) await options.storage.set(key, JSON.stringify(file));
|
|
2105
2336
|
user = brushes;
|
|
2106
2337
|
};
|
|
2338
|
+
const loadSources = async () => {
|
|
2339
|
+
if (sources) return sources;
|
|
2340
|
+
const raw = options.storage ? await options.storage.get(sourcesKey) : null;
|
|
2341
|
+
let list2 = [];
|
|
2342
|
+
if (raw) {
|
|
2343
|
+
try {
|
|
2344
|
+
list2 = parseBrushLibrarySources(JSON.parse(raw));
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
sources = list2;
|
|
2349
|
+
return sources;
|
|
2350
|
+
};
|
|
2351
|
+
const storeSources = async (list2) => {
|
|
2352
|
+
if (options.storage) await options.storage.set(sourcesKey, JSON.stringify({ format: "pixerate-brush-sources", version: 1, sources: list2 }));
|
|
2353
|
+
sources = list2;
|
|
2354
|
+
};
|
|
2355
|
+
const putSources = async (incoming, overwrite, problems) => {
|
|
2356
|
+
let list2 = [...await loadSources()];
|
|
2357
|
+
const changed = [];
|
|
2358
|
+
for (const src of incoming) {
|
|
2359
|
+
const at = list2.findIndex((x) => x.id === src.id);
|
|
2360
|
+
if (at === -1) {
|
|
2361
|
+
if (list2.length >= MAX_BRUSH_SOURCES) {
|
|
2362
|
+
problems.push(`Source "${src.id}" skipped: the library holds at most ${MAX_BRUSH_SOURCES} sources.`);
|
|
2363
|
+
continue;
|
|
2364
|
+
}
|
|
2365
|
+
list2.push(src);
|
|
2366
|
+
changed.push(src.id);
|
|
2367
|
+
} else if (list2[at].image === src.image) continue;
|
|
2368
|
+
else if (overwrite) {
|
|
2369
|
+
list2 = list2.map((x, i) => i === at ? src : x);
|
|
2370
|
+
changed.push(src.id);
|
|
2371
|
+
} else problems.push(`Source "${src.id}" exists already with another image; skipped (pass overwrite to replace it).`);
|
|
2372
|
+
}
|
|
2373
|
+
if (changed.length) await storeSources(list2);
|
|
2374
|
+
return changed;
|
|
2375
|
+
};
|
|
2107
2376
|
const entries = async () => [...presets.map((brush) => ({ brush, builtin: true })), ...(await load()).map((brush) => ({ brush, builtin: false }))];
|
|
2108
2377
|
const find = async (id) => {
|
|
2109
2378
|
const entry = (await entries()).find((e) => e.brush.id === id);
|
|
@@ -2165,12 +2434,17 @@ function createBrushLibrary(options = {}) {
|
|
|
2165
2434
|
}),
|
|
2166
2435
|
exportFile: (ids) => run(async () => {
|
|
2167
2436
|
const brushes = ids ? await Promise.all(ids.map(async (id) => (await find(id)).brush)) : await load();
|
|
2168
|
-
|
|
2437
|
+
const used = new Set(brushes.flatMap(brushSourceIds));
|
|
2438
|
+
const withSources = (await loadSources()).filter((s) => used.has(s.id));
|
|
2439
|
+
return { format: "pixerate-brushes", version: 2, brushes: brushes.map(copy3), ...withSources.length ? { sources: withSources.map(copy3) } : {} };
|
|
2169
2440
|
}),
|
|
2170
2441
|
importFile: (raw, o = {}) => run(async () => {
|
|
2171
2442
|
const problems = [];
|
|
2172
2443
|
const incoming = parseBrushLibrary2(raw, problems);
|
|
2173
2444
|
const result = { added: [], replaced: [], skipped: [], problems };
|
|
2445
|
+
const fileSources = parseBrushLibrarySources(typeof raw === "string" ? JSON.parse(raw) : raw, problems);
|
|
2446
|
+
const changedSources = fileSources.length ? await putSources(fileSources, !!o.overwrite, problems) : [];
|
|
2447
|
+
if (changedSources.length) result.sources = changedSources;
|
|
2174
2448
|
let brushes = [...await load()];
|
|
2175
2449
|
for (const brush of incoming) {
|
|
2176
2450
|
if (builtinIds.has(brush.id)) {
|
|
@@ -2190,12 +2464,45 @@ function createBrushLibrary(options = {}) {
|
|
|
2190
2464
|
problems.push(`"${brush.id}" exists already; skipped (pass overwrite to replace it).`);
|
|
2191
2465
|
}
|
|
2192
2466
|
}
|
|
2193
|
-
if (result.added.length || result.replaced.length)
|
|
2194
|
-
|
|
2195
|
-
notify();
|
|
2196
|
-
}
|
|
2467
|
+
if (result.added.length || result.replaced.length) await store(brushes);
|
|
2468
|
+
if (result.added.length || result.replaced.length || changedSources.length) notify();
|
|
2197
2469
|
return result;
|
|
2198
2470
|
}),
|
|
2471
|
+
listSources: () => run(async () => (await loadSources()).map(copy3)),
|
|
2472
|
+
getSource: (id) => run(async () => {
|
|
2473
|
+
const s = (await loadSources()).find((x) => x.id === id);
|
|
2474
|
+
return s ? copy3(s) : null;
|
|
2475
|
+
}),
|
|
2476
|
+
addSource: (input, o = {}) => run(async () => {
|
|
2477
|
+
const problems = [];
|
|
2478
|
+
let source;
|
|
2479
|
+
if (isObject2(input) && "width" in input) {
|
|
2480
|
+
const read2 = readBrushSource(input, "source", problems);
|
|
2481
|
+
if (!read2) throw new BrushLibraryError("invalid", problems.join(" "));
|
|
2482
|
+
source = read2;
|
|
2483
|
+
} else {
|
|
2484
|
+
const given = input;
|
|
2485
|
+
const taken = new Set((await loadSources()).map((x) => x.id));
|
|
2486
|
+
let id = given.id;
|
|
2487
|
+
if (id === void 0 && typeof given.name === "string") {
|
|
2488
|
+
const stem = brushSourceIdFromName(given.name);
|
|
2489
|
+
id = stem;
|
|
2490
|
+
for (let n = 2; taken.has(id); n++) id = `${stem}-${n}`;
|
|
2491
|
+
}
|
|
2492
|
+
try {
|
|
2493
|
+
source = makeBrushSource({ ...given, id }, problems);
|
|
2494
|
+
} catch (e) {
|
|
2495
|
+
throw new BrushLibraryError("invalid", e.message);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
const list2 = await loadSources();
|
|
2499
|
+
const at = list2.findIndex((x) => x.id === source.id);
|
|
2500
|
+
if (at !== -1 && !o.overwrite) throw new BrushLibraryError("exists", `A source "${source.id}" exists already; pick another id, or pass overwrite to replace its image (brushes using it change too).`);
|
|
2501
|
+
if (at === -1 && list2.length >= MAX_BRUSH_SOURCES) throw new BrushLibraryError("invalid", `The library holds at most ${MAX_BRUSH_SOURCES} sources.`);
|
|
2502
|
+
await storeSources(at === -1 ? [...list2, source] : list2.map((x, i) => i === at ? source : x));
|
|
2503
|
+
notify();
|
|
2504
|
+
return { source: copy3(source), problems, replaced: at !== -1 };
|
|
2505
|
+
}),
|
|
2199
2506
|
subscribe(listener) {
|
|
2200
2507
|
listeners.add(listener);
|
|
2201
2508
|
return () => {
|
|
@@ -2274,6 +2581,9 @@ export {
|
|
|
2274
2581
|
DEFAULT_MULTI_COLOR,
|
|
2275
2582
|
DEFAULT_BEHAVIOR,
|
|
2276
2583
|
BRUSH_ROTATIONS,
|
|
2584
|
+
DAB_FLIPS,
|
|
2585
|
+
TIP_GRAIN_MODES,
|
|
2586
|
+
DEFAULT_TIP_GRAIN,
|
|
2277
2587
|
DEFAULT_DYNAMICS,
|
|
2278
2588
|
normalizeDynamics,
|
|
2279
2589
|
BRUSH_ID,
|
|
@@ -2305,10 +2615,30 @@ export {
|
|
|
2305
2615
|
BRUSH_TIP_SHAPES,
|
|
2306
2616
|
BRUSH_MARKS_KINDS,
|
|
2307
2617
|
BRUSH_SURFACES,
|
|
2618
|
+
BRUSH_GRAIN_MODES,
|
|
2619
|
+
BRUSH_FLIPS,
|
|
2308
2620
|
MAX_BRUSH_PALETTE,
|
|
2309
2621
|
BRUSH_WORD,
|
|
2622
|
+
TIP_GRAIN_PATTERNS,
|
|
2623
|
+
BRUSH_GRAIN_SOURCE_PATTERN,
|
|
2310
2624
|
BRUSH_JSON_SCHEMA,
|
|
2311
2625
|
BRUSH_FACET_SCHEMAS,
|
|
2626
|
+
BRUSH_SOURCE_PROVIDER,
|
|
2627
|
+
MAX_BRUSH_SOURCE_SIZE,
|
|
2628
|
+
MAX_BRUSH_SOURCE_INPUT_PIXELS,
|
|
2629
|
+
MAX_BRUSH_SOURCES,
|
|
2630
|
+
BRUSH_SOURCE_CHANNELS,
|
|
2631
|
+
BrushSourceError,
|
|
2632
|
+
brushSourceRef,
|
|
2633
|
+
isBrushSourceRef,
|
|
2634
|
+
brushSourceIdOf,
|
|
2635
|
+
brushSourceIds,
|
|
2636
|
+
summarizeBrushSource,
|
|
2637
|
+
brushSourceIdFromName,
|
|
2638
|
+
brushSourceFromMask,
|
|
2639
|
+
makeBrushSource,
|
|
2640
|
+
readBrushSource,
|
|
2641
|
+
decodeBrushSource,
|
|
2312
2642
|
WET_BRUSH_TOOLS,
|
|
2313
2643
|
normalizeBrushSpec,
|
|
2314
2644
|
PAINT_PROFILE,
|
|
@@ -2324,6 +2654,7 @@ export {
|
|
|
2324
2654
|
degrade,
|
|
2325
2655
|
BrushLibraryError,
|
|
2326
2656
|
isBrushLibrary,
|
|
2657
|
+
parseBrushLibrarySources,
|
|
2327
2658
|
parseBrushLibrary2,
|
|
2328
2659
|
createBrushLibrary,
|
|
2329
2660
|
closest,
|