@pixerate/editor 0.239.0 → 0.241.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 +58 -58
- package/dist/animation-editor/index.js +3 -3
- package/dist/{brushstrokeRender-WB67GMUP.js → brushstrokeRender-MOYGEXOC.js} +2 -2
- package/dist/{brushstrokeRender-MPOM7BFW.cjs → brushstrokeRender-UKADOV5D.cjs} +5 -5
- package/dist/canvas/index.cjs +8 -8
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-NKTNLJUJ.cjs → chunk-5BR7LC24.cjs} +99 -11
- package/dist/{chunk-ZXNO3ZOX.cjs → chunk-7F4G74UQ.cjs} +35 -35
- package/dist/{chunk-AD7GA3TL.cjs → chunk-AFLMXITL.cjs} +23 -23
- package/dist/{chunk-PP6BXVRL.cjs → chunk-HGGJ36HR.cjs} +1 -1
- package/dist/{chunk-BBUE3UJL.cjs → chunk-HW72ABGR.cjs} +86 -86
- package/dist/{chunk-7YGBSFC6.js → chunk-ILM6GYAX.js} +1 -1
- package/dist/{chunk-THXFU3JB.cjs → chunk-KCMV2534.cjs} +2 -2
- package/dist/{chunk-3E36VDMJ.js → chunk-NE6OOKWJ.js} +1 -1
- package/dist/{chunk-RV5DKZHU.js → chunk-QP2B3MID.js} +1 -1
- package/dist/{chunk-UUBO6RB5.js → chunk-UDBDYZET.js} +1 -1
- package/dist/{chunk-PJYG6FCF.js → chunk-XBZK47LV.js} +98 -10
- package/dist/{chunk-XK4SST43.js → chunk-ZDPLZDTT.js} +1 -1
- package/dist/image-editor/index.cjs +775 -775
- package/dist/image-editor/index.js +4 -4
- package/dist/paint-editor/controller.d.cts +158 -1
- package/dist/paint-editor/controller.d.ts +158 -1
- package/dist/paint-editor/document.d.cts +7 -2
- package/dist/paint-editor/document.d.ts +7 -2
- package/dist/paint-editor/effect-paint.d.cts +60 -3
- package/dist/paint-editor/effect-paint.d.ts +60 -3
- package/dist/paint-editor/index.cjs +1246 -430
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +888 -72
- package/dist/paint-editor/live-strokes.d.cts +24 -0
- package/dist/paint-editor/live-strokes.d.ts +24 -0
- package/dist/paint-editor/stickers.d.cts +6 -0
- package/dist/paint-editor/stickers.d.ts +6 -0
- package/dist/paint-editor/thick.d.cts +4 -0
- package/dist/paint-editor/thick.d.ts +4 -0
- package/dist/paint-editor/ui.d.cts +24 -4
- package/dist/paint-editor/ui.d.ts +24 -4
- package/dist/paint-editor/wet.d.cts +8 -0
- package/dist/paint-editor/wet.d.ts +8 -0
- package/dist/vector/index.cjs +962 -962
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +38 -27
- package/skills/pixerate-paint-editor/references/effect-paint.md +25 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveExternalAssets,
|
|
8
8
|
serializeLayers,
|
|
9
9
|
serializeLayersExternal
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-NE6OOKWJ.js";
|
|
11
11
|
import "../chunk-3CI7FRMK.js";
|
|
12
12
|
import "../chunk-ZNA3IQZF.js";
|
|
13
13
|
import {
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
realPresenceClock,
|
|
56
56
|
tweenPresenceCursor
|
|
57
57
|
} from "../chunk-IVSWRVB4.js";
|
|
58
|
-
import "../chunk-
|
|
58
|
+
import "../chunk-ILM6GYAX.js";
|
|
59
59
|
import {
|
|
60
60
|
BRUSH_ID,
|
|
61
61
|
BRUSH_ROTATIONS,
|
|
@@ -72,6 +72,8 @@ import {
|
|
|
72
72
|
DEFAULT_STICKER,
|
|
73
73
|
DEFAULT_WET_BRUSH,
|
|
74
74
|
DryStroke,
|
|
75
|
+
EFFECT_MAP_REPEATS,
|
|
76
|
+
EFFECT_STRIP_HEIGHT,
|
|
75
77
|
IDENTITY_COPY,
|
|
76
78
|
IMPASTO_TOOLS,
|
|
77
79
|
MULTI_COLOR_MODES,
|
|
@@ -92,7 +94,9 @@ import {
|
|
|
92
94
|
brushToJSON,
|
|
93
95
|
clonePaper,
|
|
94
96
|
drawSticker,
|
|
97
|
+
effectDabField,
|
|
95
98
|
effectPaintKey,
|
|
99
|
+
effectPaintSize,
|
|
96
100
|
hexToRgb8,
|
|
97
101
|
isPaintEffectPaint,
|
|
98
102
|
isWetBrush,
|
|
@@ -114,6 +118,7 @@ import {
|
|
|
114
118
|
paperPreset,
|
|
115
119
|
parseBrushLibrary,
|
|
116
120
|
penOrientation,
|
|
121
|
+
readEffectStrokeMap,
|
|
117
122
|
readPaintEffectPaint,
|
|
118
123
|
renderPaper,
|
|
119
124
|
renderStickers,
|
|
@@ -126,7 +131,7 @@ import {
|
|
|
126
131
|
tiltToOrientation,
|
|
127
132
|
toStrokePoint,
|
|
128
133
|
usesPaintExchange
|
|
129
|
-
} from "../chunk-
|
|
134
|
+
} from "../chunk-XBZK47LV.js";
|
|
130
135
|
import "../chunk-M6DNZNVY.js";
|
|
131
136
|
import "../chunk-WEIK3UYB.js";
|
|
132
137
|
import "../chunk-PWBZH3FK.js";
|
|
@@ -458,7 +463,7 @@ var PaintDabStroke = class {
|
|
|
458
463
|
let flow = 1;
|
|
459
464
|
if (this.options.effect) {
|
|
460
465
|
const rgb = [0, 0, 0];
|
|
461
|
-
flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb);
|
|
466
|
+
flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb, d);
|
|
462
467
|
ex.setReservoirColor(rgb[0], rgb[1], rgb[2]);
|
|
463
468
|
} else if (this.sampler && !this.gradient) {
|
|
464
469
|
const g = this.sampler.dab(d, this.sample);
|
|
@@ -711,6 +716,7 @@ var ThickStrokeDriver = class {
|
|
|
711
716
|
height = /* @__PURE__ */ new Map();
|
|
712
717
|
material = /* @__PURE__ */ new Map();
|
|
713
718
|
paint = [0, 0, 0];
|
|
719
|
+
centre = [0, 0, 0];
|
|
714
720
|
tip = new Float32Array(0);
|
|
715
721
|
lastRaw = null;
|
|
716
722
|
lastFed = null;
|
|
@@ -837,7 +843,11 @@ var ThickStrokeDriver = class {
|
|
|
837
843
|
}
|
|
838
844
|
const ex = st.exchange;
|
|
839
845
|
let flow = 1;
|
|
840
|
-
|
|
846
|
+
const effect = this.init.effect;
|
|
847
|
+
if (effect) {
|
|
848
|
+
sampleEffectAt(effect, d.x, d.y, this.centre, d);
|
|
849
|
+
ex.setReservoirColor(this.centre[0], this.centre[1], this.centre[2]);
|
|
850
|
+
} else if (this.sampler) {
|
|
841
851
|
const g = this.sampler.dab(d, this.sample);
|
|
842
852
|
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
843
853
|
flow = g.alpha;
|
|
@@ -849,7 +859,12 @@ var ThickStrokeDriver = class {
|
|
|
849
859
|
if (dep > 0) {
|
|
850
860
|
const cov = new Float32Array(n);
|
|
851
861
|
for (let p = 0; p < n; p++) cov[p] = tip[p] > 0 ? Math.min(1, tip[p] * (im.tool === "roller" || im.tool === "gloop" || im.tool === "tube" ? 1.6 : 1.1)) : 0;
|
|
852
|
-
|
|
862
|
+
let field = null;
|
|
863
|
+
if (effect) {
|
|
864
|
+
field = new Float32Array(n * 3);
|
|
865
|
+
effectDabField(effect, rect, cov, this.centre, this.paint, field, 0, d);
|
|
866
|
+
}
|
|
867
|
+
this.medium.deposit(rect, cov, dep, this.paint, ex.paintPigment, field);
|
|
853
868
|
}
|
|
854
869
|
const seed = this.init.seed;
|
|
855
870
|
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
@@ -996,7 +1011,6 @@ var PaintStrokeSession = class {
|
|
|
996
1011
|
const brush = init.brush;
|
|
997
1012
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
998
1013
|
const effect = init.effect?.pixels ?? null;
|
|
999
|
-
if (effect && (init.wet || init.thick || init.sticker)) throw new Error(`The ${brush.name} brush (${brush.engine} engine) cannot paint with effect paint.`);
|
|
1000
1014
|
this.apply = {
|
|
1001
1015
|
mode: brush.engine === "eraser" ? "erase" : "paint",
|
|
1002
1016
|
color: init.color,
|
|
@@ -1009,10 +1023,10 @@ var PaintStrokeSession = class {
|
|
|
1009
1023
|
};
|
|
1010
1024
|
if (this.wetDriver) {
|
|
1011
1025
|
} else if (init.sticker) {
|
|
1012
|
-
this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null });
|
|
1026
|
+
this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect });
|
|
1013
1027
|
} else if (init.thick) {
|
|
1014
1028
|
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
1015
|
-
this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null });
|
|
1029
|
+
this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect });
|
|
1016
1030
|
} else if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush, Boolean(init.paint))) {
|
|
1017
1031
|
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
1018
1032
|
this.wet = new PaintDabStroke(brush, {
|
|
@@ -1519,7 +1533,8 @@ function variationAffine(v, origin) {
|
|
|
1519
1533
|
return composeAffine(back, composeAffine(linear, toOrigin));
|
|
1520
1534
|
}
|
|
1521
1535
|
function varyStroke(stroke, m, v) {
|
|
1522
|
-
const
|
|
1536
|
+
const det = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
|
|
1537
|
+
const s = Math.abs(det - 1) < 1e-9 ? 1 : det;
|
|
1523
1538
|
const ps = v.pressureScale ?? 1;
|
|
1524
1539
|
const points = stroke.points.map((p) => {
|
|
1525
1540
|
const q = mapPenPoint(m, p);
|
|
@@ -1529,6 +1544,7 @@ function varyStroke(stroke, m, v) {
|
|
|
1529
1544
|
const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;
|
|
1530
1545
|
const out = { ...stroke, brush, points };
|
|
1531
1546
|
if (stroke.paint?.type === "gradient" && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);
|
|
1547
|
+
if (stroke.paint?.type === "effect" && stroke.paint.map && s !== 1) out.paint = { ...stroke.paint, map: { ...stroke.paint.map, length: Math.min(4096, Math.max(16, Math.round(stroke.paint.map.length * s))) } };
|
|
1532
1548
|
if (stroke.copies?.length) {
|
|
1533
1549
|
const inv = invertAffine(m);
|
|
1534
1550
|
out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));
|
|
@@ -1940,6 +1956,7 @@ var WetStrokeDriver = class {
|
|
|
1940
1956
|
tip = new Float32Array(0);
|
|
1941
1957
|
cov = new Float32Array(0);
|
|
1942
1958
|
paint = [0, 0, 0];
|
|
1959
|
+
centre = [0, 0, 0];
|
|
1943
1960
|
depth;
|
|
1944
1961
|
sampler;
|
|
1945
1962
|
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
@@ -2040,14 +2057,23 @@ var WetStrokeDriver = class {
|
|
|
2040
2057
|
case "paint": {
|
|
2041
2058
|
const ex = st.exchange;
|
|
2042
2059
|
let flow = 1;
|
|
2043
|
-
|
|
2060
|
+
const effect = this.init.effect;
|
|
2061
|
+
if (effect) {
|
|
2062
|
+
sampleEffectAt(effect, d.x, d.y, this.centre, d);
|
|
2063
|
+
ex.setReservoirColor(this.centre[0], this.centre[1], this.centre[2]);
|
|
2064
|
+
} else if (this.sampler) {
|
|
2044
2065
|
const g = this.sampler.dab(d, this.sample);
|
|
2045
2066
|
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
2046
2067
|
flow = g.alpha;
|
|
2047
2068
|
}
|
|
2048
2069
|
ex.pickUp(this.medium.sample(rect, tip));
|
|
2049
2070
|
const total = ex.paint(this.paint);
|
|
2050
|
-
if (total > 0 && flow > 0
|
|
2071
|
+
if (total > 0 && flow > 0 && effect) {
|
|
2072
|
+
const ec = cov.slice();
|
|
2073
|
+
const field = new Float32Array(ec.length * 3);
|
|
2074
|
+
effectDabField(effect, rect, ec, this.centre, this.paint, field, COVERAGE_STEPS, d);
|
|
2075
|
+
this.medium.deposit(rect, ec, Math.min(1, total) * k, this.paint, null, field);
|
|
2076
|
+
} else if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);
|
|
2051
2077
|
else world.paint(laneId, rect, cov, this.medium.options.water * k, null);
|
|
2052
2078
|
ex.drain();
|
|
2053
2079
|
if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);
|
|
@@ -3085,6 +3111,43 @@ var MixingPad = class {
|
|
|
3085
3111
|
}
|
|
3086
3112
|
};
|
|
3087
3113
|
|
|
3114
|
+
// src/paint-editor/live-strokes.ts
|
|
3115
|
+
var LIVE_STROKE_ENGINES = ["dab", "particle", "bristle", "mixer"];
|
|
3116
|
+
function liveStrokeRefusal(brush) {
|
|
3117
|
+
switch (brush.engine) {
|
|
3118
|
+
case "wet":
|
|
3119
|
+
return `The ${brush.name} brush paints wet media, which a stroke layer can't redraw (the paint depends on the simulation's history); paint it on a pixel layer (add_layer).`;
|
|
3120
|
+
case "impasto":
|
|
3121
|
+
return `The ${brush.name} brush paints thick paint with relief, which a stroke layer doesn't keep; paint it on a pixel layer (add_layer).`;
|
|
3122
|
+
case "sticker":
|
|
3123
|
+
return `The ${brush.name} brush sprays stickers, which are their own objects; spray them on an empty pixel layer (add_layer).`;
|
|
3124
|
+
case "eraser":
|
|
3125
|
+
return `Erasers don't paint on a stroke layer; delete strokes instead (remove_strokes), or rasterize the layer first (rasterize_stroke_layer).`;
|
|
3126
|
+
case "blender":
|
|
3127
|
+
case "smudge":
|
|
3128
|
+
return `The ${brush.name} brush moves the paint under it, so it can't be redrawn on its own; rasterize the layer first (rasterize_stroke_layer) or blend on a pixel layer.`;
|
|
3129
|
+
}
|
|
3130
|
+
if (brush.behavior.pickup > 0) return `The ${brush.name} brush picks up the paint under it (pickup ${brush.behavior.pickup}), so it can't be redrawn on its own; set behavior.pickup to 0 (set_brush) or paint on a pixel layer.`;
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3133
|
+
function liveStrokeBounds(stroke) {
|
|
3134
|
+
let x0 = Infinity;
|
|
3135
|
+
let y0 = Infinity;
|
|
3136
|
+
let x1 = -Infinity;
|
|
3137
|
+
let y1 = -Infinity;
|
|
3138
|
+
for (const m of [null, ...stroke.copies ?? []]) {
|
|
3139
|
+
for (const p of stroke.points) {
|
|
3140
|
+
const q = m ? applyAffine(m, p.x, p.y) : p;
|
|
3141
|
+
if (q.x < x0) x0 = q.x;
|
|
3142
|
+
if (q.y < y0) y0 = q.y;
|
|
3143
|
+
if (q.x > x1) x1 = q.x;
|
|
3144
|
+
if (q.y > y1) y1 = q.y;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
const pad = stroke.brush.tip.size * (stroke.brush.engine === "particle" ? 2 : 1) + 2;
|
|
3148
|
+
return { x: Math.floor(x0 - pad), y: Math.floor(y0 - pad), width: Math.ceil(x1 - x0 + 2 * pad), height: Math.ceil(y1 - y0 + 2 * pad) };
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3088
3151
|
// src/paint-editor/replay.ts
|
|
3089
3152
|
function replayItems(doc) {
|
|
3090
3153
|
const items = [];
|
|
@@ -3126,7 +3189,15 @@ async function replayPainting(source, target, options = {}) {
|
|
|
3126
3189
|
if (!target.getLayer(e.layerId)) continue;
|
|
3127
3190
|
if (e.kind === "fill") target.fillLayer({ layerId: e.layerId, ...e.data });
|
|
3128
3191
|
else if (e.kind === "clear") target.clearLayer(e.layerId);
|
|
3129
|
-
else if (e.kind === "
|
|
3192
|
+
else if (e.kind === "live-strokes") {
|
|
3193
|
+
const d = e.data;
|
|
3194
|
+
if (d?.rasterized) target.replaceLiveStrokes(e.layerId, null);
|
|
3195
|
+
else {
|
|
3196
|
+
const list = (d?.strokes ?? []).map((st2) => s === 1 ? st2 : { ...varyStroke(st2, scaleM, { scale: s }), ...st2.stencils ? { stencils: st2.stencils.map((x) => scaleStencil(x, s)) } : {} });
|
|
3197
|
+
await target.prepareEffectPaints(list.flatMap((st2) => st2.paint?.type === "effect" ? [st2.paint] : []));
|
|
3198
|
+
target.replaceLiveStrokes(e.layerId, list);
|
|
3199
|
+
}
|
|
3200
|
+
} else if (e.kind === "stickers") {
|
|
3130
3201
|
const list = (e.data?.stickers ?? []).map((x) => s === 1 ? x : { ...x, x: x.x * s, y: x.y * s, size: x.size * s });
|
|
3131
3202
|
target.replaceStickers(e.layerId, list);
|
|
3132
3203
|
} else target.flattenRelief(e.layerId);
|
|
@@ -3450,6 +3521,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3450
3521
|
mixerLoad = EMPTY_MIXER_LOAD;
|
|
3451
3522
|
view = { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
|
|
3452
3523
|
hand;
|
|
3524
|
+
selectedStroke = null;
|
|
3453
3525
|
selectedSticker = null;
|
|
3454
3526
|
/** A sticker drag in progress (`previewSticker`): the document before it, so the drop undoes in one step, and the one it showed last. */
|
|
3455
3527
|
stickerPreview = null;
|
|
@@ -3463,6 +3535,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3463
3535
|
dirty = "all";
|
|
3464
3536
|
seedCounter = 1;
|
|
3465
3537
|
strokeCounter = 1;
|
|
3538
|
+
/** Live effect paint: playing and the frame rate cap (editor state), and the time the view shows. */
|
|
3539
|
+
effectPlaybackState = { playing: true, fps: 30 };
|
|
3540
|
+
effectTime = 0;
|
|
3541
|
+
/** While a frame is drawn: the time live effect strokes are drawn at (else their own time). */
|
|
3542
|
+
liveEffectAt = null;
|
|
3543
|
+
/** The view's stroke layers at `effectTime`, each with the document surface it was drawn from (stale once that changes). */
|
|
3544
|
+
animatedLayers = /* @__PURE__ */ new Map();
|
|
3545
|
+
/** `renderAnimationFrame` / `withEffectTime`: layer surfaces at another time, for one render. */
|
|
3546
|
+
frameOverride = null;
|
|
3547
|
+
/** The area each stroke painted (document pixels; `null`: nothing), learnt as it is painted or drawn, so a stroke layer edit redraws only where it changes (see `drawLiveStrokes`). */
|
|
3548
|
+
paintedBounds = /* @__PURE__ */ new WeakMap();
|
|
3466
3549
|
guides;
|
|
3467
3550
|
swatches = DEFAULT_SWATCHES.map((s) => ({ ...s }));
|
|
3468
3551
|
pad = new MixingPad();
|
|
@@ -3549,6 +3632,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3549
3632
|
mixerLoad: this.mixerLoad,
|
|
3550
3633
|
view: this.view,
|
|
3551
3634
|
hand: this.hand,
|
|
3635
|
+
effectPlayback: { ...this.effectPlaybackState, time: this.effectTime },
|
|
3636
|
+
selectedStroke: this.selectedStroke,
|
|
3552
3637
|
selectedSticker: this.selectedSticker,
|
|
3553
3638
|
canUndo: this.undoStack.length > 0 && !this.session,
|
|
3554
3639
|
canRedo: this.redoStack.length > 0 && !this.session,
|
|
@@ -3897,6 +3982,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3897
3982
|
locks: { ...l.locks ?? {} },
|
|
3898
3983
|
active: l.id === this.doc.activeLayerId,
|
|
3899
3984
|
bounds,
|
|
3985
|
+
...this.doc.liveStrokes?.[l.id] ? { liveStrokes: this.doc.liveStrokes[l.id].length } : {},
|
|
3900
3986
|
parentId,
|
|
3901
3987
|
depth
|
|
3902
3988
|
};
|
|
@@ -3923,11 +4009,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3923
4009
|
this.emit();
|
|
3924
4010
|
}
|
|
3925
4011
|
/** Adds an empty pixel layer or group above `aboveId` (default: the active layer) and makes it active. */
|
|
4012
|
+
/** Adds a layer above the active one: `pixel` (default), `group`, or `strokes`, a live stroke layer whose strokes stay editable. */
|
|
3926
4013
|
addLayer(options = {}) {
|
|
3927
4014
|
this.requireIdle("adding a layer");
|
|
3928
4015
|
const count = flattenLayers(this.doc.layers).length;
|
|
3929
4016
|
const type = options.type ?? "pixel";
|
|
3930
|
-
const name = options.name?.trim() || (type === "group" ? `Group ${count + 1}` : `Layer ${count + 1}`);
|
|
4017
|
+
const name = options.name?.trim() || (type === "group" ? `Group ${count + 1}` : type === "strokes" ? `Strokes ${count + 1}` : `Layer ${count + 1}`);
|
|
3931
4018
|
const layer = type === "group" ? { type: "group", id: newPaintLayerId(), name, visible: true, opacity: 1, blend: "pass-through", children: [] } : createPaintLayer(name);
|
|
3932
4019
|
let parentId = null;
|
|
3933
4020
|
let index;
|
|
@@ -3943,7 +4030,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3943
4030
|
index = loc.index + 1;
|
|
3944
4031
|
}
|
|
3945
4032
|
const layers = insertLayer(this.doc.layers, layer, parentId, index);
|
|
3946
|
-
|
|
4033
|
+
const live = type === "strokes" ? { liveStrokes: { ...this.doc.liveStrokes ?? {}, [layer.id]: [] } } : {};
|
|
4034
|
+
this.commit(type === "group" ? "Add group" : type === "strokes" ? "Add stroke layer" : "Add layer", { ...this.doc, layers, activeLayerId: layer.id, ...live });
|
|
3947
4035
|
return this.describeLayer(layer);
|
|
3948
4036
|
}
|
|
3949
4037
|
updateLayer(id, update) {
|
|
@@ -3980,7 +4068,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3980
4068
|
const layers = mapLayer(this.doc.layers, id, () => null);
|
|
3981
4069
|
let active = this.doc.activeLayerId;
|
|
3982
4070
|
if (active && !findLayer(layers, active)) active = flattenLayers(layers).filter((x) => x.type === "pixel").pop()?.id ?? null;
|
|
3983
|
-
this.commit("Remove layer", { ...this.doc, layers, activeLayerId: active, ...this.thickWithout(flattenLayers([l]).map((x) => x.id)), ...this.stickersWithout(flattenLayers([l]).map((x) => x.id)) });
|
|
4071
|
+
this.commit("Remove layer", { ...this.doc, layers, activeLayerId: active, ...this.thickWithout(flattenLayers([l]).map((x) => x.id)), ...this.stickersWithout(flattenLayers([l]).map((x) => x.id)), ...this.liveStrokesWithout(flattenLayers([l]).map((x) => x.id)) });
|
|
3984
4072
|
return { removed: id, activeLayerId: active };
|
|
3985
4073
|
}
|
|
3986
4074
|
duplicateLayer(id) {
|
|
@@ -3993,15 +4081,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3993
4081
|
if (!loc) throw new PaintEditorError(`Unknown layer "${id}". Call list_layers for the ids.`);
|
|
3994
4082
|
const thick = { ...this.doc.thick ?? {} };
|
|
3995
4083
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
4084
|
+
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
3996
4085
|
const copy = (l) => {
|
|
3997
4086
|
const id2 = newPaintLayerId();
|
|
3998
4087
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
3999
4088
|
if (stickers[l.id]) stickers[id2] = stickers[l.id].map((x) => ({ ...x }));
|
|
4089
|
+
if (liveStrokes[l.id]) liveStrokes[id2] = liveStrokes[l.id].map((x) => ({ ...x, id: `s${this.strokeCounter++}`, layerId: id2 }));
|
|
4000
4090
|
return l.type === "group" ? { ...l, id: id2, children: l.children.map(copy) } : { ...l, id: id2 };
|
|
4001
4091
|
};
|
|
4002
4092
|
const dup = { ...copy(loc.layer), name: `${loc.layer.name} copy` };
|
|
4003
4093
|
const layers = insertLayer(this.doc.layers, dup, loc.parent?.id ?? null, loc.index + 1);
|
|
4004
|
-
this.commit("Duplicate layer", this.withWetEvents({ ...this.doc, layers, activeLayerId: dup.id, ...this.doc.thick ? { thick } : {}, ...this.doc.stickers ? { stickers } : {} }));
|
|
4094
|
+
this.commit("Duplicate layer", this.withWetEvents({ ...this.doc, layers, activeLayerId: dup.id, ...this.doc.thick ? { thick } : {}, ...this.doc.stickers ? { stickers } : {}, ...this.doc.liveStrokes ? { liveStrokes } : {} }));
|
|
4005
4095
|
return this.describeLayer(dup);
|
|
4006
4096
|
}
|
|
4007
4097
|
/** Moves a layer to `index` (0 = bottom) within `parentId` (default its own parent; `null` = top level). */
|
|
@@ -4047,7 +4137,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4047
4137
|
}
|
|
4048
4138
|
const merged = { ...below, surface, offset: { x: 0, y: 0 } };
|
|
4049
4139
|
const layers = mapLayer(mapLayer(this.doc.layers, upper.id, () => null), below.id, () => merged);
|
|
4050
|
-
this.commit("Merge down", this.withWetEvents({ ...this.doc, layers, activeLayerId: below.id, ...this.thickWithout([upper.id]), ...this.stickersWithout([upper.id, below.id]) }));
|
|
4140
|
+
this.commit("Merge down", this.withWetEvents({ ...this.doc, layers, activeLayerId: below.id, ...this.thickWithout([upper.id]), ...this.stickersWithout([upper.id, below.id]), ...this.liveStrokesWithout([upper.id, below.id]) }));
|
|
4051
4141
|
return this.describeLayer(merged);
|
|
4052
4142
|
}
|
|
4053
4143
|
/** Removes every pixel of a layer. */
|
|
@@ -4059,7 +4149,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4059
4149
|
this.wet.world.removeLane(l.id);
|
|
4060
4150
|
}
|
|
4061
4151
|
const bounds = this.describeLayer(l).bounds;
|
|
4062
|
-
this.commit("Clear layer", this.withPaintEvent({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })), ...this.thickWithout([l.id]), ...this.stickersWithout([l.id]) }, { kind: "clear", layerId: l.id }));
|
|
4152
|
+
this.commit("Clear layer", this.withPaintEvent({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: emptySurface(4) })), ...this.thickWithout([l.id]), ...this.stickersWithout([l.id]), ...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {} }, { kind: "clear", layerId: l.id }));
|
|
4063
4153
|
return { layerId: l.id, bounds };
|
|
4064
4154
|
}
|
|
4065
4155
|
/**
|
|
@@ -4070,6 +4160,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4070
4160
|
this.requireIdle("filling");
|
|
4071
4161
|
const fillTarget = this.paintTarget(options.layerId, "fill");
|
|
4072
4162
|
if (this.doc.stickers?.[fillTarget.id]?.length) throw new PaintEditorError(`Layer "${fillTarget.name}" is a sticker layer; fill another layer (add_layer).`);
|
|
4163
|
+
if (this.doc.liveStrokes?.[fillTarget.id]) throw new PaintEditorError(`Layer "${fillTarget.name}" is a stroke layer (its pixels are drawn from its strokes); fill another layer (add_layer), or rasterize it first (rasterize_stroke_layer).`);
|
|
4073
4164
|
this.dryLayersFirst([options.layerId ?? this.doc.activeLayerId ?? ""]);
|
|
4074
4165
|
const l = this.paintTarget(options.layerId, "fill");
|
|
4075
4166
|
const color = options.color === void 0 ? this.color : parseColor(options.color);
|
|
@@ -4181,6 +4272,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4181
4272
|
effectTarget(layerId, what) {
|
|
4182
4273
|
const l = this.paintTarget(layerId, what);
|
|
4183
4274
|
if (this.doc.stickers?.[l.id]?.length) throw new PaintEditorError(`Layer "${l.name}" is a sticker layer; ${what} another layer (add_layer).`);
|
|
4275
|
+
if (this.doc.liveStrokes?.[l.id]) throw new PaintEditorError(`Layer "${l.name}" is a stroke layer (its pixels are drawn from its strokes); ${what} another layer (add_layer), or rasterize it first (rasterize_stroke_layer).`);
|
|
4184
4276
|
if (this.wet.wetLayerIds().includes(l.id)) throw new PaintEditorError(`Layer "${l.name}" holds wet paint; effects go on dried paint, so dry it first (dry_paint) and try again.`);
|
|
4185
4277
|
return l;
|
|
4186
4278
|
}
|
|
@@ -4344,16 +4436,25 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4344
4436
|
* size, and brushes then take each pixel's colour (and alpha) from it, fixed to the canvas, as if
|
|
4345
4437
|
* painting a hidden picture into view. `setColor` and `setPaint` go back to colour. Editor state,
|
|
4346
4438
|
* not an undo step. Strokes that would paint with it throw while it renders. Returns the paint.
|
|
4439
|
+
* With `map: { by: 'stroke' }` the effect lies along each stroke instead of the canvas; with
|
|
4440
|
+
* `live`, strokes on a stroke layer keep playing it (see `setEffectPlayback`).
|
|
4347
4441
|
*/
|
|
4348
4442
|
async setEffectPaint(source) {
|
|
4349
4443
|
if (!source || typeof source.effect !== "string" || !source.effect) throw new PaintEditorError("effect is missing; pass an effect id from search_effects (use generator, overlay or title).");
|
|
4350
4444
|
this.checkTime(source.time);
|
|
4445
|
+
let map;
|
|
4446
|
+
try {
|
|
4447
|
+
map = readEffectStrokeMap(source.map);
|
|
4448
|
+
} catch (e) {
|
|
4449
|
+
throw new PaintEditorError(e.message);
|
|
4450
|
+
}
|
|
4451
|
+
if (source.live !== void 0 && typeof source.live !== "boolean") throw new PaintEditorError("live must be true or false.");
|
|
4351
4452
|
const epoch = ++this.paintEpoch;
|
|
4352
4453
|
const changed = (name) => new PaintEditorError(`The paint changed while "${name}" rendered (set_color, set_paint or another set_effect_paint); call set_effect_paint again to paint with it.`);
|
|
4353
4454
|
const d = await this.describeEffect(source.effect, "paint");
|
|
4354
4455
|
const values = this.effectValues(d, { ...d.defaults }, source.values);
|
|
4355
4456
|
if (epoch !== this.paintEpoch) throw changed(d.name);
|
|
4356
|
-
const paint = { type: "effect", effect: d.id, provider: this.effects.require().id, values, time: source.time ?? 0 };
|
|
4457
|
+
const paint = { type: "effect", effect: d.id, provider: this.effects.require().id, values, time: source.time ?? 0, ...map ? { map } : {}, ...source.live ? { live: true } : {} };
|
|
4357
4458
|
this.effectPaintPending = paint;
|
|
4358
4459
|
this.emit([]);
|
|
4359
4460
|
try {
|
|
@@ -4376,7 +4477,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4376
4477
|
effectPaintImage(p) {
|
|
4377
4478
|
const key = effectPaintKey(p);
|
|
4378
4479
|
const img = this.effectPaintPixels.get(key);
|
|
4379
|
-
|
|
4480
|
+
const size = effectPaintSize(p, this.doc.canvas);
|
|
4481
|
+
if (!img || img.width !== size.width || img.height !== size.height) return null;
|
|
4380
4482
|
this.effectPaintPixels.delete(key);
|
|
4381
4483
|
this.effectPaintPixels.set(key, img);
|
|
4382
4484
|
return img;
|
|
@@ -4389,14 +4491,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4389
4491
|
const what = `the effect "${p.effect}"${p.provider ? ` from provider "${p.provider}"` : ""}`;
|
|
4390
4492
|
if (!provider) throw new PaintEditorError(`Effect paint needs ${what}, but no effects provider is registered; register it (setEffectsProvider) and try again.`);
|
|
4391
4493
|
if (p.provider && provider.id !== p.provider) throw new PaintEditorError(`Effect paint needs ${what}, but the registered provider is "${provider.id}"; register "${p.provider}" (setEffectsProvider) and try again.`);
|
|
4392
|
-
const { width, height } = this.doc.canvas;
|
|
4494
|
+
const { width, height } = effectPaintSize(p, this.doc.canvas);
|
|
4393
4495
|
let pixels;
|
|
4394
4496
|
try {
|
|
4395
4497
|
pixels = await this.effectsRenderer.render(provider, p.effect, { width, height }, { time: p.time, values: p.values });
|
|
4396
4498
|
} catch (e) {
|
|
4397
4499
|
throw new PaintEditorError(`Effect paint could not render ${what}: ${e instanceof Error ? e.message : String(e)}`);
|
|
4398
4500
|
}
|
|
4399
|
-
const img = { width, height, data: new Uint8ClampedArray(pixels.data) };
|
|
4501
|
+
const img = { width, height, data: new Uint8ClampedArray(pixels.data), ...p.map ? { map: p.map } : {} };
|
|
4400
4502
|
this.effectPaintPixels.set(effectPaintKey(p), img);
|
|
4401
4503
|
while (this.effectPaintPixels.size > _PaintEditorController.EFFECT_PAINT_CACHE) this.effectPaintPixels.delete(this.effectPaintPixels.keys().next().value);
|
|
4402
4504
|
return img;
|
|
@@ -4457,11 +4559,6 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4457
4559
|
source = this.effectPaint;
|
|
4458
4560
|
}
|
|
4459
4561
|
if (!source) return null;
|
|
4460
|
-
if (brush.engine === "wet" || brush.engine === "impasto" || brush.engine === "sticker") {
|
|
4461
|
-
throw new PaintEditorError(
|
|
4462
|
-
`The ${brush.name} brush (${brush.engine} engine) cannot paint with effect paint: it carries one colour as ${brush.engine === "sticker" ? "stickers" : brush.engine === "wet" ? "wet pigment" : "thick paint"}. Paint with a dry brush (a dab, particle, bristle or mixer brush), or paint the shape first and fill it with fill_layer_with_effect {lockTransparency: true}.`
|
|
4463
|
-
);
|
|
4464
|
-
}
|
|
4465
4562
|
const pixels = this.effectPaintImage(source);
|
|
4466
4563
|
if (!pixels) throw new PaintEditorError(`The effect paint "${source.effect}"${source.provider ? ` from provider "${source.provider}"` : ""} is not rendered at this canvas size; call set_effect_paint (or prepareEffectPaints) first.`);
|
|
4467
4564
|
return { source, pixels };
|
|
@@ -4754,6 +4851,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4754
4851
|
})();
|
|
4755
4852
|
const brush = this.strokeBrush(init);
|
|
4756
4853
|
const effect = this.strokeEffect(init, brush);
|
|
4854
|
+
if (doc.liveStrokes?.[layer.id]) {
|
|
4855
|
+
const why = liveStrokeRefusal(brush);
|
|
4856
|
+
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
4857
|
+
}
|
|
4858
|
+
if (doc.liveStrokes?.[layer.id] && effect?.source.live) {
|
|
4859
|
+
const key = effectPaintKey(effect.source);
|
|
4860
|
+
const { sources } = this.liveEffects(doc);
|
|
4861
|
+
if (!sources.some((p) => effectPaintKey(p) === key) && sources.length >= MAX_LIVE_EFFECT_SOURCES) {
|
|
4862
|
+
throw new PaintEditorError(`The painting already plays ${MAX_LIVE_EFFECT_SOURCES} live effects, the most it can; paint with one of them, or set_effect_paint with live false to bake this one.`);
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4757
4865
|
const stickerLayer = Boolean(doc.stickers?.[layer.id]?.length);
|
|
4758
4866
|
if (brush.engine === "sticker") {
|
|
4759
4867
|
if (!stickerLayer && contentBounds(layer.surface)) throw new PaintEditorError(`Layer "${layer.name}" already has paint; spray stickers onto an empty layer (add_layer) so they stay editable.`);
|
|
@@ -4793,7 +4901,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4793
4901
|
seed: seedW,
|
|
4794
4902
|
copies: [IDENTITY_AFFINE, ...copiesW].map(affineCopy),
|
|
4795
4903
|
mask: init.maskStencils ? this.stencilSelection(init.maskStencils) : init.stencils !== false ? this.stencilSelection() : null,
|
|
4796
|
-
paint: wetGradient
|
|
4904
|
+
paint: effect ? null : wetGradient,
|
|
4905
|
+
effect: effect?.pixels ?? null
|
|
4797
4906
|
});
|
|
4798
4907
|
init = { ...init, seed: seedW, load };
|
|
4799
4908
|
wet = { driver, surface: () => world.display(id).surface };
|
|
@@ -4908,6 +5017,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4908
5017
|
}
|
|
4909
5018
|
const stroke = this.record(s);
|
|
4910
5019
|
s.result.strokeId = stroke.id;
|
|
5020
|
+
this.paintedBounds.set(stroke, s.result?.bounds ?? null);
|
|
4911
5021
|
if (wetStroke && this.wet.instantDry) {
|
|
4912
5022
|
this.wet.dry(s.layerId);
|
|
4913
5023
|
this.logWet({ kind: "dry", layerId: s.layerId, at: this.doc.strokes.length + 1 });
|
|
@@ -4919,7 +5029,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4919
5029
|
const thick = ch ? { ...this.doc.thick ?? {}, [l.id]: ch } : this.doc.thick;
|
|
4920
5030
|
const sprayed = s.stickers();
|
|
4921
5031
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
4922
|
-
this.commit(`${brush.name} stroke`, this.withWetEvents({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {} }), wetStroke ? before : this.doc);
|
|
5032
|
+
this.commit(`${brush.name} stroke`, this.withWetEvents(withLiveStroke({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {} }, stroke)), wetStroke ? before : this.doc);
|
|
4923
5033
|
}
|
|
4924
5034
|
};
|
|
4925
5035
|
let session;
|
|
@@ -4947,17 +5057,18 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4947
5057
|
if (cancelled || !surface) return;
|
|
4948
5058
|
const stroke = this.record(s);
|
|
4949
5059
|
s.result.strokeId = stroke.id;
|
|
5060
|
+
this.paintedBounds.set(stroke, s.result?.bounds ?? null);
|
|
4950
5061
|
const l = findLayer(out.layers, s.layerId).layer;
|
|
4951
5062
|
const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;
|
|
4952
5063
|
const ch = s.thickChannels();
|
|
4953
5064
|
const sprayed = s.stickers();
|
|
4954
|
-
out = {
|
|
5065
|
+
out = withLiveStroke({
|
|
4955
5066
|
...out,
|
|
4956
5067
|
layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),
|
|
4957
5068
|
strokes: [...out.strokes, stroke],
|
|
4958
5069
|
...ch ? { thick: { ...out.thick ?? {}, [l.id]: ch } } : {},
|
|
4959
5070
|
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {}
|
|
4960
|
-
};
|
|
5071
|
+
}, stroke);
|
|
4961
5072
|
}
|
|
4962
5073
|
};
|
|
4963
5074
|
const session = this.sessionFor(doc, init, host);
|
|
@@ -5355,8 +5466,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5355
5466
|
}
|
|
5356
5467
|
/** The layers with the live stroke's surface in place. */
|
|
5357
5468
|
currentLayers() {
|
|
5469
|
+
const override = this.frameOverride;
|
|
5470
|
+
if (override) return Array.from(override).reduce((ls, [id, surface]) => mapLayer(ls, id, (l) => ({ ...l, surface })), this.doc.layers);
|
|
5358
5471
|
const live = this.live;
|
|
5359
|
-
if (!live) return this.
|
|
5472
|
+
if (!live) return this.animatedView();
|
|
5360
5473
|
if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;
|
|
5361
5474
|
const layers = mapLayer(this.doc.layers, live.layerId, (l) => ({ ...l, surface: live.surface }));
|
|
5362
5475
|
this.liveLayers = { key: live.surface, layers };
|
|
@@ -5501,6 +5614,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5501
5614
|
for (const [id, list] of Object.entries(this.doc.stickers ?? {})) if (list.length && findLayer(this.doc.layers, id)) stickers[id] = list.map((x) => ({ ...x }));
|
|
5502
5615
|
if (Object.keys(stickers).length) ext.stickers = stickers;
|
|
5503
5616
|
else delete ext.stickers;
|
|
5617
|
+
const liveStrokes = {};
|
|
5618
|
+
for (const [id, list] of Object.entries(this.doc.liveStrokes ?? {})) if (findLayer(this.doc.layers, id)) liveStrokes[id] = list.map((x) => JSON.parse(JSON.stringify(x)));
|
|
5619
|
+
if (Object.keys(liveStrokes).length) ext.liveStrokes = liveStrokes;
|
|
5620
|
+
else delete ext.liveStrokes;
|
|
5504
5621
|
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
5505
5622
|
if (effectLayers) ext.effectLayers = effectLayers;
|
|
5506
5623
|
else delete ext.effectLayers;
|
|
@@ -5524,6 +5641,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5524
5641
|
}
|
|
5525
5642
|
if (Object.keys(stickers).length) state = { ...state, stickers };
|
|
5526
5643
|
}
|
|
5644
|
+
if (ext.liveStrokes && typeof ext.liveStrokes === "object") {
|
|
5645
|
+
const liveStrokes = {};
|
|
5646
|
+
for (const [id, list] of Object.entries(ext.liveStrokes)) {
|
|
5647
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer) || !Array.isArray(list)) continue;
|
|
5648
|
+
const ok = list.map(normalizeBrushStroke).filter((x) => x !== null).map((x) => ({ ...x, layerId: id }));
|
|
5649
|
+
if (ok.length !== list.length) problems.push(`Some strokes of stroke layer "${id}" could not be read; they were dropped (the layer's pixels are kept until a stroke is edited).`);
|
|
5650
|
+
liveStrokes[id] = ok;
|
|
5651
|
+
}
|
|
5652
|
+
if (Object.keys(liveStrokes).length) state = { ...state, liveStrokes };
|
|
5653
|
+
}
|
|
5527
5654
|
if (ext.effectLayers && typeof ext.effectLayers === "object") {
|
|
5528
5655
|
const effectLayers = {};
|
|
5529
5656
|
for (const [id, v] of Object.entries(ext.effectLayers)) {
|
|
@@ -5574,10 +5701,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5574
5701
|
this.undoStack = [];
|
|
5575
5702
|
this.redoStack = [];
|
|
5576
5703
|
this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);
|
|
5704
|
+
for (const list of Object.values(this.doc.liveStrokes ?? {})) {
|
|
5705
|
+
for (const s of list) {
|
|
5706
|
+
const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
|
|
5707
|
+
if (n >= this.strokeCounter) this.strokeCounter = n + 1;
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5577
5710
|
this.docRevision++;
|
|
5578
5711
|
delete this.extensions.layerChannels;
|
|
5579
5712
|
delete this.extensions.lighting;
|
|
5580
5713
|
delete this.extensions.stickers;
|
|
5714
|
+
delete this.extensions.liveStrokes;
|
|
5581
5715
|
delete this.extensions.effectLayers;
|
|
5582
5716
|
delete this.extensions.paperEffect;
|
|
5583
5717
|
this.viewCompositor.clear();
|
|
@@ -6042,6 +6176,420 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6042
6176
|
const l = this.wetLayer(layerId, "set the stickers of");
|
|
6043
6177
|
this.commitStickers("Stickers", l, stickers.map((s) => ({ ...s })));
|
|
6044
6178
|
}
|
|
6179
|
+
// --- Live stroke layers ---------------------------------------------------------
|
|
6180
|
+
/** The document's `liveStrokes` without these layers (as a spread: `{}` when unchanged). */
|
|
6181
|
+
liveStrokesWithout(ids) {
|
|
6182
|
+
const t = this.doc.liveStrokes;
|
|
6183
|
+
if (!t || !ids.some((id) => t[id])) return {};
|
|
6184
|
+
const out = { ...t };
|
|
6185
|
+
for (const id of ids) delete out[id];
|
|
6186
|
+
return { liveStrokes: out };
|
|
6187
|
+
}
|
|
6188
|
+
liveStrokeLayer(layerId, what) {
|
|
6189
|
+
const l = this.wetLayer(layerId, what);
|
|
6190
|
+
const strokes = this.doc.liveStrokes?.[l.id];
|
|
6191
|
+
if (!strokes) throw new PaintEditorError(`Layer "${l.name}" is not a stroke layer; add one with add_layer {type: "strokes"} (or convert_to_stroke_layer on an empty layer), then paint on it.`);
|
|
6192
|
+
return { layer: l, strokes };
|
|
6193
|
+
}
|
|
6194
|
+
/** Whether a layer is a live stroke layer. */
|
|
6195
|
+
isLiveStrokeLayer(layerId) {
|
|
6196
|
+
return Boolean(this.doc.liveStrokes?.[layerId]);
|
|
6197
|
+
}
|
|
6198
|
+
/** Makes an empty pixel layer a live stroke layer (one undo step). */
|
|
6199
|
+
convertToLiveStrokes(layerId) {
|
|
6200
|
+
this.requireIdle("converting a layer");
|
|
6201
|
+
const l = this.paintTarget(layerId, "convert");
|
|
6202
|
+
if (this.doc.liveStrokes?.[l.id]) return this.describeLayer(l);
|
|
6203
|
+
if (this.doc.stickers?.[l.id]?.length) throw new PaintEditorError(`Layer "${l.name}" is a sticker layer; add a stroke layer instead (add_layer {type: "strokes"}).`);
|
|
6204
|
+
if (contentBounds(l.surface) || this.wet.wetLayerIds().includes(l.id)) throw new PaintEditorError(`Layer "${l.name}" already has paint, which isn't kept as strokes; convert an empty layer or add a stroke layer (add_layer {type: "strokes"}).`);
|
|
6205
|
+
this.commit("Convert to stroke layer", { ...this.doc, liveStrokes: { ...this.doc.liveStrokes ?? {}, [l.id]: [] } });
|
|
6206
|
+
return this.describeLayer(this.getLayer(l.id));
|
|
6207
|
+
}
|
|
6208
|
+
/** A stroke layer's strokes, bottom first. */
|
|
6209
|
+
listLiveStrokes(layerId) {
|
|
6210
|
+
return this.liveStrokeLayer(layerId, "list the strokes of").strokes.map((x) => ({ ...x }));
|
|
6211
|
+
}
|
|
6212
|
+
/** The stroke layer holding a live stroke, and where. */
|
|
6213
|
+
findLiveStroke(id) {
|
|
6214
|
+
for (const [layerId, strokes] of Object.entries(this.doc.liveStrokes ?? {})) {
|
|
6215
|
+
const index = strokes.findIndex((x) => x.id === id);
|
|
6216
|
+
const layer = findLayer(this.doc.layers, layerId)?.layer;
|
|
6217
|
+
if (index >= 0 && isPixelLayer(layer)) return { layer, strokes, index };
|
|
6218
|
+
}
|
|
6219
|
+
const baked = this.doc.strokes.some((x) => x.id === id);
|
|
6220
|
+
throw new PaintEditorError(
|
|
6221
|
+
baked ? `Stroke "${id}" was painted on a pixel layer, so its pixels are baked; only strokes on a stroke layer can be edited (list_strokes {live: true}). Replay it onto a stroke layer (replay_strokes layerId) to edit the copy.` : `Unknown stroke "${id}". Call list_strokes {live: true} for the ids of editable strokes.`
|
|
6222
|
+
);
|
|
6223
|
+
}
|
|
6224
|
+
/**
|
|
6225
|
+
* Changes a live stroke's brush (a preset by `brushId`, keeping the stroke's size, and/or brush
|
|
6226
|
+
* settings), colour, paint or seed, and redraws its layer (one undo step). A new colour replaces
|
|
6227
|
+
* gradient or effect paint. Returns the stroke and the area that changed.
|
|
6228
|
+
*/
|
|
6229
|
+
updateStroke(id, update) {
|
|
6230
|
+
this.requireIdle("editing a stroke");
|
|
6231
|
+
const { layer, strokes, index } = this.findLiveStroke(id);
|
|
6232
|
+
const was = strokes[index];
|
|
6233
|
+
let brush = was.brush;
|
|
6234
|
+
if (update.brushId !== void 0) {
|
|
6235
|
+
const b = this.requireBrush(update.brushId);
|
|
6236
|
+
brush = normalizePaintBrush({ tip: { ...b.tip, size: was.brush.tip.size } }, b);
|
|
6237
|
+
}
|
|
6238
|
+
if (update.brush !== void 0) {
|
|
6239
|
+
if (!update.brush || typeof update.brush !== "object") throw new PaintEditorError("brush must be an object of brush settings (as set_brush takes).");
|
|
6240
|
+
brush = normalizePaintBrush({ ...update.brush, id: brush.id }, brush);
|
|
6241
|
+
}
|
|
6242
|
+
const why = liveStrokeRefusal(brush);
|
|
6243
|
+
if (why) throw new PaintEditorError(why);
|
|
6244
|
+
const next = { ...was, brush: brushToJSON(brush) };
|
|
6245
|
+
if (update.seed !== void 0) {
|
|
6246
|
+
if (!Number.isInteger(update.seed) || update.seed < 0) throw new PaintEditorError(`seed ${update.seed} must be a whole number of 0 or more.`);
|
|
6247
|
+
next.seed = update.seed;
|
|
6248
|
+
}
|
|
6249
|
+
if (update.color !== void 0) {
|
|
6250
|
+
next.color = parseColor(update.color);
|
|
6251
|
+
delete next.paint;
|
|
6252
|
+
}
|
|
6253
|
+
if (update.paint !== void 0) {
|
|
6254
|
+
if (update.paint === null) delete next.paint;
|
|
6255
|
+
else if (isPaintEffectPaint(update.paint)) {
|
|
6256
|
+
if (!this.effectPaintImage(update.paint)) throw new PaintEditorError(`The effect "${update.paint.effect}" is not rendered yet; render it first (prepareEffectPaints; the update_stroke tool does this).`);
|
|
6257
|
+
next.paint = JSON.parse(JSON.stringify(update.paint));
|
|
6258
|
+
} else {
|
|
6259
|
+
const p = this.readStrokePaint(update.paint);
|
|
6260
|
+
if (p.type === "color") {
|
|
6261
|
+
next.color = p.color;
|
|
6262
|
+
delete next.paint;
|
|
6263
|
+
} else next.paint = JSON.parse(JSON.stringify(p));
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
const rgb = hexToRgb8(next.color).map((v) => v / 255);
|
|
6267
|
+
const load = brushLoadForStroke(brush, EMPTY_MIXER_LOAD, rgb);
|
|
6268
|
+
if (load.reservoir || load.pickup) next.load = load;
|
|
6269
|
+
else delete next.load;
|
|
6270
|
+
const list = strokes.map((x, i) => i === index ? next : x);
|
|
6271
|
+
this.commitLiveStrokes("Edit stroke", layer, list, this.strokeAreas([was]));
|
|
6272
|
+
return { stroke: { ...next }, bounds: unionRect(this.strokeArea(was), this.strokeArea(next)) ?? { x: 0, y: 0, width: 0, height: 0 } };
|
|
6273
|
+
}
|
|
6274
|
+
/**
|
|
6275
|
+
* Replaces a live stroke's points, or moves, scales, rotates or flips them (`transform`, about
|
|
6276
|
+
* the stroke's centre unless an origin is given; the brush size scales too), and redraws its
|
|
6277
|
+
* layer (one undo step). Returns the stroke and the area that changed.
|
|
6278
|
+
*/
|
|
6279
|
+
editStrokePoints(id, edit) {
|
|
6280
|
+
this.requireIdle("editing a stroke");
|
|
6281
|
+
if (edit.points === void 0 === (edit.transform === void 0)) throw new PaintEditorError("Pass either points (the new pen samples) or transform (dx, dy, scale, rotate, flipX), not both.");
|
|
6282
|
+
const { layer, strokes, index } = this.findLiveStroke(id);
|
|
6283
|
+
const was = strokes[index];
|
|
6284
|
+
let next;
|
|
6285
|
+
if (edit.points !== void 0) {
|
|
6286
|
+
const points = this.checkPoints(edit.points);
|
|
6287
|
+
next = { ...was, points: points.map((p) => ({ ...p })) };
|
|
6288
|
+
} else {
|
|
6289
|
+
const t = edit.transform;
|
|
6290
|
+
if (t.scale !== void 0 && !(t.scale > 0 && t.scale <= 20)) throw new PaintEditorError(`scale ${t.scale} is out of range; use above 0 up to 20.`);
|
|
6291
|
+
for (const k of ["dx", "dy", "rotate"]) if (t[k] !== void 0 && !Number.isFinite(t[k])) throw new PaintEditorError(`${k} must be a number.`);
|
|
6292
|
+
const b = strokeBounds(was.points);
|
|
6293
|
+
const m = variationAffine(t, t.origin ?? { x: b.x + b.width / 2, y: b.y + b.height / 2 });
|
|
6294
|
+
next = varyStroke(was, m, {});
|
|
6295
|
+
if (was.stencils) next.stencils = was.stencils;
|
|
6296
|
+
}
|
|
6297
|
+
const list = strokes.map((x, i) => i === index ? next : x);
|
|
6298
|
+
this.commitLiveStrokes("Edit stroke points", layer, list, this.strokeAreas([was]));
|
|
6299
|
+
return { stroke: { ...next }, bounds: unionRect(this.strokeArea(was), this.strokeArea(next)) ?? { x: 0, y: 0, width: 0, height: 0 } };
|
|
6300
|
+
}
|
|
6301
|
+
/** Deletes live strokes (one undo step). Returns the ids removed and the area that changed. */
|
|
6302
|
+
removeStrokes(ids) {
|
|
6303
|
+
this.requireIdle("removing strokes");
|
|
6304
|
+
if (!ids.length) throw new PaintEditorError("Pass at least one stroke id (list_strokes {live: true}).");
|
|
6305
|
+
const found = ids.map((id) => this.findLiveStroke(id));
|
|
6306
|
+
let doc = this.doc;
|
|
6307
|
+
let bounds = null;
|
|
6308
|
+
const layers = /* @__PURE__ */ new Map();
|
|
6309
|
+
for (const f of found) layers.set(f.layer.id, f.layer);
|
|
6310
|
+
for (const [layerId, layer] of layers) {
|
|
6311
|
+
const gone = (doc.liveStrokes?.[layerId] ?? []).filter((x) => ids.includes(x.id));
|
|
6312
|
+
for (const x of gone) bounds = unionRect(bounds, this.strokeArea(x));
|
|
6313
|
+
doc = this.withLiveStrokes(doc, layer, (doc.liveStrokes?.[layerId] ?? []).filter((x) => !ids.includes(x.id)), this.strokeAreas(gone));
|
|
6314
|
+
}
|
|
6315
|
+
this.commit(ids.length === 1 ? "Remove stroke" : "Remove strokes", doc);
|
|
6316
|
+
return { removed: [...ids], bounds };
|
|
6317
|
+
}
|
|
6318
|
+
/** Moves a live stroke to `index` in its layer (0 = bottom) and redraws (one undo step). Returns the new order. */
|
|
6319
|
+
reorderStroke(id, index) {
|
|
6320
|
+
this.requireIdle("reordering strokes");
|
|
6321
|
+
const { layer, strokes, index: from } = this.findLiveStroke(id);
|
|
6322
|
+
if (!Number.isInteger(index) || index < 0 || index >= strokes.length) throw new PaintEditorError(`index ${index} is out of range; the layer has ${strokes.length} strokes, so use 0..${strokes.length - 1} (0 = bottom).`);
|
|
6323
|
+
const list = [...strokes];
|
|
6324
|
+
const [moved] = list.splice(from, 1);
|
|
6325
|
+
list.splice(index, 0, moved);
|
|
6326
|
+
if (from !== index) this.commitLiveStrokes("Reorder stroke", layer, list, this.strokeAreas([moved]));
|
|
6327
|
+
return list.map((x) => x.id);
|
|
6328
|
+
}
|
|
6329
|
+
/** Turns a stroke layer into a plain pixel layer with the same pixels (one undo step). */
|
|
6330
|
+
rasterizeLiveStrokes(layerId) {
|
|
6331
|
+
this.requireIdle("rasterizing");
|
|
6332
|
+
const { layer } = this.liveStrokeLayer(layerId, "rasterize");
|
|
6333
|
+
this.commit("Rasterize stroke layer", this.withPaintEvent({ ...this.doc, ...this.liveStrokesWithout([layer.id]) }, { kind: "live-strokes", layerId: layer.id, data: { rasterized: true } }));
|
|
6334
|
+
return this.describeLayer(this.getLayer(layer.id));
|
|
6335
|
+
}
|
|
6336
|
+
/** Sets a layer's live strokes and redraws it (replays). `null` makes it a plain pixel layer. */
|
|
6337
|
+
replaceLiveStrokes(layerId, strokes) {
|
|
6338
|
+
const l = this.wetLayer(layerId, "set the strokes of");
|
|
6339
|
+
if (strokes === null) {
|
|
6340
|
+
this.commit("Rasterize stroke layer", { ...this.doc, ...this.liveStrokesWithout([l.id]) });
|
|
6341
|
+
return;
|
|
6342
|
+
}
|
|
6343
|
+
this.commitLiveStrokes("Strokes", l, strokes.map((x) => ({ ...x, layerId: l.id })));
|
|
6344
|
+
}
|
|
6345
|
+
/**
|
|
6346
|
+
* The top-most stroke of a stroke layer (default the active layer) that painted the pixel at
|
|
6347
|
+
* (x, y) document pixels, or `null`. Strokes whose area holds the point are drawn alone, top
|
|
6348
|
+
* first, until one covers it.
|
|
6349
|
+
*/
|
|
6350
|
+
liveStrokeAt(layerId, x, y) {
|
|
6351
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new PaintEditorError("x and y must be numbers of document pixels.");
|
|
6352
|
+
const { layer, strokes } = this.liveStrokeLayer(layerId, "pick a stroke on");
|
|
6353
|
+
const plain = { ...layer, visible: true, locks: void 0, offset: { x: 0, y: 0 } };
|
|
6354
|
+
const doc = { ...this.doc, liveStrokes: void 0, stickers: void 0 };
|
|
6355
|
+
const px = Math.floor(x);
|
|
6356
|
+
const py = Math.floor(y);
|
|
6357
|
+
for (let i = strokes.length - 1; i >= 0; i--) {
|
|
6358
|
+
const area = this.strokeArea(strokes[i]);
|
|
6359
|
+
if (!area || px < area.x || py < area.y || px >= area.x + area.width || py >= area.y + area.height) continue;
|
|
6360
|
+
const drawn = this.drawLiveStroke(doc, plain, emptySurface(4), strokes[i]).surface;
|
|
6361
|
+
if (readPixel(drawn, px, py)[3] > 24) return { ...strokes[i] };
|
|
6362
|
+
}
|
|
6363
|
+
return null;
|
|
6364
|
+
}
|
|
6365
|
+
/** Picks a stroke for the stroke panel and canvas (`null` for none). Editor state, not an undo step. */
|
|
6366
|
+
selectStroke(id) {
|
|
6367
|
+
if (id === this.selectedStroke) return;
|
|
6368
|
+
this.selectedStroke = id;
|
|
6369
|
+
this.emit([]);
|
|
6370
|
+
}
|
|
6371
|
+
/** Where a live stroke painted (document pixels), or `null`. */
|
|
6372
|
+
liveStrokeArea(id) {
|
|
6373
|
+
return this.strokeArea(this.findLiveStroke(id).strokes.find((s) => s.id === id));
|
|
6374
|
+
}
|
|
6375
|
+
/** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
|
|
6376
|
+
commitLiveStrokes(label, layer, list, dirty) {
|
|
6377
|
+
this.commit(label, this.withLiveStrokes(this.doc, layer, list, dirty));
|
|
6378
|
+
}
|
|
6379
|
+
withLiveStrokes(doc, layer, list, dirty) {
|
|
6380
|
+
if (isLayerLocked(layer, "pixels")) throw new PaintEditorError(`Layer "${layer.name}" has its pixels locked; unlock it (update_layer locks) first.`);
|
|
6381
|
+
const current = findLayer(doc.layers, layer.id)?.layer;
|
|
6382
|
+
const surface = this.drawLiveStrokes(current ?? layer, list, dirty);
|
|
6383
|
+
const next = { ...doc, layers: mapLayer(doc.layers, layer.id, (l) => ({ ...l, surface, offset: { x: 0, y: 0 } })), liveStrokes: { ...doc.liveStrokes ?? {}, [layer.id]: [...list] } };
|
|
6384
|
+
return this.withPaintEvent(next, { kind: "live-strokes", layerId: layer.id, data: { strokes: list.map((x) => JSON.parse(JSON.stringify(x))) } });
|
|
6385
|
+
}
|
|
6386
|
+
/** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
|
|
6387
|
+
strokeArea(s) {
|
|
6388
|
+
return this.paintedBounds.has(s) ? this.paintedBounds.get(s) : liveStrokeBounds(s);
|
|
6389
|
+
}
|
|
6390
|
+
/** The areas strokes painted, or `null` when one is not known (a full redraw then). */
|
|
6391
|
+
strokeAreas(strokes) {
|
|
6392
|
+
const out = [];
|
|
6393
|
+
for (const s of strokes) {
|
|
6394
|
+
if (!this.paintedBounds.has(s)) return null;
|
|
6395
|
+
const b = this.paintedBounds.get(s);
|
|
6396
|
+
if (b) out.push(b);
|
|
6397
|
+
}
|
|
6398
|
+
return out;
|
|
6399
|
+
}
|
|
6400
|
+
/** Draws one stroke onto `surface` of a plain copy of the layer; returns the result and the area it painted. */
|
|
6401
|
+
drawLiveStroke(doc, plain, surface, st) {
|
|
6402
|
+
let out = surface;
|
|
6403
|
+
const host = {
|
|
6404
|
+
update: () => {
|
|
6405
|
+
},
|
|
6406
|
+
commit: (_s, drawn, cancelled) => {
|
|
6407
|
+
if (!cancelled && drawn) out = drawn;
|
|
6408
|
+
}
|
|
6409
|
+
};
|
|
6410
|
+
const paint = st.paint && this.liveEffectAt !== null && isLiveEffect(st) ? framePaint(st.paint, this.liveEffectAt) : st.paint;
|
|
6411
|
+
const init = { layerId: plain.id, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...paint ? { paint } : {}, snapToPerspective: false, stencils: false, maskStencils: st.stencils ?? [] };
|
|
6412
|
+
const session = this.sessionFor({ ...doc, layers: mapLayer(doc.layers, plain.id, () => ({ ...plain, surface })) }, init, host);
|
|
6413
|
+
session.push(st.points);
|
|
6414
|
+
const result = session.end();
|
|
6415
|
+
const had = this.paintedBounds.get(st);
|
|
6416
|
+
this.paintedBounds.set(st, had && result.bounds ? unionRect(had, result.bounds) : result.bounds ?? had ?? null);
|
|
6417
|
+
return { surface: out, bounds: result.bounds };
|
|
6418
|
+
}
|
|
6419
|
+
/**
|
|
6420
|
+
* A stroke layer's pixels drawn from its strokes, bottom first. With `dirty` (the areas the old
|
|
6421
|
+
* strokes that changed had painted), only the tiles there and under any stroke whose area isn't
|
|
6422
|
+
* known yet are drawn again, from the strokes that reach them (a pixel depends only on the
|
|
6423
|
+
* strokes over it); the other tiles are kept. Without it, or with `null`, every stroke is drawn.
|
|
6424
|
+
*/
|
|
6425
|
+
drawLiveStrokes(layer, list, dirty) {
|
|
6426
|
+
const plain = { ...layer, visible: true, locks: void 0, offset: { x: 0, y: 0 } };
|
|
6427
|
+
const doc = { ...this.doc, liveStrokes: void 0, stickers: void 0 };
|
|
6428
|
+
const unknown = list.filter((s) => !this.paintedBounds.has(s));
|
|
6429
|
+
const partial = dirty && !layer.offset.x && !layer.offset.y && unknown.length <= 4;
|
|
6430
|
+
if (!partial) {
|
|
6431
|
+
let surface2 = emptySurface(4);
|
|
6432
|
+
for (const st of list) surface2 = this.drawLiveStroke(doc, plain, surface2, st).surface;
|
|
6433
|
+
return surface2;
|
|
6434
|
+
}
|
|
6435
|
+
const areas = [...dirty];
|
|
6436
|
+
for (const st of unknown) {
|
|
6437
|
+
const b = this.drawLiveStroke(doc, plain, emptySurface(4), st).bounds;
|
|
6438
|
+
if (b) areas.push(b);
|
|
6439
|
+
}
|
|
6440
|
+
const { width, height } = this.doc.canvas;
|
|
6441
|
+
const rects = areas.map((r) => clipRect(r, width, height)).filter((r) => r !== null);
|
|
6442
|
+
if (!rects.length) return layer.surface;
|
|
6443
|
+
const overlaps = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
6444
|
+
let drawn = emptySurface(4);
|
|
6445
|
+
for (const st of list) {
|
|
6446
|
+
const b = this.paintedBounds.get(st);
|
|
6447
|
+
if (b && rects.some((r) => overlaps(r, b))) drawn = this.drawLiveStroke(doc, plain, drawn, st).surface;
|
|
6448
|
+
}
|
|
6449
|
+
let surface = layer.surface;
|
|
6450
|
+
for (const r of rects) surface = writeRegion(surface, r, readRegion(drawn, r));
|
|
6451
|
+
return surface;
|
|
6452
|
+
}
|
|
6453
|
+
// --- Live effect paint ------------------------------------------------------------
|
|
6454
|
+
/** The view's layers: stroke layers playing live effects at `effectTime`, the rest as they are. */
|
|
6455
|
+
animatedView() {
|
|
6456
|
+
if (!this.animatedLayers.size) return this.doc.layers;
|
|
6457
|
+
let layers = this.doc.layers;
|
|
6458
|
+
for (const [id, a] of this.animatedLayers) {
|
|
6459
|
+
const l = findLayer(layers, id)?.layer;
|
|
6460
|
+
if (isPixelLayer(l) && l.surface === a.base) layers = mapLayer(layers, id, () => ({ ...l, surface: a.surface }));
|
|
6461
|
+
}
|
|
6462
|
+
return layers;
|
|
6463
|
+
}
|
|
6464
|
+
/** Stroke layers with live effect strokes, and the distinct live sources (frame 0). */
|
|
6465
|
+
liveEffects(doc = this.doc) {
|
|
6466
|
+
const layers = [];
|
|
6467
|
+
const sources = /* @__PURE__ */ new Map();
|
|
6468
|
+
for (const [id, strokes] of Object.entries(doc.liveStrokes ?? {})) {
|
|
6469
|
+
const layer = findLayer(doc.layers, id)?.layer;
|
|
6470
|
+
const live = strokes.filter(isLiveEffect);
|
|
6471
|
+
if (!isPixelLayer(layer) || !live.length) continue;
|
|
6472
|
+
layers.push({ layer, strokes });
|
|
6473
|
+
for (const st of live) sources.set(effectPaintKey(st.paint), st.paint);
|
|
6474
|
+
}
|
|
6475
|
+
return { layers, sources: [...sources.values()] };
|
|
6476
|
+
}
|
|
6477
|
+
/** What plays: the live effect strokes and their layers, or `null` when nothing does. */
|
|
6478
|
+
getAnimation() {
|
|
6479
|
+
const { layers, sources } = this.liveEffects();
|
|
6480
|
+
if (!layers.length) return null;
|
|
6481
|
+
return { layerIds: layers.map((x) => x.layer.id), strokes: layers.reduce((n, x) => n + x.strokes.filter(isLiveEffect).length, 0), sources: sources.length };
|
|
6482
|
+
}
|
|
6483
|
+
/** Plays or pauses live effect paint, and caps its frame rate (1..60). Editor state, not an undo step. */
|
|
6484
|
+
setEffectPlayback(update) {
|
|
6485
|
+
if (update.playing !== void 0 && typeof update.playing !== "boolean") throw new PaintEditorError("playing must be true or false.");
|
|
6486
|
+
if (update.fps !== void 0 && !(Number.isInteger(update.fps) && update.fps >= 1 && update.fps <= 60)) throw new PaintEditorError(`fps ${update.fps} is out of range; use a whole number 1..60.`);
|
|
6487
|
+
this.effectPlaybackState = { ...this.effectPlaybackState, ...update };
|
|
6488
|
+
this.emit([]);
|
|
6489
|
+
return { ...this.effectPlaybackState, time: this.effectTime };
|
|
6490
|
+
}
|
|
6491
|
+
/** The surfaces of every stroke layer with live effects at `time` ms, drawn in full (frames rendered first). */
|
|
6492
|
+
async liveSurfacesAt(time) {
|
|
6493
|
+
const { layers, sources } = this.liveEffects();
|
|
6494
|
+
await this.prepareEffectPaints(sources.map((p) => framePaint(p, time)));
|
|
6495
|
+
const out = /* @__PURE__ */ new Map();
|
|
6496
|
+
this.liveEffectAt = time;
|
|
6497
|
+
try {
|
|
6498
|
+
for (const { layer, strokes } of layers) out.set(layer.id, this.drawLiveStrokes({ ...layer, offset: { x: 0, y: 0 } }, strokes));
|
|
6499
|
+
} finally {
|
|
6500
|
+
this.liveEffectAt = null;
|
|
6501
|
+
}
|
|
6502
|
+
return out;
|
|
6503
|
+
}
|
|
6504
|
+
/**
|
|
6505
|
+
* Shows the live effects at `time` ms (each live stroke's effect at its own time plus `time`):
|
|
6506
|
+
* renders their frames, then redraws only where live effect strokes paint. Nothing in the
|
|
6507
|
+
* document changes. Resolves `false` when the document changed while frames rendered (the next
|
|
6508
|
+
* frame catches up).
|
|
6509
|
+
*/
|
|
6510
|
+
async showEffectTime(time) {
|
|
6511
|
+
this.checkTime(time);
|
|
6512
|
+
const { layers, sources } = this.liveEffects();
|
|
6513
|
+
const revision = this.docRevision;
|
|
6514
|
+
await this.prepareEffectPaints(sources.map((p) => framePaint(p, time)));
|
|
6515
|
+
if (revision !== this.docRevision || this.session) return false;
|
|
6516
|
+
const next = /* @__PURE__ */ new Map();
|
|
6517
|
+
const dirty = [];
|
|
6518
|
+
this.liveEffectAt = time;
|
|
6519
|
+
try {
|
|
6520
|
+
for (const { layer, strokes } of layers) {
|
|
6521
|
+
const shown = this.animatedLayers.get(layer.id);
|
|
6522
|
+
const from = shown && shown.base === layer.surface ? shown.surface : layer.surface;
|
|
6523
|
+
const areas = strokes.filter(isLiveEffect).flatMap((st) => [liveStrokeBounds(st), ...this.paintedBounds.get(st) ? [this.paintedBounds.get(st)] : []]);
|
|
6524
|
+
const surface = this.drawLiveStrokes({ ...layer, surface: from, offset: { x: 0, y: 0 } }, strokes, layer.offset.x || layer.offset.y ? null : areas);
|
|
6525
|
+
next.set(layer.id, { base: layer.surface, surface });
|
|
6526
|
+
dirty.push(...areas);
|
|
6527
|
+
}
|
|
6528
|
+
} finally {
|
|
6529
|
+
this.liveEffectAt = null;
|
|
6530
|
+
}
|
|
6531
|
+
this.animatedLayers = next;
|
|
6532
|
+
this.effectTime = time;
|
|
6533
|
+
this.emit(dirty);
|
|
6534
|
+
return true;
|
|
6535
|
+
}
|
|
6536
|
+
/**
|
|
6537
|
+
* Plays live effect paint in the view until `stop`: a frame per tick of `clock` (default
|
|
6538
|
+
* animation frames) at most `fps` a second, one render in flight. Idle while paused, while a
|
|
6539
|
+
* stroke is painted, or when nothing is live. The bindings start it while the editor is shown.
|
|
6540
|
+
*/
|
|
6541
|
+
playEffects(clock = defaultEffectClock) {
|
|
6542
|
+
let stopped = false;
|
|
6543
|
+
let cancel = () => {
|
|
6544
|
+
};
|
|
6545
|
+
let last = -Infinity;
|
|
6546
|
+
let started = clock.now() - this.effectTime;
|
|
6547
|
+
const tick = () => {
|
|
6548
|
+
if (stopped) return;
|
|
6549
|
+
const now = clock.now();
|
|
6550
|
+
const { playing, fps } = this.effectPlaybackState;
|
|
6551
|
+
if (!playing) started = now - this.effectTime;
|
|
6552
|
+
if (!playing || this.session || now - last < 1e3 / fps - 1 || !this.liveEffects().layers.length) {
|
|
6553
|
+
cancel = clock.frame(tick);
|
|
6554
|
+
return;
|
|
6555
|
+
}
|
|
6556
|
+
last = now;
|
|
6557
|
+
this.showEffectTime(Math.max(0, now - started)).then(
|
|
6558
|
+
() => {
|
|
6559
|
+
if (!stopped) cancel = clock.frame(tick);
|
|
6560
|
+
},
|
|
6561
|
+
() => {
|
|
6562
|
+
if (!stopped) cancel = clock.frame(tick);
|
|
6563
|
+
}
|
|
6564
|
+
);
|
|
6565
|
+
};
|
|
6566
|
+
cancel = clock.frame(tick);
|
|
6567
|
+
return {
|
|
6568
|
+
stop() {
|
|
6569
|
+
stopped = true;
|
|
6570
|
+
cancel();
|
|
6571
|
+
}
|
|
6572
|
+
};
|
|
6573
|
+
}
|
|
6574
|
+
/**
|
|
6575
|
+
* Runs `fn` with the painting as it is `time` ms into its live effects (see `getAnimation`): for
|
|
6576
|
+
* exports at a moment. Nothing in the document changes.
|
|
6577
|
+
*/
|
|
6578
|
+
async withEffectTime(time, fn) {
|
|
6579
|
+
this.checkTime(time);
|
|
6580
|
+
const surfaces = await this.liveSurfacesAt(time);
|
|
6581
|
+
const was = this.frameOverride;
|
|
6582
|
+
this.frameOverride = surfaces;
|
|
6583
|
+
try {
|
|
6584
|
+
return await fn();
|
|
6585
|
+
} finally {
|
|
6586
|
+
this.frameOverride = was;
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
/** The painting composited `time` ms into its live effects, like `compositePixels`. Nothing in the document changes. */
|
|
6590
|
+
async renderAnimationFrame(time, options = {}) {
|
|
6591
|
+
return this.withEffectTime(time, () => this.compositePixels(options));
|
|
6592
|
+
}
|
|
6045
6593
|
// --- Stencils --------------------------------------------------------------------
|
|
6046
6594
|
stencilCache = null;
|
|
6047
6595
|
nextStencilId = 1;
|
|
@@ -6259,6 +6807,35 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6259
6807
|
var MAX_IMAGE_SIDE = 8192;
|
|
6260
6808
|
var movePoint = (p, dx, dy) => ({ x: p.x + dx, y: p.y + dy });
|
|
6261
6809
|
var moveRect = (r, dx, dy) => ({ ...r, x: r.x + dx, y: r.y + dy });
|
|
6810
|
+
function isLiveEffect(s) {
|
|
6811
|
+
return s.paint?.type === "effect" && s.paint.live === true;
|
|
6812
|
+
}
|
|
6813
|
+
function framePaint(p, t) {
|
|
6814
|
+
return { ...p, time: p.time + t };
|
|
6815
|
+
}
|
|
6816
|
+
var MAX_LIVE_EFFECT_SOURCES = 8;
|
|
6817
|
+
var defaultEffectClock = {
|
|
6818
|
+
now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
|
|
6819
|
+
frame: (cb) => {
|
|
6820
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
6821
|
+
const t = setTimeout(cb, 16);
|
|
6822
|
+
return () => clearTimeout(t);
|
|
6823
|
+
}
|
|
6824
|
+
const id = requestAnimationFrame(cb);
|
|
6825
|
+
return () => cancelAnimationFrame(id);
|
|
6826
|
+
}
|
|
6827
|
+
};
|
|
6828
|
+
function clipRect(r, width, height) {
|
|
6829
|
+
const x0 = Math.max(0, Math.floor(r.x));
|
|
6830
|
+
const y0 = Math.max(0, Math.floor(r.y));
|
|
6831
|
+
const x1 = Math.min(width, Math.ceil(r.x + r.width));
|
|
6832
|
+
const y1 = Math.min(height, Math.ceil(r.y + r.height));
|
|
6833
|
+
return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
|
|
6834
|
+
}
|
|
6835
|
+
function withLiveStroke(doc, stroke) {
|
|
6836
|
+
const list = doc.liveStrokes?.[stroke.layerId];
|
|
6837
|
+
return list ? { ...doc, liveStrokes: { ...doc.liveStrokes, [stroke.layerId]: [...list, stroke] } } : doc;
|
|
6838
|
+
}
|
|
6262
6839
|
function shiftDocument(doc, dx, dy) {
|
|
6263
6840
|
if (!dx && !dy) return doc;
|
|
6264
6841
|
let layers = doc.layers;
|
|
@@ -6275,11 +6852,18 @@ function shiftDocument(doc, dx, dy) {
|
|
|
6275
6852
|
return s.stencils ? { ...moved, stencils: s.stencils.map((x) => translateStencil(x, dx, dy)) } : moved;
|
|
6276
6853
|
})
|
|
6277
6854
|
};
|
|
6855
|
+
const moveStroke = (s) => {
|
|
6856
|
+
const moved = varyStroke(s, m, {});
|
|
6857
|
+
return s.stencils ? { ...moved, stencils: s.stencils.map((x) => translateStencil(x, dx, dy)) } : moved;
|
|
6858
|
+
};
|
|
6278
6859
|
if (doc.stickers) out.stickers = Object.fromEntries(Object.entries(doc.stickers).map(([id, list]) => [id, moveStickers(list)]));
|
|
6860
|
+
if (doc.liveStrokes) out.liveStrokes = Object.fromEntries(Object.entries(doc.liveStrokes).map(([id, list]) => [id, list.map(moveStroke)]));
|
|
6279
6861
|
if (doc.paintEvents) {
|
|
6280
6862
|
out.paintEvents = doc.paintEvents.map((e) => {
|
|
6281
6863
|
const list = e.kind === "stickers" ? e.data?.stickers : void 0;
|
|
6282
|
-
|
|
6864
|
+
if (list) return { ...e, data: { ...e.data, stickers: moveStickers(list) } };
|
|
6865
|
+
const strokes = e.kind === "live-strokes" ? e.data?.strokes : void 0;
|
|
6866
|
+
return strokes ? { ...e, data: { ...e.data, strokes: strokes.map(moveStroke) } } : e;
|
|
6283
6867
|
});
|
|
6284
6868
|
}
|
|
6285
6869
|
if (doc.wetEvents) {
|
|
@@ -7694,6 +8278,7 @@ var BRUSH_FIELDS = {
|
|
|
7694
8278
|
sticker: STICKER_BRUSH,
|
|
7695
8279
|
paint: BRUSH_PAINT
|
|
7696
8280
|
};
|
|
8281
|
+
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)." };
|
|
7697
8282
|
var POINTS = {
|
|
7698
8283
|
type: "array",
|
|
7699
8284
|
minItems: 1,
|
|
@@ -7899,12 +8484,12 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
7899
8484
|
add({
|
|
7900
8485
|
name: "add_layer",
|
|
7901
8486
|
title: "Add a layer",
|
|
7902
|
-
description: "Adds an empty pixel layer
|
|
8487
|
+
description: "Adds an empty pixel layer, a group, or a stroke layer (type strokes: its strokes stay editable with update_stroke and edit_stroke_points) above the active layer, or above aboveId, or at the top of parentId; it becomes the active layer. One undo step. Returns the layer (liveStrokes: its stroke count, for a stroke layer).",
|
|
7903
8488
|
inputSchema: {
|
|
7904
8489
|
type: "object",
|
|
7905
8490
|
properties: {
|
|
7906
8491
|
name: { type: "string", minLength: 1, description: 'Layer name (default "Layer n").' },
|
|
7907
|
-
type: { type: "string", enum: ["pixel", "group"], description: "pixel (default) or
|
|
8492
|
+
type: { type: "string", enum: ["pixel", "group", "strokes"], description: "pixel (default), group, or strokes: a live stroke layer whose pixels are drawn from its strokes, so each stays editable. It takes dab, particle, bristle and Mixer Brush strokes that pick up no paint." },
|
|
7908
8493
|
aboveId: { type: "string", description: "Put it directly above this layer (default the active layer)." },
|
|
7909
8494
|
parentId: { type: "string", description: "Put it at the top of this group instead." }
|
|
7910
8495
|
},
|
|
@@ -8266,44 +8851,156 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8266
8851
|
add({
|
|
8267
8852
|
name: "list_strokes",
|
|
8268
8853
|
title: "List recorded strokes",
|
|
8269
|
-
description: "Lists the recorded strokes, oldest first (every stroke is kept as data): id, layer, brush id and name, colour, point count, bounds and
|
|
8854
|
+
description: "Lists the recorded strokes, oldest first (every stroke is kept as data): id, layer, brush id and name, colour, point count, bounds, seed and whether it is live. With ids, also the points. Use the ids with replay_strokes. With live true, lists the strokes of stroke layers as they are now, bottom first: the ones update_stroke, edit_stroke_points, reorder_stroke and remove_strokes change.",
|
|
8270
8855
|
inputSchema: {
|
|
8271
8856
|
type: "object",
|
|
8272
8857
|
properties: {
|
|
8273
8858
|
ids: { type: "array", items: { type: "string", description: "Stroke id." }, description: "Only these strokes, with their points." },
|
|
8274
|
-
last: { type: "integer", minimum: 1, maximum: 1e3, description: "Only the most recent n strokes (default 100)." }
|
|
8859
|
+
last: { type: "integer", minimum: 1, maximum: 1e3, description: "Only the most recent n strokes (default 100)." },
|
|
8860
|
+
live: { type: "boolean", description: "Only the editable strokes of stroke layers, as they are now, bottom first (default false: the whole recorded history)." },
|
|
8861
|
+
layerId: { type: "string", description: "Only strokes of this layer." }
|
|
8275
8862
|
},
|
|
8276
8863
|
additionalProperties: false
|
|
8277
8864
|
},
|
|
8278
8865
|
annotations: { title: "List recorded strokes", ...READ6 },
|
|
8279
8866
|
coordinates: { out: ["strokes[].bounds", "strokes[].points[]"] },
|
|
8280
|
-
handler: ({ ids, last }) => {
|
|
8281
|
-
|
|
8867
|
+
handler: ({ ids, last, live, layerId }) => {
|
|
8868
|
+
if (layerId !== void 0 && !c.getLayer(layerId)) throw new Error(`Unknown layer "${layerId}". Call list_layers for the ids.`);
|
|
8869
|
+
const liveAll = Object.values(c.document.liveStrokes ?? {}).flat();
|
|
8870
|
+
const liveIds = new Set(liveAll.map((s) => s.id));
|
|
8871
|
+
let all = live ? liveAll : c.getStrokes();
|
|
8872
|
+
if (layerId !== void 0) all = all.filter((s) => s.layerId === layerId);
|
|
8282
8873
|
const picked = ids ? all.filter((s) => ids.includes(s.id)) : all.slice(-(last ?? 100));
|
|
8283
8874
|
if (ids) {
|
|
8284
8875
|
for (const id of ids) if (!all.some((s) => s.id === id)) throw new Error(`Unknown stroke "${id}". Call list_strokes without ids for the ids.`);
|
|
8285
8876
|
}
|
|
8286
|
-
return {
|
|
8287
|
-
total: all.length,
|
|
8288
|
-
strokes: picked.map((s) => {
|
|
8289
|
-
const xs = s.points.map((p) => p.x);
|
|
8290
|
-
const ys = s.points.map((p) => p.y);
|
|
8291
|
-
const pad = s.brush.tip.size / 2;
|
|
8292
|
-
return {
|
|
8293
|
-
id: s.id,
|
|
8294
|
-
layerId: s.layerId,
|
|
8295
|
-
brushId: s.brush.id,
|
|
8296
|
-
brushName: s.brush.name,
|
|
8297
|
-
color: s.color,
|
|
8298
|
-
points: ids ? s.points : s.points.length,
|
|
8299
|
-
seed: s.seed,
|
|
8300
|
-
copies: s.copies?.length ?? 0,
|
|
8301
|
-
bounds: { x: Math.floor(Math.min(...xs) - pad), y: Math.floor(Math.min(...ys) - pad), width: Math.ceil(Math.max(...xs) - Math.min(...xs) + 2 * pad), height: Math.ceil(Math.max(...ys) - Math.min(...ys) + 2 * pad) }
|
|
8302
|
-
};
|
|
8303
|
-
})
|
|
8304
|
-
};
|
|
8877
|
+
return { total: all.length, strokes: picked.map((s) => strokeSummary(s, Boolean(ids), live ? true : liveIds.has(s.id) && c.document.liveStrokes?.[s.layerId]?.includes(s) === true)) };
|
|
8305
8878
|
}
|
|
8306
8879
|
});
|
|
8880
|
+
add({
|
|
8881
|
+
name: "stroke_at",
|
|
8882
|
+
title: "Find the stroke at a point",
|
|
8883
|
+
description: "Returns the top-most stroke of a stroke layer that painted the pixel at a point (document pixels), or stroke null when none did, so you can pick one stroke among overlapping ones before update_stroke, edit_stroke_points or remove_strokes. Changes nothing.",
|
|
8884
|
+
inputSchema: {
|
|
8885
|
+
type: "object",
|
|
8886
|
+
properties: {
|
|
8887
|
+
layerId: { type: "string", description: "Stroke layer id from list_layers (default the active layer)." },
|
|
8888
|
+
x: { type: "number", description: "Point x in document pixels." },
|
|
8889
|
+
y: { type: "number", description: "Point y in document pixels." }
|
|
8890
|
+
},
|
|
8891
|
+
required: ["x", "y"],
|
|
8892
|
+
additionalProperties: false
|
|
8893
|
+
},
|
|
8894
|
+
annotations: { title: "Find the stroke at a point", ...READ6 },
|
|
8895
|
+
coordinates: { in: [""], out: ["stroke.bounds"] },
|
|
8896
|
+
handler: ({ layerId, x, y }) => {
|
|
8897
|
+
const s = c.liveStrokeAt(layerId, x, y);
|
|
8898
|
+
return { stroke: s ? strokeSummary(s, false, true) : null };
|
|
8899
|
+
}
|
|
8900
|
+
});
|
|
8901
|
+
add({
|
|
8902
|
+
name: "update_stroke",
|
|
8903
|
+
title: "Change a stroke",
|
|
8904
|
+
description: "Changes a stroke on a stroke layer after it was painted: another brush preset (brushId, keeping the stroke size), brush settings (as set_brush takes), colour (replaces gradient paint), paint, or seed, then redraws the layer exactly. Ids come from list_strokes {live: true}. Refuses brushes a stroke layer cannot redraw (wet, impasto, sticker, eraser, smudge, blender, pick-up). One undo step. Returns the stroke and the area that changed.",
|
|
8905
|
+
inputSchema: {
|
|
8906
|
+
type: "object",
|
|
8907
|
+
properties: {
|
|
8908
|
+
id: { type: "string", description: "Stroke id from list_strokes {live: true}." },
|
|
8909
|
+
brushId: { type: "string", description: "Brush preset id from list_brushes; the stroke keeps its size." },
|
|
8910
|
+
brush: { type: "object", properties: BRUSH_FIELDS, additionalProperties: false, description: "Brush settings to change on top of brushId or the stroke\u2019s brush; missing fields keep their values." },
|
|
8911
|
+
color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "New colour #rrggbb (replaces gradient paint)." },
|
|
8912
|
+
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." },
|
|
8913
|
+
seed: { type: "integer", minimum: 0, description: "Another jitter seed: particles, scatter and texture fall differently." }
|
|
8914
|
+
},
|
|
8915
|
+
required: ["id"],
|
|
8916
|
+
additionalProperties: false
|
|
8917
|
+
},
|
|
8918
|
+
annotations: { title: "Change a stroke", ...WRITE5 },
|
|
8919
|
+
coordinates: { out: ["bounds", "stroke.bounds"] },
|
|
8920
|
+
handler: ({ id, ...update }) => {
|
|
8921
|
+
if (!Object.keys(update).length) throw new Error("Pass at least one change: brushId, brush, color, paint or seed.");
|
|
8922
|
+
const r = c.updateStroke(id, update);
|
|
8923
|
+
return { stroke: strokeSummary(r.stroke, false, true), bounds: r.bounds };
|
|
8924
|
+
}
|
|
8925
|
+
});
|
|
8926
|
+
add({
|
|
8927
|
+
name: "edit_stroke_points",
|
|
8928
|
+
title: "Move or reshape a stroke",
|
|
8929
|
+
description: "Replaces a stroke\u2019s points (points, document pixels), or moves, scales, rotates or flips it (transform, about its centre unless origin is given; the brush size scales with it), then redraws its stroke layer exactly. Pass one of points or transform. One undo step. Returns the stroke with its points and the area that changed.",
|
|
8930
|
+
inputSchema: {
|
|
8931
|
+
type: "object",
|
|
8932
|
+
properties: {
|
|
8933
|
+
id: { type: "string", description: "Stroke id from list_strokes {live: true}." },
|
|
8934
|
+
points: POINTS,
|
|
8935
|
+
transform: {
|
|
8936
|
+
type: "object",
|
|
8937
|
+
description: "A move, scale, rotation or flip of the whole stroke.",
|
|
8938
|
+
properties: {
|
|
8939
|
+
dx: { type: "number", description: "Move right by this many pixels (default 0)." },
|
|
8940
|
+
dy: { type: "number", description: "Move down by this many pixels (default 0)." },
|
|
8941
|
+
scale: { type: "number", exclusiveMinimum: 0, maximum: 20, description: "Scale about the origin; brush size scales too (default 1)." },
|
|
8942
|
+
rotate: { type: "number", minimum: -360, maximum: 360, description: "Rotate about the origin, degrees clockwise (default 0)." },
|
|
8943
|
+
flipX: { type: "boolean", description: "Mirror left to right about the origin (default false)." },
|
|
8944
|
+
origin: { type: "object", description: "Centre for scale, rotate and flip (default the centre of the stroke).", properties: { x: { type: "number", description: "Document x." }, y: { type: "number", description: "Document y." } }, required: ["x", "y"], additionalProperties: false }
|
|
8945
|
+
},
|
|
8946
|
+
additionalProperties: false
|
|
8947
|
+
}
|
|
8948
|
+
},
|
|
8949
|
+
required: ["id"],
|
|
8950
|
+
additionalProperties: false
|
|
8951
|
+
},
|
|
8952
|
+
annotations: { title: "Move or reshape a stroke", ...WRITE5 },
|
|
8953
|
+
coordinates: { in: ["points[]", "transform.origin"], out: ["bounds", "stroke.bounds", "stroke.points[]"] },
|
|
8954
|
+
handler: ({ id, points, transform }) => {
|
|
8955
|
+
const r = c.editStrokePoints(id, { points, transform });
|
|
8956
|
+
return { stroke: strokeSummary(r.stroke, true, true), bounds: r.bounds };
|
|
8957
|
+
}
|
|
8958
|
+
});
|
|
8959
|
+
add({
|
|
8960
|
+
name: "reorder_stroke",
|
|
8961
|
+
title: "Move a stroke up or down",
|
|
8962
|
+
description: "Moves a stroke to index in its stroke layer (0 = bottom, drawn first) and redraws the layer. One undo step. Returns the new order of ids, bottom first.",
|
|
8963
|
+
inputSchema: {
|
|
8964
|
+
type: "object",
|
|
8965
|
+
properties: { id: { type: "string", description: "Stroke id from list_strokes {live: true}." }, index: { type: "integer", minimum: 0, description: "New position, 0 = bottom." } },
|
|
8966
|
+
required: ["id", "index"],
|
|
8967
|
+
additionalProperties: false
|
|
8968
|
+
},
|
|
8969
|
+
annotations: { title: "Move a stroke up or down", ...WRITE5 },
|
|
8970
|
+
handler: ({ id, index }) => ({ order: c.reorderStroke(id, index) })
|
|
8971
|
+
});
|
|
8972
|
+
add({
|
|
8973
|
+
name: "remove_strokes",
|
|
8974
|
+
title: "Delete strokes",
|
|
8975
|
+
description: "Deletes strokes from their stroke layers (the eraser of a stroke layer) and redraws them. All checked first; one undo step. Returns the ids removed and the area that changed.",
|
|
8976
|
+
inputSchema: {
|
|
8977
|
+
type: "object",
|
|
8978
|
+
properties: { ids: { type: "array", minItems: 1, items: { type: "string", description: "Stroke id." }, description: "Stroke ids from list_strokes {live: true}." } },
|
|
8979
|
+
required: ["ids"],
|
|
8980
|
+
additionalProperties: false
|
|
8981
|
+
},
|
|
8982
|
+
annotations: { title: "Delete strokes", ...DESTRUCTIVE3 },
|
|
8983
|
+
coordinates: { out: ["bounds"] },
|
|
8984
|
+
handler: ({ ids }) => c.removeStrokes(ids)
|
|
8985
|
+
});
|
|
8986
|
+
add({
|
|
8987
|
+
name: "convert_to_stroke_layer",
|
|
8988
|
+
title: "Make a layer a stroke layer",
|
|
8989
|
+
description: 'Makes an empty pixel layer a stroke layer, so strokes painted on it stay editable. Refuses a layer with paint (add_layer {type: "strokes"} instead). One undo step. Returns the layer.',
|
|
8990
|
+
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers (default the active layer)." } }, additionalProperties: false },
|
|
8991
|
+
annotations: { title: "Make a layer a stroke layer", ...WRITE5 },
|
|
8992
|
+
coordinates: { out: ["bounds"] },
|
|
8993
|
+
handler: ({ layerId }) => c.convertToLiveStrokes(layerId)
|
|
8994
|
+
});
|
|
8995
|
+
add({
|
|
8996
|
+
name: "rasterize_stroke_layer",
|
|
8997
|
+
title: "Rasterize a stroke layer",
|
|
8998
|
+
description: "Turns a stroke layer into a plain pixel layer with the same pixels: its strokes stop being editable, and erasers, smudges and fills work on it. One undo step. Returns the layer.",
|
|
8999
|
+
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Stroke layer id from list_layers (default the active layer)." } }, additionalProperties: false },
|
|
9000
|
+
annotations: { title: "Rasterize a stroke layer", ...DESTRUCTIVE3 },
|
|
9001
|
+
coordinates: { out: ["bounds"] },
|
|
9002
|
+
handler: ({ layerId }) => c.rasterizeLiveStrokes(layerId)
|
|
9003
|
+
});
|
|
8307
9004
|
add({
|
|
8308
9005
|
name: "replay_strokes",
|
|
8309
9006
|
title: "Replay strokes, varied",
|
|
@@ -8418,20 +9115,22 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8418
9115
|
add({
|
|
8419
9116
|
name: "export_image",
|
|
8420
9117
|
title: "Export the painting",
|
|
8421
|
-
description: "Exports the painting as PNG, JPEG or WebP (base64). JPEG always includes the paper; JPEG and WebP need a browser canvas. Changes nothing. Returns the data, its MIME type and size.",
|
|
9118
|
+
description: "Exports the painting as PNG, JPEG or WebP (base64). JPEG always includes the paper; JPEG and WebP need a browser canvas. With time, live effect paint is drawn at that moment (get_effect_playback / set_effect_playback). Changes nothing. Returns the data, its MIME type and size.",
|
|
8422
9119
|
inputSchema: {
|
|
8423
9120
|
type: "object",
|
|
8424
9121
|
properties: {
|
|
8425
9122
|
format: { type: "string", enum: ["image/png", "image/jpeg", "image/webp"], description: "File type (default image/png)." },
|
|
8426
9123
|
quality: { type: "number", minimum: 0, maximum: 1, description: "JPEG / WebP quality 0..1 (default 0.92)." },
|
|
8427
9124
|
scale: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Shrink by this factor (default 1)." },
|
|
8428
|
-
paper: { type: "boolean", description: "Include the paper (default true)." }
|
|
9125
|
+
paper: { type: "boolean", description: "Include the paper (default true)." },
|
|
9126
|
+
time: TIME
|
|
8429
9127
|
},
|
|
8430
9128
|
additionalProperties: false
|
|
8431
9129
|
},
|
|
8432
9130
|
annotations: { title: "Export the painting", ...READ6 },
|
|
8433
|
-
handler: async ({ format, quality, scale, paper }) => {
|
|
8434
|
-
const
|
|
9131
|
+
handler: async ({ format, quality, scale, paper, time }) => {
|
|
9132
|
+
const run = () => c.exportImage({ format, quality, scale, paper });
|
|
9133
|
+
const bytes = time === void 0 ? await run() : await c.withEffectTime(time, run);
|
|
8435
9134
|
return { mimeType: format ?? "image/png", bytes: bytes.length, data: bytesToBase64(bytes) };
|
|
8436
9135
|
}
|
|
8437
9136
|
});
|
|
@@ -8439,10 +9138,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8439
9138
|
name: "export_psd",
|
|
8440
9139
|
title: "Export as PSD",
|
|
8441
9140
|
description: 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked "Paper" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',
|
|
8442
|
-
inputSchema:
|
|
9141
|
+
inputSchema: { type: "object", properties: { time: TIME }, additionalProperties: false },
|
|
8443
9142
|
annotations: { title: "Export as PSD", ...READ6 },
|
|
8444
|
-
handler: () => {
|
|
8445
|
-
const r = c.exportPsd();
|
|
9143
|
+
handler: async ({ time }) => {
|
|
9144
|
+
const r = time === void 0 ? c.exportPsd() : await c.withEffectTime(time, () => c.exportPsd());
|
|
8446
9145
|
return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
|
|
8447
9146
|
}
|
|
8448
9147
|
});
|
|
@@ -8479,12 +9178,30 @@ function hostTool({ coordinates, ...tool }, { origin, onChange }) {
|
|
|
8479
9178
|
}
|
|
8480
9179
|
};
|
|
8481
9180
|
}
|
|
9181
|
+
function strokeSummary(s, withPoints, live) {
|
|
9182
|
+
const xs = s.points.map((p) => p.x);
|
|
9183
|
+
const ys = s.points.map((p) => p.y);
|
|
9184
|
+
const pad = s.brush.tip.size / 2;
|
|
9185
|
+
return {
|
|
9186
|
+
id: s.id,
|
|
9187
|
+
layerId: s.layerId,
|
|
9188
|
+
brushId: s.brush.id,
|
|
9189
|
+
brushName: s.brush.name,
|
|
9190
|
+
color: s.color,
|
|
9191
|
+
points: withPoints ? s.points : s.points.length,
|
|
9192
|
+
seed: s.seed,
|
|
9193
|
+
copies: s.copies?.length ?? 0,
|
|
9194
|
+
live,
|
|
9195
|
+
bounds: { x: Math.floor(Math.min(...xs) - pad), y: Math.floor(Math.min(...ys) - pad), width: Math.ceil(Math.max(...xs) - Math.min(...xs) + 2 * pad), height: Math.ceil(Math.max(...ys) - Math.min(...ys) + 2 * pad) }
|
|
9196
|
+
};
|
|
9197
|
+
}
|
|
8482
9198
|
|
|
8483
9199
|
// src/paint-editor/ui.ts
|
|
8484
9200
|
var PAINT_TOOLS = [
|
|
8485
9201
|
{ id: "brush", label: "Brush", key: "b" },
|
|
8486
9202
|
{ id: "eyedropper", label: "Eyedropper", key: "i" },
|
|
8487
9203
|
{ id: "sticker", label: "Stickers", key: "s" },
|
|
9204
|
+
{ id: "stroke", label: "Select stroke", key: "v" },
|
|
8488
9205
|
{ id: "pan", label: "Pan", key: "h" },
|
|
8489
9206
|
{ id: "rotate", label: "Rotate view", key: "r" }
|
|
8490
9207
|
];
|
|
@@ -8533,6 +9250,7 @@ var PaintGesture = class {
|
|
|
8533
9250
|
this.mode = { kind: "rotate", pointerId: e.pointerId, angle: Math.atan2(e.clientY - b.top - b.height / 2, e.clientX - b.left - b.width / 2), rotation: this.controller.getState().view.rotation };
|
|
8534
9251
|
return true;
|
|
8535
9252
|
}
|
|
9253
|
+
if (tool === "stroke") return this.strokeDown(e);
|
|
8536
9254
|
if (tool === "eyedropper" || e.altKey) {
|
|
8537
9255
|
this.pick(e);
|
|
8538
9256
|
return false;
|
|
@@ -8549,6 +9267,34 @@ var PaintGesture = class {
|
|
|
8549
9267
|
return false;
|
|
8550
9268
|
}
|
|
8551
9269
|
}
|
|
9270
|
+
/**
|
|
9271
|
+
* The Select stroke tool, on a stroke layer: pressing a stroke picks it and dragging moves it
|
|
9272
|
+
* (one undo step at the drop); pressing where no stroke painted clears the pick.
|
|
9273
|
+
*/
|
|
9274
|
+
strokeDown(e) {
|
|
9275
|
+
const p = this.doc(e);
|
|
9276
|
+
const s = this.controller.getState();
|
|
9277
|
+
const layerId = s.document.activeLayerId;
|
|
9278
|
+
if (!layerId || !s.document.liveStrokes?.[layerId]) {
|
|
9279
|
+
this.error = "Pick a stroke layer in the Layers panel (or add one) to select its strokes.";
|
|
9280
|
+
return false;
|
|
9281
|
+
}
|
|
9282
|
+
try {
|
|
9283
|
+
const hit = this.controller.liveStrokeAt(layerId, p.x, p.y);
|
|
9284
|
+
this.controller.selectStroke(hit?.id ?? null);
|
|
9285
|
+
if (!hit) return false;
|
|
9286
|
+
this.mode = { kind: "move-stroke", pointerId: e.pointerId, id: hit.id, from: p, to: p };
|
|
9287
|
+
return true;
|
|
9288
|
+
} catch (err) {
|
|
9289
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
9290
|
+
return false;
|
|
9291
|
+
}
|
|
9292
|
+
}
|
|
9293
|
+
/** While a stroke is dragged: how far it has moved (document pixels), to draw its outline there. */
|
|
9294
|
+
get strokeDrag() {
|
|
9295
|
+
const m = this.mode;
|
|
9296
|
+
return m?.kind === "move-stroke" ? { id: m.id, dx: m.to.x - m.from.x, dy: m.to.y - m.from.y } : null;
|
|
9297
|
+
}
|
|
8552
9298
|
/**
|
|
8553
9299
|
* The Stickers tool: pressing a sticker picks it and dragging moves it (one undo step at the
|
|
8554
9300
|
* drop); pressing anywhere else places the active brush's first stamp there, at its tip size and
|
|
@@ -8594,6 +9340,10 @@ var PaintGesture = class {
|
|
|
8594
9340
|
this.controller.setView({ rotation: r });
|
|
8595
9341
|
return;
|
|
8596
9342
|
}
|
|
9343
|
+
if (m.kind === "move-stroke") {
|
|
9344
|
+
m.to = this.doc(e);
|
|
9345
|
+
return;
|
|
9346
|
+
}
|
|
8597
9347
|
if (m.kind === "sticker") {
|
|
8598
9348
|
const p = this.doc(e);
|
|
8599
9349
|
m.at = { x: round2(p.x + m.dx), y: round2(p.y + m.dy) };
|
|
@@ -8618,6 +9368,17 @@ var PaintGesture = class {
|
|
|
8618
9368
|
this.mode = null;
|
|
8619
9369
|
this.predicted = [];
|
|
8620
9370
|
if (m.kind === "stroke") this.controller.activeStroke?.end();
|
|
9371
|
+
if (m.kind === "move-stroke") {
|
|
9372
|
+
const dx = Math.round((this.doc(e).x - m.from.x) * 100) / 100;
|
|
9373
|
+
const dy = Math.round((this.doc(e).y - m.from.y) * 100) / 100;
|
|
9374
|
+
if (dx || dy) {
|
|
9375
|
+
try {
|
|
9376
|
+
this.controller.editStrokePoints(m.id, { transform: { dx, dy } });
|
|
9377
|
+
} catch (err) {
|
|
9378
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
}
|
|
8621
9382
|
if (m.kind === "sticker" && m.at) {
|
|
8622
9383
|
try {
|
|
8623
9384
|
this.controller.updateSticker(void 0, m.id, m.at);
|
|
@@ -8726,11 +9487,24 @@ var PaintCanvasPresenter = class {
|
|
|
8726
9487
|
return drawn;
|
|
8727
9488
|
}
|
|
8728
9489
|
};
|
|
8729
|
-
function guideOverlay(controller) {
|
|
9490
|
+
function guideOverlay(controller, drag) {
|
|
8730
9491
|
const s = controller.getState();
|
|
8731
9492
|
const size = s.document.canvas;
|
|
8732
9493
|
const out = [];
|
|
8733
9494
|
const layerId = s.document.activeLayerId;
|
|
9495
|
+
const strokePicked = layerId && s.selectedStroke ? s.document.liveStrokes?.[layerId]?.some((x) => x.id === s.selectedStroke) : false;
|
|
9496
|
+
const area = strokePicked ? controller.liveStrokeArea(s.selectedStroke) : null;
|
|
9497
|
+
if (area) {
|
|
9498
|
+
const dx = drag && drag.id === s.selectedStroke ? drag.dx : 0;
|
|
9499
|
+
const dy = drag && drag.id === s.selectedStroke ? drag.dy : 0;
|
|
9500
|
+
const box = [
|
|
9501
|
+
{ x: area.x + dx, y: area.y + dy },
|
|
9502
|
+
{ x: area.x + area.width + dx, y: area.y + dy },
|
|
9503
|
+
{ x: area.x + area.width + dx, y: area.y + area.height + dy },
|
|
9504
|
+
{ x: area.x + dx, y: area.y + area.height + dy }
|
|
9505
|
+
];
|
|
9506
|
+
for (let i = 0; i < 4; i++) out.push({ from: box[i], to: box[(i + 1) % 4], kind: "stroke" });
|
|
9507
|
+
}
|
|
8734
9508
|
const picked = layerId && s.selectedSticker ? s.document.stickers?.[layerId]?.find((x) => x.id === s.selectedSticker) : void 0;
|
|
8735
9509
|
if (picked) {
|
|
8736
9510
|
const r = picked.size / 2 + 2;
|
|
@@ -9060,6 +9834,11 @@ function toggleStamp(stamps, stamp) {
|
|
|
9060
9834
|
const next = stamps.includes(stamp) ? stamps.filter((s) => s !== stamp) : [...stamps, stamp];
|
|
9061
9835
|
return next.length ? next : [stamp];
|
|
9062
9836
|
}
|
|
9837
|
+
var STROKE_SLIDERS = [
|
|
9838
|
+
{ path: "tip.size", label: "Size", min: 1, max: 400, step: 1 },
|
|
9839
|
+
{ path: "tip.opacity", label: "Opacity", min: 0, max: 1, step: 0.01 },
|
|
9840
|
+
{ path: "tip.flow", label: "Flow", min: 0, max: 1, step: 0.01 }
|
|
9841
|
+
];
|
|
9063
9842
|
var STICKER_SLIDERS = [
|
|
9064
9843
|
{ path: "size", label: "Size", min: 2, max: 400, step: 1 },
|
|
9065
9844
|
{ path: "rotation", label: "Rotation", min: -180, max: 180, step: 1 },
|
|
@@ -9094,7 +9873,7 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
9094
9873
|
};
|
|
9095
9874
|
const paintInfo = () => {
|
|
9096
9875
|
const p = controller.getEffectPaint();
|
|
9097
|
-
return p ? [{ id: PAINT_TARGET, effect: p.effect, provider: p.provider, role: controller.getState().effectPaintRendering ? "effect paint (rendering)" : "effect paint", enabled: true, values: p.values, ...p.time ? { time: p.time } : {} }] : [];
|
|
9876
|
+
return p ? [{ id: PAINT_TARGET, effect: p.effect, provider: p.provider, role: controller.getState().effectPaintRendering ? "effect paint (rendering)" : "effect paint", enabled: true, values: p.values, ...p.time ? { time: p.time } : {}, ...p.map ? { map: p.map } : {}, ...p.live ? { live: true } : {} }] : [];
|
|
9098
9877
|
};
|
|
9099
9878
|
const layerInfo = (layerId) => {
|
|
9100
9879
|
const s = controller.getEffectLayer(layerId);
|
|
@@ -9163,7 +9942,7 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
9163
9942
|
if (target === PAINT_TARGET) {
|
|
9164
9943
|
if (changes.opacity !== void 0) throw new Error("opacity does not apply to the effect paint: change the brush opacity (set_brush tip.opacity).");
|
|
9165
9944
|
const p = controller.getEffectPaint();
|
|
9166
|
-
await controller.setEffectPaint({ effect: p.effect, values: { ...p.values, ...changes.values ?? {} }, time: p.time });
|
|
9945
|
+
await controller.setEffectPaint({ effect: p.effect, values: { ...p.values, ...changes.values ?? {} }, time: p.time, map: p.map, live: p.live });
|
|
9167
9946
|
const effects2 = paintInfo();
|
|
9168
9947
|
return { target, effect: effects2[0], effects: effects2 };
|
|
9169
9948
|
}
|
|
@@ -9222,13 +10001,25 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
9222
10001
|
{
|
|
9223
10002
|
name: "set_effect_paint",
|
|
9224
10003
|
title: "Paint with an effect",
|
|
9225
|
-
description: `Makes brushes paint with a provider generator, overlay or title effect (search_effects with use generator, overlay or title): it is rendered once at the canvas size, and each stroke then takes every pixel's colour from it, fixed to the canvas, as if painting a hidden picture into view (the effect's transparency paints nothing). Dab, particle and bristle brushes take the colour per pixel; mixer brushes and dab brushes that pick up paint take it per dab, at its centre.
|
|
10004
|
+
description: `Makes brushes paint with a provider generator, overlay or title effect (search_effects with use generator, overlay or title): it is rendered once at the canvas size, and each stroke then takes every pixel's colour from it, fixed to the canvas, as if painting a hidden picture into view (the effect's transparency paints nothing). Dab, particle and bristle brushes take the colour per pixel; mixer brushes and dab brushes that pick up paint take it per dab, at its centre. Watercolour, gouache, oil and impasto brushes take it per pixel too, shifted by paint they pick up (in wet paint the pattern then flows with the water; impasto keeps its relief), and sprayed stickers take the colour at their centre. Erasers, blenders and smudges ignore it. It replaces the paint colour or gradient until set_color or set_paint; a brush with its own gradient paints that instead. Strokes record it and replay exactly. Editor state, not an undo step. Returns the effect paint: { effect, provider, values (every control), time } (list_applied_effects target "paint" shows it; update_effect / remove_effect with target and effect "paint" change or clear it).`,
|
|
9226
10005
|
inputSchema: {
|
|
9227
10006
|
type: "object",
|
|
9228
10007
|
properties: {
|
|
9229
10008
|
effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
|
|
9230
10009
|
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
9231
|
-
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to paint with, in ms (default 0)." }
|
|
10010
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to paint with, in ms (default 0)." },
|
|
10011
|
+
map: {
|
|
10012
|
+
type: "object",
|
|
10013
|
+
description: "How the effect lies on the paint: by canvas (default: fixed to the canvas, painting reveals a hidden picture) or by stroke (along each stroke: the effect is rendered as a strip length px long, and each pixel takes the colour at its distance along the stroke and its place across the tip).",
|
|
10014
|
+
properties: {
|
|
10015
|
+
by: { type: "string", enum: ["canvas", "stroke"], description: "canvas (default) or stroke." },
|
|
10016
|
+
length: { type: "number", minimum: 16, maximum: 4096, description: "by stroke: px of stroke per repeat of the effect (default 300)." },
|
|
10017
|
+
repeat: { type: "string", enum: ["repeat", "mirror", "clamp"], description: "by stroke: past the strip end start again (default), run back, or hold the last colour." }
|
|
10018
|
+
},
|
|
10019
|
+
required: ["by"],
|
|
10020
|
+
additionalProperties: false
|
|
10021
|
+
},
|
|
10022
|
+
live: { type: "boolean", description: "Live effect paint (default false): strokes on a stroke layer (add_layer type strokes) keep playing the effect, so the paint moves (set_effect_playback); elsewhere strokes bake the frame at time." }
|
|
9232
10023
|
},
|
|
9233
10024
|
required: ["effect"],
|
|
9234
10025
|
additionalProperties: false
|
|
@@ -9236,9 +10027,24 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
9236
10027
|
annotations: { title: "Paint with an effect", ...ctx.annotations.writeIdempotent },
|
|
9237
10028
|
handler: async (args) => {
|
|
9238
10029
|
ctx.requireProvider();
|
|
9239
|
-
return { effectPaint: await controller.setEffectPaint({ effect: args.effect, values: values(args.values), time: args.time }) };
|
|
10030
|
+
return { effectPaint: await controller.setEffectPaint({ effect: args.effect, values: values(args.values), time: args.time, map: args.map, live: args.live }) };
|
|
9240
10031
|
}
|
|
9241
10032
|
},
|
|
10033
|
+
{
|
|
10034
|
+
name: "set_effect_playback",
|
|
10035
|
+
title: "Play or pause live effect paint",
|
|
10036
|
+
description: "Plays or pauses live effect paint (strokes painted on stroke layers with set_effect_paint live true) in the editor view, and caps its frame rate. Exports draw it at a chosen time with export_image / export_psd time. Editor state, not an undo step. Returns playing, fps, the time the view shows (ms) and what plays (animation: layer ids, live strokes, sources; null when nothing does).",
|
|
10037
|
+
inputSchema: {
|
|
10038
|
+
type: "object",
|
|
10039
|
+
properties: {
|
|
10040
|
+
playing: { type: "boolean", description: "Play (true, the default state) or pause at the current frame." },
|
|
10041
|
+
fps: { type: "integer", minimum: 1, maximum: 60, description: "Frame rate cap, frames a second (default 30); lower it on large canvases." }
|
|
10042
|
+
},
|
|
10043
|
+
additionalProperties: false
|
|
10044
|
+
},
|
|
10045
|
+
annotations: { title: "Play or pause live effect paint", ...ctx.annotations.writeIdempotent },
|
|
10046
|
+
handler: async (args) => ({ ...controller.setEffectPlayback({ playing: args.playing, fps: args.fps }), animation: controller.getAnimation() })
|
|
10047
|
+
},
|
|
9242
10048
|
{
|
|
9243
10049
|
name: "add_effect_layer",
|
|
9244
10050
|
title: "Add an effect layer",
|
|
@@ -9348,6 +10154,8 @@ export {
|
|
|
9348
10154
|
DEFAULT_WET_BRUSH,
|
|
9349
10155
|
DEFAULT_WET_PARAMS,
|
|
9350
10156
|
DryStroke,
|
|
10157
|
+
EFFECT_MAP_REPEATS,
|
|
10158
|
+
EFFECT_STRIP_HEIGHT,
|
|
9351
10159
|
GPU_LEAD,
|
|
9352
10160
|
GRAIN_PATTERNS,
|
|
9353
10161
|
GRAIN_TILE,
|
|
@@ -9357,11 +10165,13 @@ export {
|
|
|
9357
10165
|
IMPASTO_TOOLS,
|
|
9358
10166
|
IMPASTO_TOOL_LABELS,
|
|
9359
10167
|
LIGHT_SLIDERS,
|
|
10168
|
+
LIVE_STROKE_ENGINES,
|
|
9360
10169
|
LitCache,
|
|
9361
10170
|
MAX_BRUSH_SIZE,
|
|
9362
10171
|
MAX_CANVAS_SIDE,
|
|
9363
10172
|
MAX_COORDINATE,
|
|
9364
10173
|
MAX_IMAGE_SIDE,
|
|
10174
|
+
MAX_LIVE_EFFECT_SOURCES,
|
|
9365
10175
|
MAX_ZOOM,
|
|
9366
10176
|
MIN_CANVAS_SIDE,
|
|
9367
10177
|
MIN_ZOOM,
|
|
@@ -9425,6 +10235,7 @@ export {
|
|
|
9425
10235
|
STENCIL_KINDS,
|
|
9426
10236
|
STENCIL_SHAPES,
|
|
9427
10237
|
STICKER_SLIDERS,
|
|
10238
|
+
STROKE_SLIDERS,
|
|
9428
10239
|
SYMMETRY_MODES,
|
|
9429
10240
|
StencilSnapper,
|
|
9430
10241
|
StickerSpray,
|
|
@@ -9480,8 +10291,10 @@ export {
|
|
|
9480
10291
|
downsampleRgba,
|
|
9481
10292
|
drawSticker,
|
|
9482
10293
|
drawWaterOverlay,
|
|
10294
|
+
effectDabField,
|
|
9483
10295
|
effectLayersToJSON,
|
|
9484
10296
|
effectPaintKey,
|
|
10297
|
+
effectPaintSize,
|
|
9485
10298
|
emptiedLayers,
|
|
9486
10299
|
emptyThickChannels,
|
|
9487
10300
|
fitZoom,
|
|
@@ -9505,6 +10318,8 @@ export {
|
|
|
9505
10318
|
isWetCapable,
|
|
9506
10319
|
lightTile,
|
|
9507
10320
|
lightingKey,
|
|
10321
|
+
liveStrokeBounds,
|
|
10322
|
+
liveStrokeRefusal,
|
|
9508
10323
|
mapPenPoint,
|
|
9509
10324
|
mix32,
|
|
9510
10325
|
mixPixels,
|
|
@@ -9565,6 +10380,7 @@ export {
|
|
|
9565
10380
|
quickStencil,
|
|
9566
10381
|
rand01,
|
|
9567
10382
|
randSigned,
|
|
10383
|
+
readEffectStrokeMap,
|
|
9568
10384
|
readPaintEffectPaint,
|
|
9569
10385
|
readPaintEffectSource,
|
|
9570
10386
|
readPaintSource,
|