@pixerate/editor 0.275.1 → 0.277.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/animation-editor/index.cjs +62 -62
- package/dist/animation-editor/index.js +6 -6
- package/dist/brushes/index.cjs +84 -71
- package/dist/brushes/index.js +20 -7
- package/dist/brushes/patch.d.cts +4 -4
- package/dist/brushes/patch.d.ts +4 -4
- package/dist/brushes/profiles.d.cts +4 -0
- package/dist/brushes/profiles.d.ts +4 -0
- package/dist/brushes/schema.d.cts +2 -0
- package/dist/brushes/schema.d.ts +2 -0
- package/dist/brushes/types.d.cts +32 -1
- package/dist/brushes/types.d.ts +32 -1
- package/dist/{brushstrokeRender-2RQL46WB.cjs → brushstrokeRender-7HOMKZNO.cjs} +21 -21
- package/dist/{brushstrokeRender-WGY4DZHF.js → brushstrokeRender-EIK3625T.js} +5 -5
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-T6KNJVBP.cjs → chunk-4NZTK7LM.cjs} +35 -35
- package/dist/{chunk-LMG57L5A.js → chunk-CTHWXW5M.js} +121 -8
- package/dist/{chunk-43XJC3PP.js → chunk-DWPFGWW5.js} +1 -1
- package/dist/{chunk-MSWLJI4G.js → chunk-FNIWHSLI.js} +2 -2
- package/dist/{chunk-JDXWLCXF.cjs → chunk-H3EME3S5.cjs} +2 -2
- package/dist/{chunk-Y2ISSVPH.js → chunk-HHX7ST6Z.js} +167 -9
- package/dist/{chunk-6QNDGMK7.cjs → chunk-HRUK5TFW.cjs} +187 -45
- package/dist/{chunk-NIBFKXMD.cjs → chunk-IFWLZZCW.cjs} +619 -2
- package/dist/{chunk-W6JDUXP3.js → chunk-J23JOHPR.js} +1 -1
- package/dist/{chunk-HVO6XCI4.js → chunk-JPB65KQ6.js} +1 -1
- package/dist/{chunk-Y354ETAE.cjs → chunk-JVFGK3DK.cjs} +69 -58
- package/dist/{chunk-ACTHSQOI.js → chunk-LDZOXESA.js} +618 -1
- package/dist/{chunk-IEEY5RJV.js → chunk-LZE2QZCT.js} +1 -1
- package/dist/{chunk-DHC4E2DU.cjs → chunk-MITQGHN3.cjs} +121 -8
- package/dist/{chunk-75SYKUOT.js → chunk-N4MCWQZ7.js} +1 -1
- package/dist/{chunk-G5TCMKX5.js → chunk-NJLPW4MM.js} +17 -6
- package/dist/{chunk-STLTOISW.cjs → chunk-NPPMKMCU.cjs} +17 -17
- package/dist/{chunk-OLZXXAER.cjs → chunk-PEQWARCP.cjs} +408 -250
- package/dist/{chunk-ZM7IKQ4X.js → chunk-PPDUCZZV.js} +152 -10
- package/dist/{chunk-RAPNRK4R.cjs → chunk-QBZEC2JJ.cjs} +88 -88
- package/dist/{chunk-RJGW3RQS.js → chunk-ROURHJYT.js} +1 -1
- package/dist/{chunk-62MFUHSI.cjs → chunk-UQXULAMQ.cjs} +16 -16
- package/dist/{chunk-YYZC2EQO.cjs → chunk-VNF7A2JA.cjs} +23 -23
- package/dist/{chunk-C6VB7CPY.cjs → chunk-WFSYZO5Z.cjs} +1 -1
- package/dist/image-editor/index.cjs +880 -852
- package/dist/image-editor/index.js +36 -8
- package/dist/paint/index.cjs +33 -5
- package/dist/paint/index.d.cts +1 -0
- package/dist/paint/index.d.ts +1 -0
- package/dist/paint/index.js +32 -4
- package/dist/paint/live-patterns.d.cts +149 -0
- package/dist/paint/live-patterns.d.ts +149 -0
- package/dist/paint/wet/world.d.cts +13 -0
- package/dist/paint/wet/world.d.ts +13 -0
- package/dist/paint-editor/brush-model.d.cts +21 -1
- package/dist/paint-editor/brush-model.d.ts +21 -1
- package/dist/paint-editor/controller.d.cts +102 -1
- package/dist/paint-editor/controller.d.ts +102 -1
- package/dist/paint-editor/document.d.cts +8 -1
- package/dist/paint-editor/document.d.ts +8 -1
- package/dist/paint-editor/ferro-tools.d.cts +10 -0
- package/dist/paint-editor/ferro-tools.d.ts +10 -0
- package/dist/paint-editor/ferro.d.cts +134 -0
- package/dist/paint-editor/ferro.d.ts +134 -0
- package/dist/paint-editor/index.cjs +2291 -904
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +1533 -146
- package/dist/paint-editor/stroke.d.cts +14 -0
- package/dist/paint-editor/stroke.d.ts +14 -0
- package/dist/paint-editor/thick.d.cts +15 -2
- package/dist/paint-editor/thick.d.ts +15 -2
- package/dist/paint-editor/ui.d.cts +7 -1
- package/dist/paint-editor/ui.d.ts +7 -1
- package/dist/paint-editor/wet-tools.d.cts +42 -0
- package/dist/paint-editor/wet-tools.d.ts +42 -0
- package/dist/paint-editor/wet.d.cts +22 -1
- package/dist/paint-editor/wet.d.ts +22 -1
- package/dist/vector/index.cjs +1002 -1002
- package/dist/vector/index.js +7 -7
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -0
- package/skills/pixerate-paint-editor/SKILL.md +11 -29
- package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
- package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
|
@@ -9,17 +9,21 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_BLENDER,
|
|
11
11
|
DEFAULT_BRUSH,
|
|
12
|
+
DEFAULT_LIVE_PATTERNS,
|
|
12
13
|
DEFAULT_MIXER,
|
|
13
14
|
DEFAULT_SMUDGE,
|
|
14
15
|
GRAIN_PATTERNS,
|
|
16
|
+
LIVE_PATTERN_KINDS,
|
|
17
|
+
LIVE_PATTERN_PRESETS,
|
|
15
18
|
MAX_BRUSH_SIZE,
|
|
16
19
|
PAINT_GRADIENT_JSON_SCHEMA,
|
|
17
20
|
normalizeBlender,
|
|
18
21
|
normalizeBrush,
|
|
22
|
+
normalizeLivePatterns,
|
|
19
23
|
normalizeMixer,
|
|
20
24
|
normalizeSmudge,
|
|
21
25
|
readPaintSource
|
|
22
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-LDZOXESA.js";
|
|
23
27
|
import {
|
|
24
28
|
BLEND_MODES
|
|
25
29
|
} from "./chunk-IRSTHLQQ.js";
|
|
@@ -278,6 +282,29 @@ var BRUSH_PRESETS = [
|
|
|
278
282
|
behaviour: { surface: "wet", exchange: { pickup: 0.4, mix: 0.4 }, wet: { water: 0.8, pigment: 0.45, rewet: 0.2 } },
|
|
279
283
|
colour: { default: { type: "gradient", stops: [{ offset: 0, color: "paint" }, { offset: 1, color: "secondary" }], space: "pigment", map: { by: "distance", length: 500, repeat: "mirror", unit: "px" } } }
|
|
280
284
|
},
|
|
285
|
+
// Live patterns: Gray-Scott reaction-diffusion that grows inside the wet wash and freezes as it dries.
|
|
286
|
+
{
|
|
287
|
+
id: "turing-wash",
|
|
288
|
+
name: "Turing wash",
|
|
289
|
+
kind: "mark",
|
|
290
|
+
medium: "paint",
|
|
291
|
+
variety: "watercolor",
|
|
292
|
+
tip: { shape: "round", size: 36, hardness: 0.5, spacing: 0.12 },
|
|
293
|
+
dynamics: { smoothing: 0.25, pressureOpacity: true, minSize: 0.35 },
|
|
294
|
+
behaviour: { surface: "wet", exchange: { pickup: 0.45 }, wet: { water: 0.75, pigment: 0.55, rewet: 0.2 } },
|
|
295
|
+
livePatterns: { pattern: "spots", contrast: 1.2, secondColor: "#0ea5e9", relief: 0.25 }
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "coral-ink",
|
|
299
|
+
name: "Coral ink",
|
|
300
|
+
kind: "mark",
|
|
301
|
+
medium: "ink",
|
|
302
|
+
variety: "watercolor",
|
|
303
|
+
tip: { shape: "round", size: 22, hardness: 0.65, spacing: 0.12 },
|
|
304
|
+
dynamics: { smoothing: 0.3, pressureOpacity: true, minSize: 0.3 },
|
|
305
|
+
behaviour: { surface: "wet", exchange: { pickup: 0.35 }, wet: { water: 0.7, pigment: 0.75, rewet: 0.15 } },
|
|
306
|
+
livePatterns: { pattern: "coral", scale: 1.2, contrast: 1.3 }
|
|
307
|
+
},
|
|
281
308
|
{
|
|
282
309
|
id: "watercolor-detail",
|
|
283
310
|
name: "Watercolour rigger",
|
|
@@ -905,6 +932,29 @@ var BRUSH_PRESETS = [
|
|
|
905
932
|
tip: { shape: "round", size: 6, hardness: 0.9, spacing: 0.06 },
|
|
906
933
|
dynamics: { smoothing: 0.35, pressureSmoothing: 0.25, minSize: 0.25 },
|
|
907
934
|
mutation: {}
|
|
935
|
+
},
|
|
936
|
+
// --- Ferrofluid paint: liquid magnetic paint that rises into lit Rosensweig spikes near magnets ---
|
|
937
|
+
{
|
|
938
|
+
id: "ferrofluid",
|
|
939
|
+
name: "Ferrofluid",
|
|
940
|
+
kind: "mark",
|
|
941
|
+
medium: "paint",
|
|
942
|
+
variety: "ferrofluid",
|
|
943
|
+
tip: { shape: "round", size: 28, hardness: 0.75, flow: 0.9 },
|
|
944
|
+
dynamics: { smoothing: 0.35 },
|
|
945
|
+
ferro: { amount: 0.85 },
|
|
946
|
+
colour: { default: "#111827", palette: ["#111827", "#1e293b", "#312e81", "#0f3d3e", "#3b0764", "#451a03"] }
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
id: "ferro-ink",
|
|
950
|
+
name: "Ferro ink",
|
|
951
|
+
kind: "mark",
|
|
952
|
+
medium: "ink",
|
|
953
|
+
variety: "ferrofluid",
|
|
954
|
+
tip: { shape: "round", size: 12, spacing: 0.08 },
|
|
955
|
+
dynamics: { smoothing: 0.45, minSize: 0.25, velocitySize: -0.25 },
|
|
956
|
+
ferro: { amount: 0.7, wavelength: 12, threshold: 0.15 },
|
|
957
|
+
colour: { default: "#0f172a" }
|
|
908
958
|
}
|
|
909
959
|
];
|
|
910
960
|
|
|
@@ -1074,6 +1124,18 @@ function normalizeMutation(raw = {}, base = DEFAULT_MUTATION) {
|
|
|
1074
1124
|
fade: range(raw.fade, 0.5, 10, base.fade)
|
|
1075
1125
|
};
|
|
1076
1126
|
}
|
|
1127
|
+
var DEFAULT_FERRO = { amount: 0, wavelength: 16, threshold: 0.12 };
|
|
1128
|
+
function normalizeFerro(raw = {}, base = DEFAULT_FERRO) {
|
|
1129
|
+
if (typeof raw === "number") {
|
|
1130
|
+
return { amount: unit2(raw, base.amount), wavelength: base.wavelength, threshold: base.threshold };
|
|
1131
|
+
}
|
|
1132
|
+
const r = raw ?? {};
|
|
1133
|
+
return {
|
|
1134
|
+
amount: unit2(r.amount, base.amount),
|
|
1135
|
+
wavelength: range(r.wavelength, 4, 64, base.wavelength),
|
|
1136
|
+
threshold: range(r.threshold, 0.01, 0.95, base.threshold)
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1077
1139
|
function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
|
|
1078
1140
|
return {
|
|
1079
1141
|
tool: IMPASTO_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
|
|
@@ -1152,6 +1214,12 @@ function countAndFlip(raw, base) {
|
|
|
1152
1214
|
return { ...n > 1 ? { count: n } : {}, ...flip !== "none" ? { flip } : {} };
|
|
1153
1215
|
}
|
|
1154
1216
|
var withTipGrain = (g) => g ? { tipGrain: g } : {};
|
|
1217
|
+
var withLivePatterns = (lp) => lp ? { livePatterns: lp } : {};
|
|
1218
|
+
function readLivePatterns(raw, base) {
|
|
1219
|
+
if (raw === null) return null;
|
|
1220
|
+
if (!raw || typeof raw !== "object") return base;
|
|
1221
|
+
return normalizeLivePatterns(raw, base ?? DEFAULT_LIVE_PATTERNS);
|
|
1222
|
+
}
|
|
1155
1223
|
function readTipGrain(raw, base) {
|
|
1156
1224
|
if (raw === null) return null;
|
|
1157
1225
|
if (!raw || typeof raw !== "object") return base;
|
|
@@ -1296,9 +1364,11 @@ function normalizePaintBrush(raw = {}, base) {
|
|
|
1296
1364
|
...withIridescence(readIridescence(raw.iridescence, b.iridescence ?? null)),
|
|
1297
1365
|
...ghost ? { ghost } : {},
|
|
1298
1366
|
...mutation ? { mutation } : {},
|
|
1367
|
+
ferro: normalizeFerro(raw.ferro, b.ferro ?? DEFAULT_FERRO),
|
|
1299
1368
|
stamp: readStamp(raw.stamp, b.stamp ?? null),
|
|
1300
1369
|
spray: readSpray(raw.spray, b.spray ?? null),
|
|
1301
1370
|
...withTipGrain(readTipGrain(raw.tipGrain, b.tipGrain ?? null)),
|
|
1371
|
+
...withLivePatterns(readLivePatterns(raw.livePatterns, b.livePatterns ?? null)),
|
|
1302
1372
|
paint,
|
|
1303
1373
|
...raw.builtin ? { builtin: true } : {}
|
|
1304
1374
|
};
|
|
@@ -1326,6 +1396,7 @@ var BASE_BRUSH = {
|
|
|
1326
1396
|
light: { ...DEFAULT_LIGHT },
|
|
1327
1397
|
fluorescence: { ...DEFAULT_FLUORESCENCE },
|
|
1328
1398
|
thermochromic: { ...DEFAULT_THERMOCHROMIC },
|
|
1399
|
+
ferro: { ...DEFAULT_FERRO },
|
|
1329
1400
|
stamp: null,
|
|
1330
1401
|
spray: null,
|
|
1331
1402
|
paint: null
|
|
@@ -1352,6 +1423,23 @@ var ONE_STAMP = {
|
|
|
1352
1423
|
toEngine: (v) => v[0] ?? null,
|
|
1353
1424
|
toFacet: (v) => v ? [v] : void 0
|
|
1354
1425
|
};
|
|
1426
|
+
var LIVE_PATTERNS_MAP = {
|
|
1427
|
+
toEngine: (v) => v !== null && typeof v === "object" ? JSON.parse(JSON.stringify(v)) : v,
|
|
1428
|
+
toFacet: (v) => {
|
|
1429
|
+
if (!v || typeof v !== "object") return void 0;
|
|
1430
|
+
const lp = v;
|
|
1431
|
+
const preset = LIVE_PATTERN_PRESETS[lp.pattern] ?? LIVE_PATTERN_PRESETS.spots;
|
|
1432
|
+
return {
|
|
1433
|
+
pattern: lp.pattern,
|
|
1434
|
+
...lp.feed !== preset.feed ? { feed: lp.feed } : {},
|
|
1435
|
+
...lp.kill !== preset.kill ? { kill: lp.kill } : {},
|
|
1436
|
+
...lp.scale !== DEFAULT_LIVE_PATTERNS.scale ? { scale: lp.scale } : {},
|
|
1437
|
+
...lp.contrast !== DEFAULT_LIVE_PATTERNS.contrast ? { contrast: lp.contrast } : {},
|
|
1438
|
+
...lp.secondColor !== null ? { secondColor: lp.secondColor } : {},
|
|
1439
|
+
...lp.relief !== DEFAULT_LIVE_PATTERNS.relief ? { relief: lp.relief } : {}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1355
1443
|
var f = (facet, engine, engines = ALL, extra = {}) => ({ facet, engine, engines, ...extra });
|
|
1356
1444
|
var each = (facetPrefix, enginePrefix, keys, engines) => keys.map((k) => f(`${facetPrefix}.${k}`, `${enginePrefix}.${k}`, engines));
|
|
1357
1445
|
var BRUSH_FIELDS = [
|
|
@@ -1368,6 +1456,7 @@ var BRUSH_FIELDS = [
|
|
|
1368
1456
|
...each("fluorescence", "fluorescence", ["amount", "invisible"]),
|
|
1369
1457
|
...each("thermochromic", "thermochromic", ["hotColor", "threshold"]),
|
|
1370
1458
|
...each("iridescence", "iridescence", ["thickness", "pressureToThickness", "ior"], FILM),
|
|
1459
|
+
...each("ferro", "ferro", ["amount", "wavelength", "threshold"]),
|
|
1371
1460
|
// Paint exchange: the dab, wet and impasto engines' behaviour, or the Mixer Brush's settings.
|
|
1372
1461
|
f("behaviour.exchange.pickup", "behavior.pickup", EXCHANGE),
|
|
1373
1462
|
f("behaviour.exchange.mix", "behavior.mix", EXCHANGE),
|
|
@@ -1390,6 +1479,7 @@ var BRUSH_FIELDS = [
|
|
|
1390
1479
|
f("behaviour.strength", "wet.strength", ["wet"]),
|
|
1391
1480
|
// Surfaces.
|
|
1392
1481
|
...each("behaviour.wet", "wet", ["medium", "water", "pigment", "body", "rewet", "load", "thinner"], ["wet"]),
|
|
1482
|
+
f("livePatterns", "livePatterns", ["wet"], { map: LIVE_PATTERNS_MAP }),
|
|
1393
1483
|
...each("ghost", "ghost", ["linger", "smear", "trigger"], ["wet"]),
|
|
1394
1484
|
...each("mutation", "mutation", ["settle", "patience", "fade"], ["dab", "wet", "impasto", "particle", "bristle"]),
|
|
1395
1485
|
f("behaviour.thick.applicator", "impasto.tool", ["impasto"]),
|
|
@@ -1486,6 +1576,7 @@ function resolveBrush(brush, options = {}) {
|
|
|
1486
1576
|
if (brush.iridescence && FILM.includes(engine)) input.iridescence = { ...input.iridescence ?? {} };
|
|
1487
1577
|
if (engine === "wet") {
|
|
1488
1578
|
setPath(input, "wet.tool", brush.kind === "tool" ? WET_TOOL_OF[brush.tool] : "paint");
|
|
1579
|
+
if (brush.kind === "tool") delete input.livePatterns;
|
|
1489
1580
|
const medium = resolveWetMedium(brush);
|
|
1490
1581
|
if (medium) setPath(input, "wet.medium", medium);
|
|
1491
1582
|
if (brush.kind !== "tool" && brush.ghost) input.ghost = { ...input.ghost ?? {} };
|
|
@@ -1574,7 +1665,7 @@ function skeleton(b) {
|
|
|
1574
1665
|
}
|
|
1575
1666
|
}
|
|
1576
1667
|
}
|
|
1577
|
-
var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "motion", "colour"];
|
|
1668
|
+
var ORDER = ["id", "name", "description", "tags", "kind", "medium", "variety", "tool", "tip", "marks", "dynamics", "behaviour", "light", "fluorescence", "thermochromic", "iridescence", "ghost", "mutation", "ferro", "motion", "livePatterns", "colour"];
|
|
1578
1669
|
function migrateBrushV1(raw) {
|
|
1579
1670
|
const b = normalizePaintBrush(raw);
|
|
1580
1671
|
const brush = skeleton(b);
|
|
@@ -1813,11 +1904,29 @@ var MUTATION = object("Mutation paint: you paint something and pause; once an ag
|
|
|
1813
1904
|
patience: num(5, 300, "Seconds to wait for an agent before giving up and keeping the paint as ordinary paint (default 45)."),
|
|
1814
1905
|
fade: num(0.5, 10, "Seconds the paint takes to sink away once claimed by read_mutation (default 2).")
|
|
1815
1906
|
});
|
|
1907
|
+
var FERRO = object("Liquid ferrofluid paint: rises into glossy Rosensweig spikes near magnets until cured.", {
|
|
1908
|
+
amount: unit3("How much liquid ferrofluid the brush lays per dab (default 0: none)."),
|
|
1909
|
+
wavelength: num(4, 64, "Capillary wavelength (spike-to-spike spacing) in pixels (default 16)."),
|
|
1910
|
+
threshold: num(0.01, 0.95, "Critical magnetic field |B| threshold where spikes begin to rise (default 0.12).")
|
|
1911
|
+
});
|
|
1816
1912
|
var MOTION = object("Animation after painting.", {
|
|
1817
1913
|
live: bool("Effect paint keeps playing on stroke layers (default false)."),
|
|
1818
1914
|
fps: int(1, 60, "Suggested playback rate in frames per second."),
|
|
1819
1915
|
waveSpeed: unit3("Particle waves: how fast they run by stroke time; 0 they stand still (default 0), 1 about four a second.")
|
|
1820
1916
|
});
|
|
1917
|
+
var LIVE_PATTERNS = object("Gray-Scott reaction-diffusion patterns (spots, stripes, coral, maze, worms) that grow inside wet paint (behaviour.surface: wet) and freeze where the paint dries.", {
|
|
1918
|
+
pattern: oneOf2(LIVE_PATTERN_KINDS, "Named Pearson feed/kill preset: spots (default, dividing mitosis spots), stripes (labyrinthine stripes), coral (branching dendrites), maze (winding channels) or worms (vermicular tracks)."),
|
|
1919
|
+
feed: num(0.01, 0.1, "Gray-Scott feed rate F (0.01..0.1; default from pattern)."),
|
|
1920
|
+
kill: num(0.03, 0.08, "Gray-Scott kill rate k (0.03..0.08; default from pattern)."),
|
|
1921
|
+
scale: num(0.5, 4, "Spatial scale of the pattern (diffusion multiplier, 0.5..4, default 1)."),
|
|
1922
|
+
contrast: num(0, 2, "Visual contrast of the V concentration field (0..2, default 1)."),
|
|
1923
|
+
secondColor: {
|
|
1924
|
+
type: ["string", "null"],
|
|
1925
|
+
pattern: "^#[0-9a-fA-F]{6}$",
|
|
1926
|
+
description: "Optional second colour (#rrggbb) that V blends the wet paint towards; null or omitted modulates pigment density/contrast."
|
|
1927
|
+
},
|
|
1928
|
+
relief: unit3("Impasto relief height written to the layer thick map as patterns grow (0..1, default 0).")
|
|
1929
|
+
});
|
|
1821
1930
|
var SOLID = {
|
|
1822
1931
|
type: "string",
|
|
1823
1932
|
pattern: "^(#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\\(\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*,\\s*\\d{1,3}\\s*(,\\s*(0|1|0?\\.\\d+)\\s*)?\\))$",
|
|
@@ -1881,7 +1990,9 @@ var BRUSH_JSON_SCHEMA = object(
|
|
|
1881
1990
|
iridescence: IRIDESCENCE,
|
|
1882
1991
|
ghost: GHOST,
|
|
1883
1992
|
mutation: MUTATION,
|
|
1993
|
+
ferro: FERRO,
|
|
1884
1994
|
motion: MOTION,
|
|
1995
|
+
livePatterns: LIVE_PATTERNS,
|
|
1885
1996
|
colour: COLOUR
|
|
1886
1997
|
},
|
|
1887
1998
|
["id", "name", "kind", "tip"]
|
|
@@ -1898,7 +2009,9 @@ var BRUSH_FACET_SCHEMAS = {
|
|
|
1898
2009
|
iridescence: IRIDESCENCE,
|
|
1899
2010
|
ghost: GHOST,
|
|
1900
2011
|
mutation: MUTATION,
|
|
2012
|
+
ferro: FERRO,
|
|
1901
2013
|
motion: MOTION,
|
|
2014
|
+
livePatterns: LIVE_PATTERNS,
|
|
1902
2015
|
colour: COLOUR,
|
|
1903
2016
|
paint: PAINT
|
|
1904
2017
|
};
|
|
@@ -2047,17 +2160,19 @@ function read(value, schema, path, problems) {
|
|
|
2047
2160
|
problems.push(`${path} must be ${JSON.stringify(schema.const)}; dropped.`);
|
|
2048
2161
|
return void 0;
|
|
2049
2162
|
}
|
|
2050
|
-
|
|
2163
|
+
if (value === null && Array.isArray(schema.type) && schema.type.includes("null")) return null;
|
|
2164
|
+
const schemaType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
2165
|
+
switch (schemaType) {
|
|
2051
2166
|
case "number":
|
|
2052
2167
|
case "integer": {
|
|
2053
2168
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2054
2169
|
problems.push(`${path} must be a number; dropped.`);
|
|
2055
2170
|
return void 0;
|
|
2056
2171
|
}
|
|
2057
|
-
let n =
|
|
2172
|
+
let n = schemaType === "integer" ? Math.round(value) : value;
|
|
2058
2173
|
if (schema.minimum !== void 0 && n < schema.minimum) n = schema.minimum;
|
|
2059
2174
|
if (schema.maximum !== void 0 && n > schema.maximum) n = schema.maximum;
|
|
2060
|
-
if (n !== value) problems.push(`${path} ${value} is out of range (${schema.minimum}..${schema.maximum}${
|
|
2175
|
+
if (n !== value) problems.push(`${path} ${value} is out of range (${schema.minimum}..${schema.maximum}${schemaType === "integer" ? ", whole" : ""}); set to ${n}.`);
|
|
2061
2176
|
return n;
|
|
2062
2177
|
}
|
|
2063
2178
|
case "boolean":
|
|
@@ -2070,7 +2185,8 @@ function read(value, schema, path, problems) {
|
|
|
2070
2185
|
problems.push(`${path} ${JSON.stringify(value)} is not valid${schema.enum ? `; use one of ${schema.enum.join(", ")}` : ""}; dropped.`);
|
|
2071
2186
|
return void 0;
|
|
2072
2187
|
}
|
|
2073
|
-
|
|
2188
|
+
const s = schema.pattern === "^#[0-9a-fA-F]{6}$" ? value.toLowerCase() : value;
|
|
2189
|
+
return schema.maxLength !== void 0 ? s.slice(0, schema.maxLength) : s;
|
|
2074
2190
|
}
|
|
2075
2191
|
case "array": {
|
|
2076
2192
|
if (!Array.isArray(value)) {
|
|
@@ -2139,14 +2255,14 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
2139
2255
|
else if (raw.kind === void 0) kind = tool ? "tool" : "mark";
|
|
2140
2256
|
else throw new Error(`Brush "${raw.id}" kind ${JSON.stringify(raw.kind)} is not valid: use mark (lays a material) or tool (works on paint already down).`);
|
|
2141
2257
|
if (kind === "tool" && !tool) throw new Error(`Brush "${raw.id}" is a tool and needs tool: one of ${BRUSH_TOOLS.join(", ")}.`);
|
|
2142
|
-
const { colour: rawColour, marks: rawMarks, kind: _k, id: _i, name: _n, tool: _t, ...rest } = raw;
|
|
2258
|
+
const { colour: rawColour, marks: rawMarks, ferro: rawFerro, kind: _k, id: _i, name: _n, tool: _t, ...rest } = raw;
|
|
2143
2259
|
if (typeof rest.fluorescence === "number" && Number.isFinite(rest.fluorescence)) rest.fluorescence = { amount: rest.fluorescence };
|
|
2144
2260
|
const out = readObject(rest, BRUSH_JSON_SCHEMA, "brush", problems);
|
|
2145
2261
|
if (out.thermochromic?.hotColor) out.thermochromic.hotColor = out.thermochromic.hotColor.toLowerCase();
|
|
2146
2262
|
const brush = { id: raw.id, name, ...out, kind, ...tool ? { tool } : {}, tip: out.tip ?? { shape: "round" } };
|
|
2147
2263
|
const ordered = () => {
|
|
2148
|
-
const { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, motion, colour } = brush;
|
|
2149
|
-
const all = { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, motion, colour };
|
|
2264
|
+
const { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, ferro, motion, livePatterns, colour } = brush;
|
|
2265
|
+
const all = { id, name: n, description, tags, kind: k, medium, variety, tool: t, tip, marks, dynamics, behaviour, light, fluorescence, thermochromic, iridescence, ghost, mutation, ferro, motion, livePatterns, colour };
|
|
2150
2266
|
return Object.fromEntries(Object.entries(all).filter(([, v]) => v !== void 0));
|
|
2151
2267
|
};
|
|
2152
2268
|
if (!out.tip) problems.push("tip is missing; a round tip is used.");
|
|
@@ -2160,6 +2276,11 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
2160
2276
|
if (!schema) problems.push(`marks.kind ${JSON.stringify(k)} is not valid; use continuous, scatter, particles or objects. marks dropped.`);
|
|
2161
2277
|
else brush.marks = readObject(rawMarks, schema, "brush.marks", problems);
|
|
2162
2278
|
}
|
|
2279
|
+
if (rawFerro !== void 0) {
|
|
2280
|
+
const ferroObj = typeof rawFerro === "number" ? { amount: rawFerro } : rawFerro;
|
|
2281
|
+
const f2 = readObject(ferroObj, BRUSH_FACET_SCHEMAS.ferro, "brush.ferro", problems);
|
|
2282
|
+
if (f2 && Object.keys(f2).length) brush.ferro = f2;
|
|
2283
|
+
}
|
|
2163
2284
|
if (rawColour !== void 0) {
|
|
2164
2285
|
const c = readObject(rawColour, { ...BRUSH_FACET_SCHEMAS.colour, properties: { own: BRUSH_FACET_SCHEMAS.colour.properties.own, default: {}, palette: {} } }, "brush.colour", problems);
|
|
2165
2286
|
if (c) {
|
|
@@ -2194,6 +2315,10 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
2194
2315
|
drop("marks", "tools lay no marks");
|
|
2195
2316
|
delete brush.marks;
|
|
2196
2317
|
}
|
|
2318
|
+
if (brush.ferro) {
|
|
2319
|
+
drop("ferro", "tools lay no ferrofluid");
|
|
2320
|
+
delete brush.ferro;
|
|
2321
|
+
}
|
|
2197
2322
|
if (brush.iridescence) {
|
|
2198
2323
|
drop("iridescence", "tools lay no film");
|
|
2199
2324
|
delete brush.iridescence;
|
|
@@ -2279,6 +2404,10 @@ function normalizeBrushSpec(raw, problems = []) {
|
|
|
2279
2404
|
delete b.exchange;
|
|
2280
2405
|
}
|
|
2281
2406
|
}
|
|
2407
|
+
if (brush.livePatterns && (kind !== "mark" || brush.behaviour?.surface !== "wet" || brush.marks?.kind === "particles" || brush.marks?.kind === "objects" || brush.tip.shape === "bristle")) {
|
|
2408
|
+
drop("livePatterns", "only wet paint brushes grow live patterns (set behaviour.surface to wet)");
|
|
2409
|
+
delete brush.livePatterns;
|
|
2410
|
+
}
|
|
2282
2411
|
if (brush.ghost) {
|
|
2283
2412
|
if (kind === "tool" || brush.behaviour?.surface !== "wet") {
|
|
2284
2413
|
drop("ghost", "it needs a mark brush with behaviour.surface wet");
|
|
@@ -2318,7 +2447,9 @@ var PAINT_PROFILE = {
|
|
|
2318
2447
|
tilt: "full",
|
|
2319
2448
|
tipGrain: "full",
|
|
2320
2449
|
dabCount: "full",
|
|
2321
|
-
|
|
2450
|
+
livePatterns: "full",
|
|
2451
|
+
iridescence: "full",
|
|
2452
|
+
ferro: "full"
|
|
2322
2453
|
};
|
|
2323
2454
|
var IMAGE_PROFILE = {
|
|
2324
2455
|
id: "image",
|
|
@@ -2395,6 +2526,7 @@ function checkBrush(brush, profile) {
|
|
|
2395
2526
|
feature(profile.exchange, !ex?.reservoir && (nonZero(ex?.pickup) || !!ex?.dirty), "behaviour.exchange", "picking paint up", "Remove behaviour.exchange.");
|
|
2396
2527
|
feature(profile.grain, nonZero(brush.behaviour?.grain), "behaviour.grain", "paper grain", "Set behaviour.grain to 0.");
|
|
2397
2528
|
feature(profile.light, nonZero(brush.light?.amount), "light", "light paint (glow and spill)", "Remove light (or set light.amount to 0).");
|
|
2529
|
+
feature(profile.livePatterns ?? "none", Boolean(brush.livePatterns), "livePatterns", "live reaction-diffusion patterns", "Remove livePatterns.");
|
|
2398
2530
|
const smartInks = profile.id === "paint" ? "full" : "none";
|
|
2399
2531
|
feature(smartInks, nonZero(brush.fluorescence?.amount), "fluorescence", "UV-fluorescent ink", "Remove fluorescence (or set fluorescence.amount to 0).");
|
|
2400
2532
|
feature(smartInks, Boolean(brush.thermochromic?.hotColor), "thermochromic", "thermochromic ink", "Remove thermochromic.");
|
|
@@ -2408,6 +2540,7 @@ function checkBrush(brush, profile) {
|
|
|
2408
2540
|
else if (nonZero(ex?.pickup) || !!ex?.dirty) add("mutation", "error", "Mutation brushes must lay their own colour without picking up paint from beneath.", "Remove behaviour.exchange (or set pickup to 0 and dirty to false), or remove mutation.");
|
|
2409
2541
|
else if (surface === "wet" && nonZero(brush.light?.amount)) add("mutation", "error", "Wet mutation brushes use the wet light channel as their fade tracer.", "Remove light (or set light.amount to 0), or remove mutation.");
|
|
2410
2542
|
}
|
|
2543
|
+
feature(profile.ferro ?? "none", nonZero(brush.ferro?.amount), "ferro", "ferrofluid spikes and magnets", "Remove ferro (or set ferro.amount to 0).");
|
|
2411
2544
|
const m = brush.motion;
|
|
2412
2545
|
feature(profile.motion, !!m && (!!m.live || nonZero(m.waveSpeed) || m.fps !== void 0), "motion", "animation", "Remove motion.");
|
|
2413
2546
|
const d = brush.dynamics ?? {};
|
|
@@ -2458,8 +2591,12 @@ function degrade(brush, profile, options = {}) {
|
|
|
2458
2591
|
delete b.behaviour.surface;
|
|
2459
2592
|
delete b.behaviour.wet;
|
|
2460
2593
|
delete b.behaviour.thick;
|
|
2594
|
+
delete b.livePatterns;
|
|
2461
2595
|
delete b.ghost;
|
|
2462
2596
|
break;
|
|
2597
|
+
case "livePatterns":
|
|
2598
|
+
delete b.livePatterns;
|
|
2599
|
+
break;
|
|
2463
2600
|
case "ghost":
|
|
2464
2601
|
delete b.ghost;
|
|
2465
2602
|
break;
|
|
@@ -2493,6 +2630,9 @@ function degrade(brush, profile, options = {}) {
|
|
|
2493
2630
|
case "iridescence":
|
|
2494
2631
|
delete b.iridescence;
|
|
2495
2632
|
break;
|
|
2633
|
+
case "ferro":
|
|
2634
|
+
delete b.ferro;
|
|
2635
|
+
break;
|
|
2496
2636
|
case "motion":
|
|
2497
2637
|
delete b.motion;
|
|
2498
2638
|
break;
|
|
@@ -2880,6 +3020,8 @@ export {
|
|
|
2880
3020
|
normalizeGhost,
|
|
2881
3021
|
DEFAULT_MUTATION,
|
|
2882
3022
|
normalizeMutation,
|
|
3023
|
+
DEFAULT_FERRO,
|
|
3024
|
+
normalizeFerro,
|
|
2883
3025
|
normalizeImpasto,
|
|
2884
3026
|
MULTI_COLOR_MODES,
|
|
2885
3027
|
DEFAULT_MULTI_COLOR,
|