@pixerate/editor 0.269.2 → 0.271.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/README.md +5 -2
- package/dist/agent/index.cjs +30 -30
- package/dist/agent/index.js +3 -3
- package/dist/animation-editor/index.cjs +62 -62
- package/dist/animation-editor/index.js +4 -4
- package/dist/canvas/editorDocument.d.cts +3 -1
- package/dist/canvas/editorDocument.d.ts +3 -1
- package/dist/canvas/index.cjs +53 -27
- package/dist/canvas/index.js +34 -8
- package/dist/chart/index.cjs +21 -21
- package/dist/chart/index.js +2 -2
- package/dist/{chunk-GBEF2D4P.js → chunk-23YL2EAB.js} +1 -1
- package/dist/{chunk-QGVTOCNB.js → chunk-6ZSFRRJI.js} +1 -1
- package/dist/{chunk-4YSPQGZW.js → chunk-B5W677L7.js} +1 -1
- package/dist/{chunk-4NMPUJUK.cjs → chunk-DDL3ZBHK.cjs} +86 -86
- package/dist/{chunk-T4JPGBW3.cjs → chunk-DGITFN73.cjs} +8 -8
- package/dist/{chunk-PY4GWSE2.cjs → chunk-EBFPMSJO.cjs} +27 -5
- package/dist/{chunk-NS62CQ2B.js → chunk-GAZPHCTV.js} +23 -2
- package/dist/{chunk-YYBW6DF2.cjs → chunk-GSLOH2IN.cjs} +2 -2
- package/dist/{chunk-56RHCIJV.cjs → chunk-JJ2H25JG.cjs} +237 -170
- package/dist/{chunk-CBVNCWJM.cjs → chunk-LBDC5434.cjs} +2 -2
- package/dist/{chunk-BENSE3VX.cjs → chunk-M2ZVPU7L.cjs} +23 -23
- package/dist/{chunk-MQ6MGIQM.js → chunk-NMZQMPFZ.js} +1 -1
- package/dist/{chunk-AI4F33IV.cjs → chunk-NQUKTRKN.cjs} +45 -45
- package/dist/{chunk-4CPOKP27.js → chunk-QYXUE37Q.js} +1 -1
- package/dist/{chunk-MWUZCEFT.js → chunk-S2XPXA47.js} +1 -1
- package/dist/{chunk-EHLOCVWB.js → chunk-T2URHAJX.js} +27 -5
- package/dist/{chunk-FUDP5QBB.js → chunk-TNHDCP4R.js} +70 -3
- package/dist/{chunk-VBRHE4AM.cjs → chunk-U2ASNGO6.cjs} +35 -35
- package/dist/{chunk-WKPXOGZZ.cjs → chunk-WPLGZBNE.cjs} +23 -2
- package/dist/{chunk-N3UMQVKX.js → chunk-X4FTNFIA.js} +1 -1
- package/dist/{chunk-2OQGPEHH.cjs → chunk-XUDMRMXQ.cjs} +2 -2
- package/dist/{chunk-THBJHPUL.js → chunk-YNFSH72F.js} +1 -1
- package/dist/diagram/index.cjs +2 -2
- package/dist/diagram/index.js +1 -1
- package/dist/effects/index.cjs +8 -5
- package/dist/effects/index.js +6 -3
- package/dist/effects/pool.d.cts +9 -0
- package/dist/effects/pool.d.ts +9 -0
- package/dist/effects/types.d.cts +2 -0
- package/dist/effects/types.d.ts +2 -0
- package/dist/extensions/index.cjs +5 -5
- package/dist/extensions/index.js +4 -4
- package/dist/gradient/index.cjs +2 -2
- package/dist/gradient/index.js +1 -1
- package/dist/history/HistoryManager.d.cts +9 -0
- package/dist/history/HistoryManager.d.ts +9 -0
- package/dist/history/index.cjs +2 -2
- package/dist/history/index.js +1 -1
- package/dist/history/types.d.cts +5 -2
- package/dist/history/types.d.ts +5 -2
- package/dist/image-editor/index.cjs +784 -784
- package/dist/image-editor/index.js +4 -4
- package/dist/index.cjs +10 -10
- package/dist/index.js +4 -4
- package/dist/paint-editor/controller.d.cts +75 -4
- package/dist/paint-editor/controller.d.ts +75 -4
- package/dist/paint-editor/effect-paint.d.cts +33 -8
- package/dist/paint-editor/effect-paint.d.ts +33 -8
- package/dist/paint-editor/effects.d.cts +4 -0
- package/dist/paint-editor/effects.d.ts +4 -0
- package/dist/paint-editor/index.cjs +692 -398
- package/dist/paint-editor/index.js +362 -68
- package/dist/paint-editor/stroke.d.cts +13 -5
- package/dist/paint-editor/stroke.d.ts +13 -5
- package/dist/parameters/index.cjs +2 -2
- package/dist/parameters/index.js +1 -1
- package/dist/vector/index.cjs +998 -998
- package/dist/vector/index.js +4 -4
- package/dist/video-editor/index.cjs +321 -321
- package/dist/video-editor/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +2 -2
- package/skills/pixerate-paint-editor/references/effect-paint.md +28 -6
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "../chunk-LETF75CA.js";
|
|
23
23
|
import {
|
|
24
24
|
toVideoFfmpeg
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-S2XPXA47.js";
|
|
26
26
|
import {
|
|
27
27
|
ToolRegistry
|
|
28
28
|
} from "../chunk-K4BGRCSH.js";
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
resolveExternalAssets,
|
|
35
35
|
serializeLayers,
|
|
36
36
|
serializeLayersExternal
|
|
37
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-B5W677L7.js";
|
|
38
38
|
import "../chunk-ADMRNWR7.js";
|
|
39
39
|
import "../chunk-WJ4WQJCC.js";
|
|
40
40
|
import {
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
EFFECT_STRIP_HEIGHT,
|
|
80
80
|
GPU_LEAD,
|
|
81
81
|
IDENTITY_COPY,
|
|
82
|
+
MAX_EFFECT_PATH_POINTS,
|
|
82
83
|
PAPER_FAMILIES,
|
|
83
84
|
PAPER_LIBRARY,
|
|
84
85
|
PaintDabStroke,
|
|
@@ -95,6 +96,7 @@ import {
|
|
|
95
96
|
effectPaintKey,
|
|
96
97
|
effectPaintSize,
|
|
97
98
|
hexToRgb8,
|
|
99
|
+
inferEffectPathControl,
|
|
98
100
|
isPaintEffectPaint,
|
|
99
101
|
mixerTip,
|
|
100
102
|
multiColorForDab,
|
|
@@ -109,11 +111,13 @@ import {
|
|
|
109
111
|
sampleEffectAt,
|
|
110
112
|
sampleEffectField,
|
|
111
113
|
stampSampler,
|
|
114
|
+
strokePathPoints,
|
|
112
115
|
tiltToOrientation,
|
|
113
116
|
toStrokePoint,
|
|
114
117
|
wetPaperInput,
|
|
115
|
-
wetPigmentOf
|
|
116
|
-
|
|
118
|
+
wetPigmentOf,
|
|
119
|
+
withStrokePath
|
|
120
|
+
} from "../chunk-TNHDCP4R.js";
|
|
117
121
|
import {
|
|
118
122
|
BRUSH_ID,
|
|
119
123
|
BRUSH_PRESETS,
|
|
@@ -201,11 +205,12 @@ import {
|
|
|
201
205
|
} from "../chunk-SZTI6EER.js";
|
|
202
206
|
import {
|
|
203
207
|
EffectRendererPool
|
|
204
|
-
} from "../chunk-
|
|
208
|
+
} from "../chunk-T2URHAJX.js";
|
|
205
209
|
import {
|
|
206
210
|
EffectUnsupportedError,
|
|
207
211
|
EffectsSlot,
|
|
208
212
|
createEffectToolProvider,
|
|
213
|
+
pathControlsOf,
|
|
209
214
|
resolveEffectValues
|
|
210
215
|
} from "../chunk-MV7CYFMJ.js";
|
|
211
216
|
import "../chunk-V2ZCJRDM.js";
|
|
@@ -2103,7 +2108,6 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2103
2108
|
const brush = init.brush;
|
|
2104
2109
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
2105
2110
|
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
2106
|
-
const effect = init.effect?.pixels ?? null;
|
|
2107
2111
|
this.apply = {
|
|
2108
2112
|
mode: brush.engine === "eraser" ? "erase" : "paint",
|
|
2109
2113
|
color: init.color,
|
|
@@ -2114,31 +2118,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2114
2118
|
selection: init.selection ?? null,
|
|
2115
2119
|
offset: init.offset
|
|
2116
2120
|
};
|
|
2117
|
-
if (this.wetDriver)
|
|
2118
|
-
} else if (init.sticker) {
|
|
2119
|
-
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, art: init.sticker.art, variant: init.sticker.variant });
|
|
2120
|
-
} else if (init.thick) {
|
|
2121
|
-
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2122
|
-
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 });
|
|
2123
|
-
} else if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush, Boolean(init.paint))) {
|
|
2124
|
-
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2125
|
-
this.wet = new PaintDabStroke(brush, {
|
|
2126
|
-
target: this.tileTarget(),
|
|
2127
|
-
color: rgb(init.color),
|
|
2128
|
-
secondary: rgb(init.secondary ?? init.background),
|
|
2129
|
-
load: init.mixerLoad,
|
|
2130
|
-
grain: init.grain,
|
|
2131
|
-
paperDepth: init.paperDepth,
|
|
2132
|
-
seed: init.seed,
|
|
2133
|
-
copies: this.copies,
|
|
2134
|
-
paint: init.paint ?? null,
|
|
2135
|
-
effect,
|
|
2136
|
-
stampArt: init.stampArt
|
|
2137
|
-
});
|
|
2138
|
-
} else {
|
|
2139
|
-
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect, stampArt: init.stampArt };
|
|
2140
|
-
this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
|
|
2141
|
-
}
|
|
2121
|
+
if (!this.wetDriver) this.build(init.effect?.pixels ?? null);
|
|
2142
2122
|
}
|
|
2143
2123
|
host;
|
|
2144
2124
|
init;
|
|
@@ -2167,6 +2147,52 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2167
2147
|
result = null;
|
|
2168
2148
|
/** The mask stencils this stroke was painted with when they are not the editor's (replays). */
|
|
2169
2149
|
maskStencils = null;
|
|
2150
|
+
/** Sets up the marks' engine (every kind but wet, which the wet layer paints), sampling `effect` for effect paint. */
|
|
2151
|
+
build(effect) {
|
|
2152
|
+
const init = this.init;
|
|
2153
|
+
const brush = init.brush;
|
|
2154
|
+
this.dry = this.wet = this.thickDriver = this.stickerSpray = null;
|
|
2155
|
+
if (init.sticker) {
|
|
2156
|
+
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, art: init.sticker.art, variant: init.sticker.variant });
|
|
2157
|
+
} else if (init.thick) {
|
|
2158
|
+
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2159
|
+
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 });
|
|
2160
|
+
} else if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush, Boolean(init.paint))) {
|
|
2161
|
+
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
2162
|
+
this.wet = new PaintDabStroke(brush, {
|
|
2163
|
+
target: this.tileTarget(),
|
|
2164
|
+
color: rgb(init.color),
|
|
2165
|
+
secondary: rgb(init.secondary ?? init.background),
|
|
2166
|
+
load: init.mixerLoad,
|
|
2167
|
+
grain: init.grain,
|
|
2168
|
+
paperDepth: init.paperDepth,
|
|
2169
|
+
seed: init.seed,
|
|
2170
|
+
copies: this.copies,
|
|
2171
|
+
paint: init.paint ?? null,
|
|
2172
|
+
effect,
|
|
2173
|
+
stampArt: init.stampArt
|
|
2174
|
+
});
|
|
2175
|
+
} else {
|
|
2176
|
+
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect, stampArt: init.stampArt };
|
|
2177
|
+
this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Paints the stroke so far again, sampling new effect pixels: a path-mapped effect rendered
|
|
2182
|
+
* along the path drawn so far (`map: { by: 'path' }`). Only the live preview changes; the stroke
|
|
2183
|
+
* keeps recording its own paint. Nothing for wet strokes or once the stroke has ended.
|
|
2184
|
+
*/
|
|
2185
|
+
repaintEffect(pixels) {
|
|
2186
|
+
if (this.closed || this.wetDriver || !this.init.effect) return;
|
|
2187
|
+
const was = this.bounds;
|
|
2188
|
+
this.live = new Map(this.base.tiles);
|
|
2189
|
+
this.touched.clear();
|
|
2190
|
+
this.fresh.clear();
|
|
2191
|
+
this.bounds = null;
|
|
2192
|
+
this.build(pixels);
|
|
2193
|
+
const rects = this.points.length ? this.dry ? this.dry.push(this.points) : this.thickDriver ? this.thickDriver.push(this.points) : this.stickerSpray ? this.stickerSpray.push(this.points) : this.wet.push(this.points) : [];
|
|
2194
|
+
this.update(was ? [...rects, was] : rects);
|
|
2195
|
+
}
|
|
2170
2196
|
/** The stroke's twin over the light channel: the same marks in grey at the brush's light amount (see `light.ts`). */
|
|
2171
2197
|
makeLightTwin(light) {
|
|
2172
2198
|
const init = this.init;
|
|
@@ -2703,7 +2729,8 @@ function varyStroke(stroke, m, v) {
|
|
|
2703
2729
|
const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;
|
|
2704
2730
|
const out = { ...stroke, brush, points };
|
|
2705
2731
|
if (stroke.paint?.type === "gradient" && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);
|
|
2706
|
-
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))) } };
|
|
2732
|
+
if (stroke.paint?.type === "effect" && stroke.paint.map?.by === "stroke" && s !== 1) out.paint = { ...stroke.paint, map: { ...stroke.paint.map, length: Math.min(4096, Math.max(16, Math.round(stroke.paint.map.length * s))) } };
|
|
2733
|
+
if (stroke.paint?.type === "effect" && stroke.paint.map?.by === "path") out.paint = withStrokePath(stroke.paint, points);
|
|
2707
2734
|
if (stroke.copies?.length) {
|
|
2708
2735
|
const inv = invertAffine(m);
|
|
2709
2736
|
out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));
|
|
@@ -4067,7 +4094,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
4067
4094
|
const chunk = Math.max(1, options.wetChunk ?? 0) || Infinity;
|
|
4068
4095
|
let strokes = 0;
|
|
4069
4096
|
const scaled = source.strokes.slice(0, maxStrokes).map((st) => s === 1 ? st : varyStroke(st, scaleM, { scale: s }));
|
|
4070
|
-
await target.prepareEffectPaints(scaled.flatMap((st) => st.paint?.type === "effect" ? [st.paint] : []));
|
|
4097
|
+
await target.prepareEffectPaints(scaled.flatMap((st) => st.paint?.type === "effect" && st.paint.map?.by !== "path" ? [st.paint] : []));
|
|
4071
4098
|
const advanceTo = async (clock) => {
|
|
4072
4099
|
const now = target.getState().wet.clock;
|
|
4073
4100
|
let left = clock - now;
|
|
@@ -4139,6 +4166,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
4139
4166
|
const st = s === 1 ? item.stroke : varyStroke(item.stroke, scaleM, { scale: s });
|
|
4140
4167
|
strokes++;
|
|
4141
4168
|
if (!target.getLayer(st.layerId)) continue;
|
|
4169
|
+
if (st.paint?.type === "effect" && st.paint.map?.by === "path") await target.prepareEffectPaints([st.paint]);
|
|
4142
4170
|
const init = { layerId: st.layerId, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...st.paint ? { paint: st.paint } : {}, snapToPerspective: false, stencils: false, maskStencils: (st.stencils ?? []).map((x) => s === 1 ? x : scaleStencil(x, s)) };
|
|
4143
4171
|
if (st.brush.engine === "wet" && st.steps?.length === st.points.length) {
|
|
4144
4172
|
await target.wetIdle();
|
|
@@ -4321,6 +4349,12 @@ var PaintEffectsRenderer = class {
|
|
|
4321
4349
|
}
|
|
4322
4350
|
return out;
|
|
4323
4351
|
}
|
|
4352
|
+
renderSync(provider, id, size, request) {
|
|
4353
|
+
this.follow(provider);
|
|
4354
|
+
const out = this.pool.renderSync(provider, id, { ...size, depth: 8 }, request);
|
|
4355
|
+
if (!out || out.width !== size.width || out.height !== size.height || out.data.length !== size.width * size.height * 4) return null;
|
|
4356
|
+
return out;
|
|
4357
|
+
}
|
|
4324
4358
|
dispose() {
|
|
4325
4359
|
this.pool.clear();
|
|
4326
4360
|
this.descriptions.clear();
|
|
@@ -4439,6 +4473,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4439
4473
|
paintEpoch = 0;
|
|
4440
4474
|
/** Rendered effect paints by `effectPaintKey` (most recently used last). */
|
|
4441
4475
|
effectPaintPixels = /* @__PURE__ */ new Map();
|
|
4476
|
+
/** Primary path control name (`format: 'svg-path'`) per effect id, learned from `describeEffect`. */
|
|
4477
|
+
effectPathControls = /* @__PURE__ */ new Map();
|
|
4478
|
+
/** How many renders the running batch needs together (`prepareEffectPaints`; a synchronous replay reads them all). */
|
|
4479
|
+
effectPaintBatch = 0;
|
|
4480
|
+
/** The hand-painted stroke waiting for its path render (`finishPathStroke`), or `null`. */
|
|
4481
|
+
pathStroke = null;
|
|
4482
|
+
/**
|
|
4483
|
+
* The hand-painted path stroke's live preview: the effect rendered along the path drawn so far
|
|
4484
|
+
* (one render at a time; the newest path when it lands), repainted into the stroke as it lands.
|
|
4485
|
+
*/
|
|
4486
|
+
pathPreview = null;
|
|
4442
4487
|
mixerLoad = EMPTY_MIXER_LOAD;
|
|
4443
4488
|
view = { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
|
|
4444
4489
|
hand;
|
|
@@ -4775,7 +4820,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4775
4820
|
secondaryColor: this.secondaryColor,
|
|
4776
4821
|
paint: this.gradient ?? { type: "color", color: this.color },
|
|
4777
4822
|
effectPaint: this.effectPaintPending ?? this.effectPaint,
|
|
4778
|
-
effectPaintRendering: Boolean(this.effectPaintPending),
|
|
4823
|
+
effectPaintRendering: Boolean(this.effectPaintPending || this.pathStroke),
|
|
4779
4824
|
mixerLoad: this.mixerLoad,
|
|
4780
4825
|
view: this.view,
|
|
4781
4826
|
hand: this.hand,
|
|
@@ -4837,6 +4882,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4837
4882
|
}
|
|
4838
4883
|
requireIdle(what) {
|
|
4839
4884
|
if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);
|
|
4885
|
+
if (this.pathStroke) throw new PaintEditorError(`The last stroke is still rendering the effect "${this.pathStroke.effect}" along its path; wait for it (strokeRendering) before ${what}.`);
|
|
4840
4886
|
if (this.grouping?.pending && !this.grouping.inside) throw new PaintEditorError(`"${this.grouping.label}" is still being painted (one undo step); wait for it to finish before ${what}.`);
|
|
4841
4887
|
}
|
|
4842
4888
|
commit(label, next, before = this.doc) {
|
|
@@ -5481,6 +5527,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5481
5527
|
/** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
|
|
5482
5528
|
async describeEffect(effect, place) {
|
|
5483
5529
|
const d = await this.effectsRenderer.describe(this.effects.require(), effect);
|
|
5530
|
+
const pcs = pathControlsOf(d);
|
|
5531
|
+
if (pcs.length) this.effectPathControls.set(d.id, pcs[0]);
|
|
5484
5532
|
if (place) checkPaintEffectPlace(d, place);
|
|
5485
5533
|
return d;
|
|
5486
5534
|
}
|
|
@@ -5681,7 +5729,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5681
5729
|
return { layerId: now.id, source: this.sourceOf(d, values, options.time) };
|
|
5682
5730
|
}
|
|
5683
5731
|
// --- Effect paint ----------------------------------------------------------------
|
|
5684
|
-
/**
|
|
5732
|
+
/**
|
|
5733
|
+
* At most this many rendered effect paints are kept (each is canvas-sized), more only while
|
|
5734
|
+
* something needs them all: a replay's batch, and the path-mapped strokes on stroke layers,
|
|
5735
|
+
* which each have a render of their own and are redrawn synchronously.
|
|
5736
|
+
*/
|
|
5685
5737
|
static EFFECT_PAINT_CACHE = 6;
|
|
5686
5738
|
/** The effect paint strokes use (or the one rendering), or `null`. */
|
|
5687
5739
|
getEffectPaint() {
|
|
@@ -5693,8 +5745,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5693
5745
|
* size, and brushes then take each pixel's colour (and alpha) from it, fixed to the canvas, as if
|
|
5694
5746
|
* painting a hidden picture into view. `setColor` and `setPaint` go back to colour. Editor state,
|
|
5695
5747
|
* not an undo step. Strokes that would paint with it throw while it renders. Returns the paint.
|
|
5696
|
-
* With `map: { by: 'stroke' }` the effect lies along each stroke
|
|
5697
|
-
* `
|
|
5748
|
+
* With `map: { by: 'stroke' }` the effect lies along each stroke as a strip; with
|
|
5749
|
+
* `map: { by: 'path' }` an effect with a path control (`format: 'svg-path'`) draws itself along
|
|
5750
|
+
* each stroke's centreline in canvas pixels; with `live`, strokes on a stroke layer keep playing
|
|
5751
|
+
* it (see `setEffectPlayback`).
|
|
5698
5752
|
*/
|
|
5699
5753
|
async setEffectPaint(source) {
|
|
5700
5754
|
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).");
|
|
@@ -5709,6 +5763,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5709
5763
|
const epoch = ++this.paintEpoch;
|
|
5710
5764
|
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.`);
|
|
5711
5765
|
const d = await this.describeEffect(source.effect, "paint");
|
|
5766
|
+
if (map?.by === "path" && !pathControlsOf(d).length) {
|
|
5767
|
+
throw new PaintEditorError(`Effect "${d.id}" has no path control (format "svg-path"); use by: "stroke" (bent along each stroke) or by: "canvas" (fixed to the canvas).`);
|
|
5768
|
+
}
|
|
5712
5769
|
const values = this.effectValues(d, { ...d.defaults }, source.values);
|
|
5713
5770
|
if (epoch !== this.paintEpoch) throw changed(d.name);
|
|
5714
5771
|
const paint = { type: "effect", effect: d.id, provider: this.effects.require().id, values, time: source.time ?? 0, ...map ? { map } : {}, ...source.live ? { live: true } : {} };
|
|
@@ -5740,10 +5797,38 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5740
5797
|
this.effectPaintPixels.set(key, img);
|
|
5741
5798
|
return img;
|
|
5742
5799
|
}
|
|
5800
|
+
cacheEffectPaintPixels(p, pixels) {
|
|
5801
|
+
return this.cacheEffectPaintImage(p, toEffectPaintPixels(p, pixels));
|
|
5802
|
+
}
|
|
5803
|
+
cacheEffectPaintImage(p, img) {
|
|
5804
|
+
this.effectPaintPixels.set(effectPaintKey(p), img);
|
|
5805
|
+
this.trimEffectPaints();
|
|
5806
|
+
return img;
|
|
5807
|
+
}
|
|
5808
|
+
trimEffectPaints() {
|
|
5809
|
+
let live = 0;
|
|
5810
|
+
for (const list of Object.values(this.doc.liveStrokes ?? {})) for (const s of list) if (s.paint?.type === "effect" && s.paint.map?.by === "path") live++;
|
|
5811
|
+
const limit = Math.max(_PaintEditorController.EFFECT_PAINT_CACHE, this.effectPaintBatch, live + 1);
|
|
5812
|
+
while (this.effectPaintPixels.size > limit) this.effectPaintPixels.delete(this.effectPaintPixels.keys().next().value);
|
|
5813
|
+
}
|
|
5814
|
+
/** Renders an effect paint on the caller's stack when its renderer is warm and has `renderSync`; otherwise `null`. */
|
|
5815
|
+
renderEffectPaintSync(p) {
|
|
5816
|
+
const have = this.effectPaintImage(p);
|
|
5817
|
+
if (have) return have;
|
|
5818
|
+
const provider = this.effects.provider;
|
|
5819
|
+
if (!provider || p.provider && provider.id !== p.provider) return null;
|
|
5820
|
+
const size = effectPaintSize(p, this.doc.canvas);
|
|
5821
|
+
const pixels = this.effectsRenderer.renderSync(provider, p.effect, size, { time: p.time, values: p.values });
|
|
5822
|
+
return pixels ? this.cacheEffectPaintPixels(p, pixels) : null;
|
|
5823
|
+
}
|
|
5743
5824
|
/** Renders an effect paint at the canvas size into the cache (nothing when it is there already). */
|
|
5744
5825
|
async renderEffectPaint(p) {
|
|
5745
5826
|
const have = this.effectPaintImage(p);
|
|
5746
5827
|
if (have) return have;
|
|
5828
|
+
return this.cacheEffectPaintImage(p, await this.renderEffectPaintPixels(p));
|
|
5829
|
+
}
|
|
5830
|
+
/** Renders an effect paint at the canvas size without keeping it. */
|
|
5831
|
+
async renderEffectPaintPixels(p) {
|
|
5747
5832
|
const provider = this.effects.provider;
|
|
5748
5833
|
const what = `the effect "${p.effect}"${p.provider ? ` from provider "${p.provider}"` : ""}`;
|
|
5749
5834
|
if (!provider) throw new PaintEditorError(`Effect paint needs ${what}, but no effects provider is registered; register it (setEffectsProvider) and try again.`);
|
|
@@ -5755,28 +5840,107 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5755
5840
|
} catch (e) {
|
|
5756
5841
|
throw new PaintEditorError(`Effect paint could not render ${what}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5757
5842
|
}
|
|
5758
|
-
|
|
5759
|
-
this.effectPaintPixels.set(effectPaintKey(p), img);
|
|
5760
|
-
while (this.effectPaintPixels.size > _PaintEditorController.EFFECT_PAINT_CACHE) this.effectPaintPixels.delete(this.effectPaintPixels.keys().next().value);
|
|
5761
|
-
return img;
|
|
5843
|
+
return toEffectPaintPixels(p, pixels);
|
|
5762
5844
|
}
|
|
5763
5845
|
/**
|
|
5764
5846
|
* Renders effect paints that are not rendered yet (strokes and replays need them rendered
|
|
5765
5847
|
* first, since strokes are synchronous). Throws naming the effect and provider when one cannot be.
|
|
5766
5848
|
*/
|
|
5767
5849
|
async prepareEffectPaints(paints2) {
|
|
5850
|
+
const list = [...paints2];
|
|
5851
|
+
this.effectPaintBatch = new Set(list.map(effectPaintKey)).size;
|
|
5768
5852
|
const seen = /* @__PURE__ */ new Set();
|
|
5769
|
-
for (const p of
|
|
5853
|
+
for (const p of list) {
|
|
5770
5854
|
const key = effectPaintKey(p);
|
|
5771
5855
|
if (seen.has(key)) continue;
|
|
5772
5856
|
seen.add(key);
|
|
5773
5857
|
await this.renderEffectPaint(p);
|
|
5774
5858
|
}
|
|
5775
5859
|
}
|
|
5860
|
+
/**
|
|
5861
|
+
* Renders the path-mapped effect paint for one or more strokes' points before a synchronous
|
|
5862
|
+
* stroke call runs (`paint_stroke` and `stroke_path` do; `paintStroke` throws without it on a
|
|
5863
|
+
* provider that can't render synchronously). For a stroke layer edit, see `prepareStrokeEdit`.
|
|
5864
|
+
*/
|
|
5865
|
+
async prepareStrokePathEffect(init, pointsList) {
|
|
5866
|
+
const brush = this.strokeBrush(init);
|
|
5867
|
+
if (!brushTakesPaint(brush)) return;
|
|
5868
|
+
let source = null;
|
|
5869
|
+
if (init.paint !== void 0) {
|
|
5870
|
+
if (!isPaintEffectPaint(init.paint)) return;
|
|
5871
|
+
source = readPaintEffectPaint(init.paint);
|
|
5872
|
+
} else if (init.color === void 0 && !brush.paint && !this.effectPaintPending) {
|
|
5873
|
+
source = this.effectPaint;
|
|
5874
|
+
}
|
|
5875
|
+
if (!source || source.map?.by !== "path") return;
|
|
5876
|
+
if (!this.effectPathControls.has(source.effect)) await this.describeEffect(source.effect);
|
|
5877
|
+
const control = this.effectPathControls.get(source.effect);
|
|
5878
|
+
const paints2 = pointsList.filter((pts) => pts.length > 0).map((pts) => withStrokePath(source, pts, control));
|
|
5879
|
+
if (paints2.length) await this.prepareEffectPaints(paints2);
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* Renders what an edit of a stroke layer's stroke paints with (`updateStroke`, `editStrokePoints`):
|
|
5883
|
+
* a new effect paint, or a path-mapped stroke's new path. Those calls are synchronous, and throw
|
|
5884
|
+
* without it on a provider that can't render synchronously. Bad edits are left for them to refuse.
|
|
5885
|
+
*/
|
|
5886
|
+
async prepareStrokeEdit(id, edit) {
|
|
5887
|
+
const p = this.strokeEditPaint(id, edit);
|
|
5888
|
+
if (!p) return;
|
|
5889
|
+
if (p.map?.by === "path" && !this.effectPathControls.has(p.effect)) await this.describeEffect(p.effect);
|
|
5890
|
+
await this.prepareEffectPaints([this.strokeEditPaint(id, edit) ?? p]);
|
|
5891
|
+
}
|
|
5892
|
+
/** Whether `prepareStrokeEdit` has to be awaited before this edit can run. */
|
|
5893
|
+
strokeEditNeedsRender(id, edit) {
|
|
5894
|
+
const p = this.strokeEditPaint(id, edit);
|
|
5895
|
+
return Boolean(p && !this.effectPaintImage(p) && !this.renderEffectPaintSync(p));
|
|
5896
|
+
}
|
|
5897
|
+
strokeEditPaint(id, edit) {
|
|
5898
|
+
const was = Object.values(this.doc.liveStrokes ?? {}).flat().find((s) => s.id === id);
|
|
5899
|
+
if (!was) return null;
|
|
5900
|
+
try {
|
|
5901
|
+
if (edit.paint !== void 0) {
|
|
5902
|
+
if (!isPaintEffectPaint(edit.paint)) return null;
|
|
5903
|
+
const p = readPaintEffectPaint(edit.paint);
|
|
5904
|
+
return p && withStrokePath(p, was.points, this.effectPathControls.get(p.effect));
|
|
5905
|
+
}
|
|
5906
|
+
if (was.paint?.type !== "effect" || was.paint.map?.by !== "path") return null;
|
|
5907
|
+
if (edit.points !== void 0) return withStrokePath(was.paint, this.checkPoints(edit.points), this.effectPathControls.get(was.paint.effect));
|
|
5908
|
+
if (!edit.transform) return null;
|
|
5909
|
+
const varied = varyStroke(was, this.strokeTransform(was, edit.transform), {}).paint;
|
|
5910
|
+
return varied?.type === "effect" ? varied : null;
|
|
5911
|
+
} catch {
|
|
5912
|
+
return null;
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
/** A stroke edit's transform as an affine map, checked. */
|
|
5916
|
+
strokeTransform(stroke, t) {
|
|
5917
|
+
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.`);
|
|
5918
|
+
for (const k of ["dx", "dy", "rotate"]) if (t[k] !== void 0 && !Number.isFinite(t[k])) throw new PaintEditorError(`${k} must be a number.`);
|
|
5919
|
+
const b = strokeBounds(stroke.points);
|
|
5920
|
+
return variationAffine(t, t.origin ?? { x: b.x + b.width / 2, y: b.y + b.height / 2 });
|
|
5921
|
+
}
|
|
5776
5922
|
/** Renders the effect paints of recorded strokes (default all of them) before a replay. */
|
|
5777
|
-
async prepareStrokeEffects(strokeIds) {
|
|
5923
|
+
async prepareStrokeEffects(strokeIds, variation) {
|
|
5778
5924
|
const ids = strokeIds ? new Set(strokeIds) : null;
|
|
5779
|
-
const
|
|
5925
|
+
const strokes = this.doc.strokes.filter((s) => (!ids || ids.has(s.id)) && s.paint?.type === "effect");
|
|
5926
|
+
const paints2 = strokes.map((s) => s.paint);
|
|
5927
|
+
if (variation && strokes.length) {
|
|
5928
|
+
const repeat = Math.max(1, Math.min(100, variation.repeat ?? 1));
|
|
5929
|
+
let origin = variation.origin;
|
|
5930
|
+
if (!origin) {
|
|
5931
|
+
const b = strokeBounds(strokes.flatMap((s) => s.points));
|
|
5932
|
+
origin = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
|
|
5933
|
+
}
|
|
5934
|
+
const step = variationAffine(variation, origin);
|
|
5935
|
+
let m = IDENTITY_AFFINE;
|
|
5936
|
+
for (let k = 0; k < repeat; k++) {
|
|
5937
|
+
m = [step[0] * m[0] + step[1] * m[2], step[0] * m[1] + step[1] * m[3], step[2] * m[0] + step[3] * m[2], step[2] * m[1] + step[3] * m[3], step[0] * m[4] + step[1] * m[5] + step[4], step[2] * m[4] + step[3] * m[5] + step[5]];
|
|
5938
|
+
for (const s of strokes) {
|
|
5939
|
+
const v = varyStroke(s, m, variation);
|
|
5940
|
+
if (v.paint?.type === "effect") paints2.push(v.paint);
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5780
5944
|
await this.prepareEffectPaints(paints2);
|
|
5781
5945
|
}
|
|
5782
5946
|
/** A small preview of the effect paint (a PNG data URL `size` px wide), or `null` without a rendered one. */
|
|
@@ -5804,7 +5968,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5804
5968
|
* Throws while the editor's is still rendering, for an engine that cannot paint with it, or when
|
|
5805
5969
|
* its pixels are not rendered.
|
|
5806
5970
|
*/
|
|
5807
|
-
strokeEffect(init, brush) {
|
|
5971
|
+
strokeEffect(init, brush, points) {
|
|
5808
5972
|
if (!brushTakesPaint(brush)) return null;
|
|
5809
5973
|
let source = null;
|
|
5810
5974
|
if (init.paint !== void 0) {
|
|
@@ -5816,8 +5980,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5816
5980
|
source = this.effectPaint;
|
|
5817
5981
|
}
|
|
5818
5982
|
if (!source) return null;
|
|
5819
|
-
const
|
|
5820
|
-
if (
|
|
5983
|
+
const path = source.map?.by === "path";
|
|
5984
|
+
if (path && points?.length) source = withStrokePath(source, points, this.effectPathControls.get(source.effect));
|
|
5985
|
+
const pixels = this.effectPaintImage(source) ?? (path ? this.renderEffectPaintSync(source) : null);
|
|
5986
|
+
if (!pixels) throw new PaintEditorError(`The effect paint "${source.effect}"${source.provider ? ` from provider "${source.provider}"` : ""} is not rendered at this canvas size; call ${path ? "prepareStrokePathEffect (the paint_stroke tool does this) or prepareStrokeEdit" : "set_effect_paint (or prepareEffectPaints)"} first.`);
|
|
5821
5987
|
return { source, pixels };
|
|
5822
5988
|
}
|
|
5823
5989
|
/**
|
|
@@ -6205,7 +6371,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6205
6371
|
strokeCopies() {
|
|
6206
6372
|
return symmetryCopies(this.guides.symmetry);
|
|
6207
6373
|
}
|
|
6208
|
-
sessionFor(doc, init, host) {
|
|
6374
|
+
sessionFor(doc, init, host, points) {
|
|
6209
6375
|
if (init.paint !== void 0 && !isPaintEffectPaint(init.paint)) {
|
|
6210
6376
|
const p = this.readStrokePaint(init.paint);
|
|
6211
6377
|
if (p.type === "color") init = { ...init, color: p.color, paint: void 0 };
|
|
@@ -6224,7 +6390,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6224
6390
|
})();
|
|
6225
6391
|
const brush = this.strokeBrush(init);
|
|
6226
6392
|
if (this.previewing && isWetBrush(brush)) throw new PaintEditorError(`Wet paint can't be previewed; the ${brush.name} brush paints wet media. Suggest it with a dry brush.`);
|
|
6227
|
-
const effect = this.strokeEffect(init, brush);
|
|
6393
|
+
const effect = this.strokeEffect(init, brush, points);
|
|
6394
|
+
if (!points && init.paint === void 0 && effect?.source.map?.by === "path" && isWetBrush(brush)) {
|
|
6395
|
+
throw new PaintEditorError(`The ${brush.name} brush paints wet media, which can't follow a path that isn't known until the stroke ends; paint with a dry brush, set_effect_paint with by: "stroke", or give the points to paint_stroke.`);
|
|
6396
|
+
}
|
|
6228
6397
|
if (brush.light.amount > 0) {
|
|
6229
6398
|
if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (glow-gel, neon-marker; list_brush_presets lists them), or set light.amount to 0 (set_brush).`);
|
|
6230
6399
|
}
|
|
@@ -6374,9 +6543,19 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6374
6543
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
6375
6544
|
this.emit(rects);
|
|
6376
6545
|
},
|
|
6377
|
-
sampled: (s, points) =>
|
|
6546
|
+
sampled: (s, points) => {
|
|
6547
|
+
this.notifySample("points", s, points);
|
|
6548
|
+
this.previewPathStroke(s);
|
|
6549
|
+
},
|
|
6378
6550
|
commit: (s, surface, cancelled) => {
|
|
6379
6551
|
this.session = null;
|
|
6552
|
+
const preview = this.pathPreview?.session === s ? this.pathPreview : null;
|
|
6553
|
+
this.pathPreview = null;
|
|
6554
|
+
const pathPaint = cancelled ? null : this.pathStrokePaint(s);
|
|
6555
|
+
if (pathPaint) {
|
|
6556
|
+
this.finishPathStroke(s, pathPaint, preview);
|
|
6557
|
+
return;
|
|
6558
|
+
}
|
|
6380
6559
|
this.live = null;
|
|
6381
6560
|
this.liveLayers = null;
|
|
6382
6561
|
this.liveThick = null;
|
|
@@ -6425,14 +6604,99 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6425
6604
|
throw e;
|
|
6426
6605
|
}
|
|
6427
6606
|
this.session = session;
|
|
6607
|
+
if (session.init.effect?.source.map?.by === "path" && !session.init.wet) this.pathPreview = { session, key: "", pending: null, landed: null };
|
|
6428
6608
|
this.emit([]);
|
|
6429
6609
|
this.notifySample("begin", session);
|
|
6430
6610
|
return session;
|
|
6431
6611
|
}
|
|
6612
|
+
/**
|
|
6613
|
+
* Renders a hand-painted path stroke's effect along the path drawn so far and paints the stroke
|
|
6614
|
+
* again with it when it lands, so the effect follows the pen while it draws. One render at a
|
|
6615
|
+
* time: when one lands, the next takes the newest path. A failed render leaves the preview as it
|
|
6616
|
+
* was; the render on release reports it.
|
|
6617
|
+
*/
|
|
6618
|
+
previewPathStroke(s) {
|
|
6619
|
+
const pv = this.pathPreview;
|
|
6620
|
+
if (!pv || pv.session !== s || pv.pending || !s.open) return;
|
|
6621
|
+
const paint = this.pathStrokePaint(s);
|
|
6622
|
+
if (!paint) return;
|
|
6623
|
+
const key = effectPaintKey(paint);
|
|
6624
|
+
if (key === pv.key) return;
|
|
6625
|
+
pv.key = key;
|
|
6626
|
+
const cached = this.effectPaintImage(paint);
|
|
6627
|
+
const pending = cached ? Promise.resolve(cached) : this.renderEffectPaintPixels(paint);
|
|
6628
|
+
pv.pending = pending;
|
|
6629
|
+
pending.then(
|
|
6630
|
+
(pixels) => {
|
|
6631
|
+
pv.pending = null;
|
|
6632
|
+
pv.landed = { key, pixels };
|
|
6633
|
+
if (this.pathPreview !== pv || !s.open) return;
|
|
6634
|
+
s.repaintEffect(pixels);
|
|
6635
|
+
this.previewPathStroke(s);
|
|
6636
|
+
},
|
|
6637
|
+
() => {
|
|
6638
|
+
pv.pending = null;
|
|
6639
|
+
}
|
|
6640
|
+
);
|
|
6641
|
+
}
|
|
6432
6642
|
/** The current live stroke, if any. */
|
|
6433
6643
|
get activeStroke() {
|
|
6434
6644
|
return this.session;
|
|
6435
6645
|
}
|
|
6646
|
+
/**
|
|
6647
|
+
* While a hand-painted stroke waits for its path render (`map: { by: 'path' }`), settles when it
|
|
6648
|
+
* has landed and rejects if it could not (the stroke is dropped); `null` otherwise. Until then
|
|
6649
|
+
* the editor is busy, as during a stroke, and shows the stroke as it was painted.
|
|
6650
|
+
*/
|
|
6651
|
+
get strokeRendering() {
|
|
6652
|
+
return this.pathStroke?.done ?? null;
|
|
6653
|
+
}
|
|
6654
|
+
/** The paint a finished hand-painted stroke must be painted again with, or `null` when it already painted with it. */
|
|
6655
|
+
pathStrokePaint(s) {
|
|
6656
|
+
const source = s.init.effect?.source;
|
|
6657
|
+
if (source?.map?.by !== "path" || !s.points.length) return null;
|
|
6658
|
+
const paint = withStrokePath(source, s.points, this.effectPathControls.get(source.effect));
|
|
6659
|
+
return effectPaintKey(paint) === effectPaintKey(source) ? null : paint;
|
|
6660
|
+
}
|
|
6661
|
+
/**
|
|
6662
|
+
* Ends a hand-painted stroke with a path-mapped effect. Its whole path isn't known until now, so
|
|
6663
|
+
* the live stroke was only a preview from renders along the path so far (`previewPathStroke`):
|
|
6664
|
+
* nothing of it was committed. Paints it again with the path's own render, as one stroke and one undo step, at
|
|
6665
|
+
* once if that can render synchronously and when the render lands if not.
|
|
6666
|
+
*/
|
|
6667
|
+
finishPathStroke(s, paint, preview) {
|
|
6668
|
+
const finish = () => {
|
|
6669
|
+
this.pathStroke = null;
|
|
6670
|
+
this.live = null;
|
|
6671
|
+
this.liveLayers = null;
|
|
6672
|
+
this.liveThick = null;
|
|
6673
|
+
const masks = s.init.selection ? (s.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask") : [];
|
|
6674
|
+
const load = s.init.mixerLoad;
|
|
6675
|
+
const result = this.paintStroke({ layerId: s.layerId, points: s.points, color: s.init.colorHex, brush: brushToJSON(s.init.brush), seed: s.init.seed, copies: s.init.affineCopies, ...load.reservoir || load.pickup ? { load } : {}, paint, snapToPerspective: false, stencils: false, maskStencils: masks });
|
|
6676
|
+
Object.assign(s.result, result);
|
|
6677
|
+
this.notifySample("end", s, [], result.strokeId);
|
|
6678
|
+
};
|
|
6679
|
+
const key = effectPaintKey(paint);
|
|
6680
|
+
if (preview?.landed?.key === key) this.cacheEffectPaintImage(paint, preview.landed.pixels);
|
|
6681
|
+
if (this.effectPaintImage(paint) || this.renderEffectPaintSync(paint)) {
|
|
6682
|
+
finish();
|
|
6683
|
+
return;
|
|
6684
|
+
}
|
|
6685
|
+
const render = preview?.pending && preview.key === key ? preview.pending.then((img) => this.cacheEffectPaintImage(paint, img)) : this.renderEffectPaint(paint);
|
|
6686
|
+
const done = render.then(finish, (e) => {
|
|
6687
|
+
this.pathStroke = null;
|
|
6688
|
+
this.live = null;
|
|
6689
|
+
this.liveLayers = null;
|
|
6690
|
+
this.liveThick = null;
|
|
6691
|
+
this.emit();
|
|
6692
|
+
this.notifySample("cancel", s, []);
|
|
6693
|
+
throw e;
|
|
6694
|
+
});
|
|
6695
|
+
done.catch(() => {
|
|
6696
|
+
});
|
|
6697
|
+
this.pathStroke = { effect: paint.effect, done };
|
|
6698
|
+
this.emit([]);
|
|
6699
|
+
}
|
|
6436
6700
|
/** Runs a whole stroke against `doc` without touching the editor; returns the new document. */
|
|
6437
6701
|
runStroke(doc, init, points) {
|
|
6438
6702
|
let out = doc;
|
|
@@ -6459,7 +6723,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6459
6723
|
}, stroke);
|
|
6460
6724
|
}
|
|
6461
6725
|
};
|
|
6462
|
-
const session = this.sessionFor(doc, init, host);
|
|
6726
|
+
const session = this.sessionFor(doc, init, host, points);
|
|
6463
6727
|
session.push(points);
|
|
6464
6728
|
const result = session.end();
|
|
6465
6729
|
if (session.init.wet) {
|
|
@@ -6609,6 +6873,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6609
6873
|
}
|
|
6610
6874
|
}
|
|
6611
6875
|
if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? "Replay stroke" : "Replay strokes", doc);
|
|
6876
|
+
this.effectPaintBatch = 0;
|
|
6877
|
+
this.trimEffectPaints();
|
|
6612
6878
|
return { strokeIds: ids, bounds, layerIds: [...layerIds] };
|
|
6613
6879
|
}
|
|
6614
6880
|
// --- Light paint --------------------------------------------------------------------
|
|
@@ -7912,8 +8178,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7912
8178
|
if (update.paint !== void 0) {
|
|
7913
8179
|
if (update.paint === null) delete next.paint;
|
|
7914
8180
|
else if (isPaintEffectPaint(update.paint)) {
|
|
7915
|
-
|
|
7916
|
-
|
|
8181
|
+
const ep = update.paint.map?.by === "path" ? withStrokePath(update.paint, next.points, this.effectPathControls.get(update.paint.effect)) : update.paint;
|
|
8182
|
+
if (!this.effectPaintImage(ep) && !this.renderEffectPaintSync(ep)) throw new PaintEditorError(`The effect "${update.paint.effect}" is not rendered yet; render it first (prepareEffectPaints; the update_stroke tool does this).`);
|
|
8183
|
+
next.paint = JSON.parse(JSON.stringify(ep));
|
|
7917
8184
|
} else {
|
|
7918
8185
|
const p = this.readStrokePaint(update.paint);
|
|
7919
8186
|
if (p.type === "color") {
|
|
@@ -7944,13 +8211,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7944
8211
|
if (edit.points !== void 0) {
|
|
7945
8212
|
const points = this.checkPoints(edit.points);
|
|
7946
8213
|
next = { ...was, points: points.map((p) => ({ ...p })) };
|
|
8214
|
+
if (next.paint?.type === "effect" && next.paint.map?.by === "path") {
|
|
8215
|
+
next.paint = withStrokePath(next.paint, next.points, this.effectPathControls.get(next.paint.effect));
|
|
8216
|
+
}
|
|
7947
8217
|
} else {
|
|
7948
|
-
|
|
7949
|
-
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.`);
|
|
7950
|
-
for (const k of ["dx", "dy", "rotate"]) if (t[k] !== void 0 && !Number.isFinite(t[k])) throw new PaintEditorError(`${k} must be a number.`);
|
|
7951
|
-
const b = strokeBounds(was.points);
|
|
7952
|
-
const m = variationAffine(t, t.origin ?? { x: b.x + b.width / 2, y: b.y + b.height / 2 });
|
|
7953
|
-
next = varyStroke(was, m, {});
|
|
8218
|
+
next = varyStroke(was, this.strokeTransform(was, edit.transform), {});
|
|
7954
8219
|
if (was.stencils) next.stencils = was.stencils;
|
|
7955
8220
|
}
|
|
7956
8221
|
const list = strokes.map((x, i) => i === index ? next : x);
|
|
@@ -8606,6 +8871,9 @@ function clipToCanvas(rect, canvas) {
|
|
|
8606
8871
|
const y1 = Math.min(canvas.height, rect.y + rect.height);
|
|
8607
8872
|
return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
|
|
8608
8873
|
}
|
|
8874
|
+
function toEffectPaintPixels(p, pixels) {
|
|
8875
|
+
return { width: pixels.width, height: pixels.height, data: new Uint8ClampedArray(pixels.data), ...p.map?.by === "stroke" ? { map: p.map } : {} };
|
|
8876
|
+
}
|
|
8609
8877
|
|
|
8610
8878
|
// src/paint-editor/presence.ts
|
|
8611
8879
|
var PaintPresence = class extends PresenceStore {
|
|
@@ -10320,6 +10588,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
10320
10588
|
if (refs.length) await c.prepareStickers(refs);
|
|
10321
10589
|
}
|
|
10322
10590
|
const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
|
|
10591
|
+
await c.prepareStrokePathEffect(stroke, [stroke.points]);
|
|
10323
10592
|
const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
|
|
10324
10593
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
10325
10594
|
}
|
|
@@ -10357,6 +10626,13 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
10357
10626
|
}
|
|
10358
10627
|
const path = options.origin && typeof a.path === "string" && a.path.trim() ? bezierPathToSvg(a.path, { offset: { x: -options.origin.x, y: -options.origin.y } }) : a.path;
|
|
10359
10628
|
const init = { path, spacing: a.spacing, pressure: a.pressure, speed: a.speed, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed };
|
|
10629
|
+
if (typeof path === "string" && path.trim()) {
|
|
10630
|
+
try {
|
|
10631
|
+
const sampled = samplePath(path, init);
|
|
10632
|
+
await c.prepareStrokePathEffect(init, sampled);
|
|
10633
|
+
} catch {
|
|
10634
|
+
}
|
|
10635
|
+
}
|
|
10360
10636
|
const r = await act(
|
|
10361
10637
|
() => {
|
|
10362
10638
|
if (typeof path !== "string" || !path.trim()) throw new PaintEditorError('path is empty; give SVG path data in document pixels, e.g. "M 10 10 C 40 0 80 60 120 30".');
|
|
@@ -10508,7 +10784,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
10508
10784
|
},
|
|
10509
10785
|
annotations: { title: "Move or reshape a stroke", ...WRITE5 },
|
|
10510
10786
|
coordinates: { in: ["points[]", "transform.origin"], out: ["bounds", "stroke.bounds", "stroke.points[]"] },
|
|
10511
|
-
handler: ({ id, points, transform }) => {
|
|
10787
|
+
handler: async ({ id, points, transform }) => {
|
|
10788
|
+
await c.prepareStrokeEdit(id, { points, transform });
|
|
10512
10789
|
const r = c.editStrokePoints(id, { points, transform });
|
|
10513
10790
|
return { stroke: strokeSummary(r.stroke, true, true), bounds: r.bounds };
|
|
10514
10791
|
}
|
|
@@ -10591,7 +10868,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
10591
10868
|
coordinates: { in: ["origin"], out: ["bounds"] },
|
|
10592
10869
|
handler: async (a) => {
|
|
10593
10870
|
const replay = a;
|
|
10594
|
-
if (replay.color === void 0) await c.prepareStrokeEffects(replay.strokeIds);
|
|
10871
|
+
if (replay.color === void 0) await c.prepareStrokeEffects(replay.strokeIds, replay);
|
|
10595
10872
|
return act({ kind: "replay", replay }, () => c.replayStrokes(replay));
|
|
10596
10873
|
}
|
|
10597
10874
|
});
|
|
@@ -10967,15 +11244,28 @@ var PaintGesture = class {
|
|
|
10967
11244
|
if (!m || m.pointerId !== e.pointerId) return;
|
|
10968
11245
|
this.mode = null;
|
|
10969
11246
|
this.predicted = [];
|
|
10970
|
-
if (m.kind === "stroke")
|
|
11247
|
+
if (m.kind === "stroke") {
|
|
11248
|
+
this.controller.activeStroke?.end();
|
|
11249
|
+
this.controller.strokeRendering?.catch((err) => {
|
|
11250
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
11251
|
+
});
|
|
11252
|
+
}
|
|
10971
11253
|
if (m.kind === "move-stroke") {
|
|
10972
11254
|
const dx = Math.round((this.doc(e).x - m.from.x) * 100) / 100;
|
|
10973
11255
|
const dy = Math.round((this.doc(e).y - m.from.y) * 100) / 100;
|
|
10974
11256
|
if (dx || dy) {
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
} catch (err) {
|
|
11257
|
+
const edit = { transform: { dx, dy } };
|
|
11258
|
+
const failed = (err) => {
|
|
10978
11259
|
this.error = err instanceof Error ? err.message : String(err);
|
|
11260
|
+
};
|
|
11261
|
+
if (this.controller.strokeEditNeedsRender(m.id, edit)) {
|
|
11262
|
+
this.controller.prepareStrokeEdit(m.id, edit).then(() => this.controller.editStrokePoints(m.id, edit)).catch(failed);
|
|
11263
|
+
} else {
|
|
11264
|
+
try {
|
|
11265
|
+
this.controller.editStrokePoints(m.id, edit);
|
|
11266
|
+
} catch (err) {
|
|
11267
|
+
failed(err);
|
|
11268
|
+
}
|
|
10979
11269
|
}
|
|
10980
11270
|
}
|
|
10981
11271
|
}
|
|
@@ -11482,9 +11772,9 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
11482
11772
|
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to paint with, in ms (default 0)." },
|
|
11483
11773
|
map: {
|
|
11484
11774
|
type: "object",
|
|
11485
|
-
description:
|
|
11775
|
+
description: `How the effect lies on the paint: by canvas (default: fixed to the canvas, painting reveals a hidden picture), 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), or by path (for effects with a path control format svg-path, e.g. search_effects accepts ["path"]: passes each stroke's centreline [x0, y0, x1, y1, \u2026] in canvas pixels to that control so the effect draws itself along the stroke; strokes longer than 256 points are simplified rather than refused).`,
|
|
11486
11776
|
properties: {
|
|
11487
|
-
by: { type: "string", enum: ["canvas", "stroke"], description: "canvas (default) or
|
|
11777
|
+
by: { type: "string", enum: ["canvas", "stroke", "path"], description: "canvas (default), stroke (bent strip along each stroke) or path (native path control for effects with format svg-path)." },
|
|
11488
11778
|
length: { type: "number", minimum: 16, maximum: 4096, description: "by stroke: px of stroke per repeat of the effect (default 300)." },
|
|
11489
11779
|
repeat: { type: "string", enum: ["repeat", "mirror", "clamp"], description: "by stroke: past the strip end start again (default), run back, or hold the last colour." }
|
|
11490
11780
|
},
|
|
@@ -12209,6 +12499,7 @@ export {
|
|
|
12209
12499
|
MAX_CANVAS_SIDE,
|
|
12210
12500
|
MAX_COORDINATE,
|
|
12211
12501
|
MAX_DOT_TEXTURES,
|
|
12502
|
+
MAX_EFFECT_PATH_POINTS,
|
|
12212
12503
|
MAX_IMAGE_SIDE,
|
|
12213
12504
|
MAX_LIVE_EFFECT_SOURCES,
|
|
12214
12505
|
MAX_PATH_SAMPLES,
|
|
@@ -12370,6 +12661,7 @@ export {
|
|
|
12370
12661
|
hslToRgb,
|
|
12371
12662
|
hsvToRgb,
|
|
12372
12663
|
hueToWheel,
|
|
12664
|
+
inferEffectPathControl,
|
|
12373
12665
|
invertAffine,
|
|
12374
12666
|
isBlendMode,
|
|
12375
12667
|
isGradientPaint,
|
|
@@ -12492,6 +12784,7 @@ export {
|
|
|
12492
12784
|
stickerSurface,
|
|
12493
12785
|
strokeBounds,
|
|
12494
12786
|
strokeDuration,
|
|
12787
|
+
strokePathPoints,
|
|
12495
12788
|
strokeSchedule,
|
|
12496
12789
|
strokeToJSON,
|
|
12497
12790
|
sweepPath,
|
|
@@ -12524,5 +12817,6 @@ export {
|
|
|
12524
12817
|
wheelPick,
|
|
12525
12818
|
wheelPoint,
|
|
12526
12819
|
wheelToHue,
|
|
12527
|
-
withPaintPlayback
|
|
12820
|
+
withPaintPlayback,
|
|
12821
|
+
withStrokePath
|
|
12528
12822
|
};
|