@pixerate/editor 0.226.0 → 0.227.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 +57 -57
- package/dist/animation-editor/index.js +3 -3
- package/dist/{brushstrokeRender-C3UU6GVC.js → brushstrokeRender-MAHOXL7K.js} +2 -2
- package/dist/{brushstrokeRender-EAJNE6WP.cjs → brushstrokeRender-WDTKD54B.cjs} +5 -5
- package/dist/canvas/index.cjs +8 -8
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-LCCMOWR7.cjs → chunk-ATKKG76G.cjs} +23 -23
- package/dist/{chunk-QAUIZPYI.cjs → chunk-BQE3Y6ZO.cjs} +2 -2
- package/dist/{chunk-G4E6RUPC.js → chunk-CDP2AKMA.js} +1 -1
- package/dist/{chunk-NDMXB4PR.js → chunk-CU6ANS5L.js} +1 -1
- package/dist/{chunk-ROVK6AXX.cjs → chunk-GNZI3KYK.cjs} +95 -23
- package/dist/{chunk-I7G3RSUJ.cjs → chunk-GXLZZVR5.cjs} +35 -35
- package/dist/{chunk-5YLOU7VF.js → chunk-KN3Y3FRY.js} +1 -1
- package/dist/{chunk-IWMDW7LB.cjs → chunk-MXJP77K4.cjs} +86 -86
- package/dist/{chunk-F7YA3MMC.js → chunk-TQOL6CF7.js} +76 -4
- package/dist/{chunk-ZMHPQ6HP.cjs → chunk-V3L7U63N.cjs} +1 -1
- package/dist/{chunk-DJBYJOHM.js → chunk-XSVUNSDJ.js} +1 -1
- package/dist/{chunk-BGOCAUXH.js → chunk-YMREOA67.js} +1 -1
- package/dist/image-editor/index.cjs +771 -771
- package/dist/image-editor/index.js +4 -4
- package/dist/paint-editor/controller.d.cts +54 -3
- package/dist/paint-editor/controller.d.ts +54 -3
- package/dist/paint-editor/dab-stroke.d.cts +3 -0
- package/dist/paint-editor/dab-stroke.d.ts +3 -0
- package/dist/paint-editor/effect-paint.d.cts +47 -0
- package/dist/paint-editor/effect-paint.d.ts +47 -0
- package/dist/paint-editor/effect-tools.d.cts +4 -2
- package/dist/paint-editor/effect-tools.d.ts +4 -2
- package/dist/paint-editor/effects.d.cts +3 -0
- package/dist/paint-editor/effects.d.ts +3 -0
- package/dist/paint-editor/engine.d.cts +4 -0
- package/dist/paint-editor/engine.d.ts +4 -0
- package/dist/paint-editor/index.cjs +536 -309
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +250 -23
- package/dist/paint-editor/procedural.d.cts +4 -0
- package/dist/paint-editor/procedural.d.ts +4 -0
- package/dist/paint-editor/stroke.d.cts +9 -0
- package/dist/paint-editor/stroke.d.ts +9 -0
- package/dist/paint-editor/strokes.d.cts +6 -2
- package/dist/paint-editor/strokes.d.ts +6 -2
- package/dist/vector/index.cjs +962 -962
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-canvas/SKILL.md +2 -1
- package/skills/pixerate-paint-editor/SKILL.md +42 -6
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +7 -0
|
@@ -40,6 +40,7 @@ export * from './stickers.cjs';
|
|
|
40
40
|
export * from './procedural.cjs';
|
|
41
41
|
export * from './sticker-tools.cjs';
|
|
42
42
|
export * from './effects.cjs';
|
|
43
|
+
export * from './effect-paint.cjs';
|
|
43
44
|
export * from './effect-tools.cjs';
|
|
44
45
|
export { WET_FLOW_KINDS, DEFAULT_FLOW, normalizeFlow, type WetFlowKind, type WetFlowSpec } from '../paint/wet/flow.cjs';
|
|
45
46
|
export { DEFAULT_WET_PARAMS, WET_PARAM_KEYS, normalizeWetParams, type WetParams, type WetTilt } from '../paint/wet/model.cjs';
|
|
@@ -40,6 +40,7 @@ export * from './stickers.js';
|
|
|
40
40
|
export * from './procedural.js';
|
|
41
41
|
export * from './sticker-tools.js';
|
|
42
42
|
export * from './effects.js';
|
|
43
|
+
export * from './effect-paint.js';
|
|
43
44
|
export * from './effect-tools.js';
|
|
44
45
|
export { WET_FLOW_KINDS, DEFAULT_FLOW, normalizeFlow, type WetFlowKind, type WetFlowSpec } from '../paint/wet/flow.js';
|
|
45
46
|
export { DEFAULT_WET_PARAMS, WET_PARAM_KEYS, normalizeWetParams, type WetParams, type WetTilt } from '../paint/wet/model.js';
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveExternalAssets,
|
|
8
8
|
serializeLayers,
|
|
9
9
|
serializeLayersExternal
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-KN3Y3FRY.js";
|
|
11
11
|
import "../chunk-3CI7FRMK.js";
|
|
12
12
|
import "../chunk-ZNA3IQZF.js";
|
|
13
13
|
import {
|
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
realPresenceClock,
|
|
59
59
|
tweenPresenceCursor
|
|
60
60
|
} from "../chunk-IVSWRVB4.js";
|
|
61
|
-
import "../chunk-
|
|
61
|
+
import "../chunk-CU6ANS5L.js";
|
|
62
62
|
import {
|
|
63
63
|
BRUSH_ID,
|
|
64
64
|
BRUSH_ROTATIONS,
|
|
@@ -95,7 +95,9 @@ import {
|
|
|
95
95
|
brushToJSON,
|
|
96
96
|
clonePaper,
|
|
97
97
|
drawSticker,
|
|
98
|
+
effectPaintKey,
|
|
98
99
|
hexToRgb8,
|
|
100
|
+
isPaintEffectPaint,
|
|
99
101
|
isWetBrush,
|
|
100
102
|
isWetCapable,
|
|
101
103
|
mixerTip,
|
|
@@ -115,15 +117,18 @@ import {
|
|
|
115
117
|
paperPreset,
|
|
116
118
|
parseBrushLibrary,
|
|
117
119
|
penOrientation,
|
|
120
|
+
readPaintEffectPaint,
|
|
118
121
|
renderPaper,
|
|
119
122
|
renderStickers,
|
|
123
|
+
sampleEffectAt,
|
|
124
|
+
sampleEffectField,
|
|
120
125
|
stampCoverage,
|
|
121
126
|
stickerRect,
|
|
122
127
|
stickerSurface,
|
|
123
128
|
tiltToOrientation,
|
|
124
129
|
toStrokePoint,
|
|
125
130
|
usesPaintExchange
|
|
126
|
-
} from "../chunk-
|
|
131
|
+
} from "../chunk-TQOL6CF7.js";
|
|
127
132
|
import "../chunk-M6DNZNVY.js";
|
|
128
133
|
import "../chunk-WEIK3UYB.js";
|
|
129
134
|
import "../chunk-PWBZH3FK.js";
|
|
@@ -449,7 +454,11 @@ var PaintDabStroke = class {
|
|
|
449
454
|
const ex = st.exchange;
|
|
450
455
|
ex.pickUp(this.medium.sample(rect, tip));
|
|
451
456
|
let flow = 1;
|
|
452
|
-
if (this.
|
|
457
|
+
if (this.options.effect) {
|
|
458
|
+
const rgb = [0, 0, 0];
|
|
459
|
+
flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb);
|
|
460
|
+
ex.setReservoirColor(rgb[0], rgb[1], rgb[2]);
|
|
461
|
+
} else if (this.sampler && !this.gradient) {
|
|
453
462
|
const g = this.sampler.dab(d, this.sample);
|
|
454
463
|
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
455
464
|
flow = g.alpha;
|
|
@@ -984,6 +993,8 @@ var PaintStrokeSession = class {
|
|
|
984
993
|
this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
985
994
|
const brush = init.brush;
|
|
986
995
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
996
|
+
const effect = init.effect?.pixels ?? null;
|
|
997
|
+
if (effect && (init.wet || init.thick || init.sticker)) throw new Error(`The ${brush.name} brush (${brush.engine} engine) cannot paint with effect paint.`);
|
|
987
998
|
this.apply = {
|
|
988
999
|
mode: brush.engine === "eraser" ? "erase" : "paint",
|
|
989
1000
|
color: init.color,
|
|
@@ -1011,10 +1022,11 @@ var PaintStrokeSession = class {
|
|
|
1011
1022
|
paperDepth: init.paperDepth,
|
|
1012
1023
|
seed: init.seed,
|
|
1013
1024
|
copies: this.copies,
|
|
1014
|
-
paint: init.paint ?? null
|
|
1025
|
+
paint: init.paint ?? null,
|
|
1026
|
+
effect
|
|
1015
1027
|
});
|
|
1016
1028
|
} else {
|
|
1017
|
-
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null };
|
|
1029
|
+
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect };
|
|
1018
1030
|
this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
|
|
1019
1031
|
}
|
|
1020
1032
|
}
|
|
@@ -1473,8 +1485,10 @@ function normalizeBrushStroke(raw) {
|
|
|
1473
1485
|
}
|
|
1474
1486
|
if (st.length) out.stencils = st;
|
|
1475
1487
|
}
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1488
|
+
const effect = readPaintEffectPaint(r.paint);
|
|
1489
|
+
const paint = effect ? null : readPaintSource(r.paint);
|
|
1490
|
+
if (effect) out.paint = effect;
|
|
1491
|
+
else if (paint && paint.type === "gradient") out.paint = paint;
|
|
1478
1492
|
if (Array.isArray(r.steps) && r.steps.length === points.length && r.steps.every((v) => Number.isInteger(v) && v >= 0)) out.steps = [...r.steps];
|
|
1479
1493
|
return out;
|
|
1480
1494
|
}
|
|
@@ -1512,7 +1526,7 @@ function varyStroke(stroke, m, v) {
|
|
|
1512
1526
|
});
|
|
1513
1527
|
const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;
|
|
1514
1528
|
const out = { ...stroke, brush, points };
|
|
1515
|
-
if (stroke.paint && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);
|
|
1529
|
+
if (stroke.paint?.type === "gradient" && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);
|
|
1516
1530
|
if (stroke.copies?.length) {
|
|
1517
1531
|
const inv = invertAffine(m);
|
|
1518
1532
|
out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));
|
|
@@ -3091,6 +3105,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
3091
3105
|
const maxStrokes = options.strokes ?? source.strokes.length;
|
|
3092
3106
|
const chunk = Math.max(1, options.wetChunk ?? 0) || Infinity;
|
|
3093
3107
|
let strokes = 0;
|
|
3108
|
+
await target.prepareEffectPaints(source.strokes.slice(0, maxStrokes).flatMap((st) => st.paint?.type === "effect" ? [st.paint] : []));
|
|
3094
3109
|
const advanceTo = async (clock) => {
|
|
3095
3110
|
const now = target.getState().wet.clock;
|
|
3096
3111
|
let left = clock - now;
|
|
@@ -3262,19 +3277,23 @@ var PAINT_EFFECT_PLACES = {
|
|
|
3262
3277
|
/** An effect fill: composited onto a layer's pixels. */
|
|
3263
3278
|
fill: ["generator", "overlay", "title"],
|
|
3264
3279
|
/** Light (or any filter) on a layer's dried paint. */
|
|
3265
|
-
light: ["filter"]
|
|
3280
|
+
light: ["filter"],
|
|
3281
|
+
/** Effect paint: brushes take each pixel's colour from it. */
|
|
3282
|
+
paint: ["generator", "overlay", "title"]
|
|
3266
3283
|
};
|
|
3267
3284
|
var PLACE_WORDS = {
|
|
3268
3285
|
paper: "effect paper takes generator effects",
|
|
3269
3286
|
layer: "an effect layer takes generator, overlay or title effects",
|
|
3270
3287
|
fill: "an effect fill takes generator, overlay or title effects",
|
|
3271
|
-
light: "light on dried paint takes filter effects"
|
|
3288
|
+
light: "light on dried paint takes filter effects",
|
|
3289
|
+
paint: "effect paint takes generator, overlay or title effects"
|
|
3272
3290
|
};
|
|
3273
3291
|
function checkPaintEffectPlace(d, place) {
|
|
3274
3292
|
const allowed = PAINT_EFFECT_PLACES[place];
|
|
3275
3293
|
if (d.kind !== "motion" && d.kind !== "vector" && d.uses.some((u) => allowed.includes(u))) return;
|
|
3276
3294
|
let instead = "";
|
|
3277
|
-
if (d.uses.includes("filter") && place
|
|
3295
|
+
if (d.uses.includes("filter") && place === "paint") instead = "; paint with a generator, overlay or title effect, or run the filter on the painted layer with apply_effect";
|
|
3296
|
+
else if (d.uses.includes("filter") && place !== "light") instead = "; run it on a layer's dried paint with apply_effect instead";
|
|
3278
3297
|
else if (place === "light" && d.uses.some((u) => PAINT_EFFECT_PLACES.layer.includes(u))) instead = "; add it as an effect layer (add_effect_layer) or fill a layer with it (fill_layer_with_effect) instead";
|
|
3279
3298
|
else if (place === "paper" && d.uses.some((u) => u === "overlay" || u === "title")) instead = "; add it as an effect layer with add_effect_layer instead";
|
|
3280
3299
|
throw new EffectUnsupportedError(`"${d.id}" (${d.name}) is a ${d.uses.join(" / ") || d.kind} effect, and ${PLACE_WORDS[place]}${instead}.`, d.id);
|
|
@@ -3418,6 +3437,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3418
3437
|
secondaryColor = "#ffffff";
|
|
3419
3438
|
/** Gradient paint chosen with `setPaint` (`null`: the paint colour). */
|
|
3420
3439
|
gradient = null;
|
|
3440
|
+
/** Effect paint chosen with `setEffectPaint` (rendered), and one still rendering. */
|
|
3441
|
+
effectPaint = null;
|
|
3442
|
+
effectPaintPending = null;
|
|
3443
|
+
/** Changes whenever the paint is chosen, so an effect paint that finishes rendering late does not take over. */
|
|
3444
|
+
paintEpoch = 0;
|
|
3445
|
+
/** Rendered effect paints by `effectPaintKey` (most recently used last). */
|
|
3446
|
+
effectPaintPixels = /* @__PURE__ */ new Map();
|
|
3421
3447
|
mixerLoad = EMPTY_MIXER_LOAD;
|
|
3422
3448
|
view = { ...DEFAULT_VIEW, pan: { ...DEFAULT_VIEW.pan } };
|
|
3423
3449
|
hand;
|
|
@@ -3512,6 +3538,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3512
3538
|
color: this.color,
|
|
3513
3539
|
secondaryColor: this.secondaryColor,
|
|
3514
3540
|
paint: this.gradient ?? { type: "color", color: this.color },
|
|
3541
|
+
effectPaint: this.effectPaintPending ?? this.effectPaint,
|
|
3542
|
+
effectPaintRendering: Boolean(this.effectPaintPending),
|
|
3515
3543
|
mixerLoad: this.mixerLoad,
|
|
3516
3544
|
view: this.view,
|
|
3517
3545
|
hand: this.hand,
|
|
@@ -4296,6 +4324,141 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4296
4324
|
this.commit("Effect fill", { ...this.doc, layers: mapLayer(this.doc.layers, now.id, () => ({ ...now, surface })) });
|
|
4297
4325
|
return { layerId: now.id, source: this.sourceOf(d, values, options.time) };
|
|
4298
4326
|
}
|
|
4327
|
+
// --- Effect paint ----------------------------------------------------------------
|
|
4328
|
+
/** At most this many rendered effect paints are kept (each is canvas-sized). */
|
|
4329
|
+
static EFFECT_PAINT_CACHE = 6;
|
|
4330
|
+
/** The effect paint strokes use (or the one rendering), or `null`. */
|
|
4331
|
+
getEffectPaint() {
|
|
4332
|
+
const p = this.effectPaintPending ?? this.effectPaint;
|
|
4333
|
+
return p ? { ...p, values: JSON.parse(JSON.stringify(p.values)) } : null;
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* Paints with a provider generator, overlay or title effect: it is rendered once at the canvas
|
|
4337
|
+
* size, and brushes then take each pixel's colour (and alpha) from it, fixed to the canvas, as if
|
|
4338
|
+
* painting a hidden picture into view. `setColor` and `setPaint` go back to colour. Editor state,
|
|
4339
|
+
* not an undo step. Strokes that would paint with it throw while it renders. Returns the paint.
|
|
4340
|
+
*/
|
|
4341
|
+
async setEffectPaint(source) {
|
|
4342
|
+
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).");
|
|
4343
|
+
this.checkTime(source.time);
|
|
4344
|
+
const epoch = ++this.paintEpoch;
|
|
4345
|
+
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.`);
|
|
4346
|
+
const d = await this.describeEffect(source.effect, "paint");
|
|
4347
|
+
const values = this.effectValues(d, { ...d.defaults }, source.values);
|
|
4348
|
+
if (epoch !== this.paintEpoch) throw changed(d.name);
|
|
4349
|
+
const paint = { type: "effect", effect: d.id, provider: this.effects.require().id, values, time: source.time ?? 0 };
|
|
4350
|
+
this.effectPaintPending = paint;
|
|
4351
|
+
this.emit([]);
|
|
4352
|
+
try {
|
|
4353
|
+
await this.renderEffectPaint(paint);
|
|
4354
|
+
} catch (e) {
|
|
4355
|
+
if (this.effectPaintPending === paint) {
|
|
4356
|
+
this.effectPaintPending = null;
|
|
4357
|
+
this.emit([]);
|
|
4358
|
+
}
|
|
4359
|
+
throw e;
|
|
4360
|
+
}
|
|
4361
|
+
if (this.effectPaintPending !== paint || epoch !== this.paintEpoch) throw changed(d.name);
|
|
4362
|
+
this.effectPaintPending = null;
|
|
4363
|
+
this.effectPaint = paint;
|
|
4364
|
+
this.gradient = null;
|
|
4365
|
+
this.emit([]);
|
|
4366
|
+
return this.getEffectPaint();
|
|
4367
|
+
}
|
|
4368
|
+
/** The rendered pixels of an effect paint at the current canvas size, or `null` when it has not been rendered. */
|
|
4369
|
+
effectPaintImage(p) {
|
|
4370
|
+
const key = effectPaintKey(p);
|
|
4371
|
+
const img = this.effectPaintPixels.get(key);
|
|
4372
|
+
if (!img || img.width !== this.doc.canvas.width || img.height !== this.doc.canvas.height) return null;
|
|
4373
|
+
this.effectPaintPixels.delete(key);
|
|
4374
|
+
this.effectPaintPixels.set(key, img);
|
|
4375
|
+
return img;
|
|
4376
|
+
}
|
|
4377
|
+
/** Renders an effect paint at the canvas size into the cache (nothing when it is there already). */
|
|
4378
|
+
async renderEffectPaint(p) {
|
|
4379
|
+
const have = this.effectPaintImage(p);
|
|
4380
|
+
if (have) return have;
|
|
4381
|
+
const provider = this.effects.provider;
|
|
4382
|
+
const what = `the effect "${p.effect}"${p.provider ? ` from provider "${p.provider}"` : ""}`;
|
|
4383
|
+
if (!provider) throw new PaintEditorError(`Effect paint needs ${what}, but no effects provider is registered; register it (setEffectsProvider) and try again.`);
|
|
4384
|
+
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.`);
|
|
4385
|
+
const { width, height } = this.doc.canvas;
|
|
4386
|
+
let pixels;
|
|
4387
|
+
try {
|
|
4388
|
+
pixels = await this.effectsRenderer.render(provider, p.effect, { width, height }, { time: p.time, values: p.values });
|
|
4389
|
+
} catch (e) {
|
|
4390
|
+
throw new PaintEditorError(`Effect paint could not render ${what}: ${e instanceof Error ? e.message : String(e)}`);
|
|
4391
|
+
}
|
|
4392
|
+
const img = { width, height, data: new Uint8ClampedArray(pixels.data) };
|
|
4393
|
+
this.effectPaintPixels.set(effectPaintKey(p), img);
|
|
4394
|
+
while (this.effectPaintPixels.size > _PaintEditorController.EFFECT_PAINT_CACHE) this.effectPaintPixels.delete(this.effectPaintPixels.keys().next().value);
|
|
4395
|
+
return img;
|
|
4396
|
+
}
|
|
4397
|
+
/**
|
|
4398
|
+
* Renders effect paints that are not rendered yet (strokes and replays need them rendered
|
|
4399
|
+
* first, since strokes are synchronous). Throws naming the effect and provider when one cannot be.
|
|
4400
|
+
*/
|
|
4401
|
+
async prepareEffectPaints(paints) {
|
|
4402
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4403
|
+
for (const p of paints) {
|
|
4404
|
+
const key = effectPaintKey(p);
|
|
4405
|
+
if (seen.has(key)) continue;
|
|
4406
|
+
seen.add(key);
|
|
4407
|
+
await this.renderEffectPaint(p);
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
/** Renders the effect paints of recorded strokes (default all of them) before a replay. */
|
|
4411
|
+
async prepareStrokeEffects(strokeIds) {
|
|
4412
|
+
const ids = strokeIds ? new Set(strokeIds) : null;
|
|
4413
|
+
const paints = this.doc.strokes.filter((s) => (!ids || ids.has(s.id)) && s.paint?.type === "effect").map((s) => s.paint);
|
|
4414
|
+
await this.prepareEffectPaints(paints);
|
|
4415
|
+
}
|
|
4416
|
+
/** A small preview of the effect paint (a PNG data URL `size` px wide), or `null` without a rendered one. */
|
|
4417
|
+
effectPaintThumbnail(size = 48) {
|
|
4418
|
+
const p = this.effectPaint;
|
|
4419
|
+
const img = p ? this.effectPaintImage(p) : null;
|
|
4420
|
+
if (!img) return null;
|
|
4421
|
+
const w = Math.max(1, Math.min(img.width, Math.round(size)));
|
|
4422
|
+
const h = Math.max(1, Math.round(img.height / img.width * w));
|
|
4423
|
+
const out = new Uint8ClampedArray(w * h * 4);
|
|
4424
|
+
for (let y = 0; y < h; y++) {
|
|
4425
|
+
const sy = Math.min(img.height - 1, Math.floor((y + 0.5) * img.height / h));
|
|
4426
|
+
for (let x = 0; x < w; x++) {
|
|
4427
|
+
const sx = Math.min(img.width - 1, Math.floor((x + 0.5) * img.width / w));
|
|
4428
|
+
const i = (sy * img.width + sx) * 4;
|
|
4429
|
+
const o = (y * w + x) * 4;
|
|
4430
|
+
for (let c = 0; c < 4; c++) out[o + c] = img.data[i + c];
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
return pngDataUrl(w, h, out);
|
|
4434
|
+
}
|
|
4435
|
+
/**
|
|
4436
|
+
* The effect paint a stroke paints with and its pixels, or `null`: the stroke's own effect
|
|
4437
|
+
* `paint`, else (without its own `paint` or `color`, and with no brush gradient) the editor's.
|
|
4438
|
+
* Throws while the editor's is still rendering, for an engine that cannot paint with it, or when
|
|
4439
|
+
* its pixels are not rendered.
|
|
4440
|
+
*/
|
|
4441
|
+
strokeEffect(init, brush) {
|
|
4442
|
+
if (!brushTakesPaint(brush)) return null;
|
|
4443
|
+
let source = null;
|
|
4444
|
+
if (init.paint !== void 0) {
|
|
4445
|
+
if (!isPaintEffectPaint(init.paint)) return null;
|
|
4446
|
+
source = readPaintEffectPaint(init.paint);
|
|
4447
|
+
if (!source) throw new PaintEditorError("The stroke's effect paint needs an effect id and values (getEffectPaint returns one).");
|
|
4448
|
+
} else if (init.color === void 0 && !brush.paint) {
|
|
4449
|
+
if (this.effectPaintPending) throw new PaintEditorError(`The effect paint "${this.effectPaintPending.effect}" is still rendering; wait for set_effect_paint to finish, then paint.`);
|
|
4450
|
+
source = this.effectPaint;
|
|
4451
|
+
}
|
|
4452
|
+
if (!source) return null;
|
|
4453
|
+
if (brush.engine === "wet" || brush.engine === "impasto" || brush.engine === "sticker") {
|
|
4454
|
+
throw new PaintEditorError(
|
|
4455
|
+
`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}.`
|
|
4456
|
+
);
|
|
4457
|
+
}
|
|
4458
|
+
const pixels = this.effectPaintImage(source);
|
|
4459
|
+
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.`);
|
|
4460
|
+
return { source, pixels };
|
|
4461
|
+
}
|
|
4299
4462
|
/**
|
|
4300
4463
|
* Runs a filter effect (light, glow, colour grade …) on a layer's dried paint and bakes it in, at
|
|
4301
4464
|
* `amount` strength (0..1, default 1). Transparent areas stay transparent; thick paint keeps its
|
|
@@ -4438,6 +4601,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4438
4601
|
setColor(color) {
|
|
4439
4602
|
this.color = parseColor(color);
|
|
4440
4603
|
this.gradient = null;
|
|
4604
|
+
this.effectPaint = this.effectPaintPending = null;
|
|
4605
|
+
this.paintEpoch++;
|
|
4441
4606
|
this.emit([]);
|
|
4442
4607
|
return this.color;
|
|
4443
4608
|
}
|
|
@@ -4460,6 +4625,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4460
4625
|
return p;
|
|
4461
4626
|
}
|
|
4462
4627
|
this.gradient = p;
|
|
4628
|
+
this.effectPaint = this.effectPaintPending = null;
|
|
4629
|
+
this.paintEpoch++;
|
|
4463
4630
|
this.emit([]);
|
|
4464
4631
|
return p;
|
|
4465
4632
|
}
|
|
@@ -4472,6 +4639,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4472
4639
|
if (!brushTakesPaint(brush)) return null;
|
|
4473
4640
|
let g = null;
|
|
4474
4641
|
if (init.paint !== void 0) {
|
|
4642
|
+
if (isPaintEffectPaint(init.paint)) return null;
|
|
4475
4643
|
const p = this.readStrokePaint(init.paint);
|
|
4476
4644
|
g = p.type === "gradient" ? p : null;
|
|
4477
4645
|
} else g = brush.paint ?? (init.color === void 0 ? this.gradient : null);
|
|
@@ -4561,7 +4729,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4561
4729
|
return symmetryCopies(this.guides.symmetry);
|
|
4562
4730
|
}
|
|
4563
4731
|
sessionFor(doc, init, host) {
|
|
4564
|
-
if (init.paint !== void 0) {
|
|
4732
|
+
if (init.paint !== void 0 && !isPaintEffectPaint(init.paint)) {
|
|
4565
4733
|
const p = this.readStrokePaint(init.paint);
|
|
4566
4734
|
if (p.type === "color") init = { ...init, color: p.color, paint: void 0 };
|
|
4567
4735
|
else init = { ...init, paint: p };
|
|
@@ -4578,6 +4746,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4578
4746
|
return l;
|
|
4579
4747
|
})();
|
|
4580
4748
|
const brush = this.strokeBrush(init);
|
|
4749
|
+
const effect = this.strokeEffect(init, brush);
|
|
4581
4750
|
const stickerLayer = Boolean(doc.stickers?.[layer.id]?.length);
|
|
4582
4751
|
if (brush.engine === "sticker") {
|
|
4583
4752
|
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.`);
|
|
@@ -4659,7 +4828,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4659
4828
|
wet,
|
|
4660
4829
|
thick: brush.engine === "impasto" ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
4661
4830
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc) } : void 0,
|
|
4662
|
-
paint: this.strokeGradient(init, brush, color)
|
|
4831
|
+
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
4832
|
+
effect
|
|
4663
4833
|
});
|
|
4664
4834
|
if (wet) this.wet.driver = wet.driver;
|
|
4665
4835
|
if (init.maskStencils) session.maskStencils = init.maskStencils;
|
|
@@ -4676,7 +4846,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4676
4846
|
seed: init.seed
|
|
4677
4847
|
};
|
|
4678
4848
|
if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m]);
|
|
4679
|
-
if (init.
|
|
4849
|
+
if (init.effect) stroke.paint = JSON.parse(JSON.stringify(init.effect.source));
|
|
4850
|
+
else if (init.paint) stroke.paint = JSON.parse(JSON.stringify(init.paint));
|
|
4680
4851
|
if (init.mixerLoad.reservoir || init.mixerLoad.pickup) stroke.load = JSON.parse(JSON.stringify(init.mixerLoad));
|
|
4681
4852
|
if (init.wet) stroke.steps = [...init.wet.driver.steps];
|
|
4682
4853
|
if (init.selection) {
|
|
@@ -4844,6 +5015,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4844
5015
|
if (options.color !== void 0) parseColor(options.color);
|
|
4845
5016
|
if (options.brushId !== void 0) this.requireBrush(options.brushId);
|
|
4846
5017
|
if (options.layerId !== void 0) this.paintTarget(options.layerId, "replay onto");
|
|
5018
|
+
if (options.color === void 0) {
|
|
5019
|
+
for (const s of strokes) {
|
|
5020
|
+
if (s.paint?.type === "effect" && !this.effectPaintImage(s.paint)) {
|
|
5021
|
+
throw new PaintEditorError(`Stroke "${s.id}" paints with the effect "${s.paint.effect}"${s.paint.provider ? ` from provider "${s.paint.provider}"` : ""}, which is not rendered in this editor; render it first (prepareStrokeEffects; the replay_strokes tool does this) or recolour the replay with color.`);
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
4847
5025
|
let origin = options.origin;
|
|
4848
5026
|
if (!origin) {
|
|
4849
5027
|
const b = strokeBounds(strokes.flatMap((s) => s.points));
|
|
@@ -4938,6 +5116,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4938
5116
|
const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };
|
|
4939
5117
|
target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });
|
|
4940
5118
|
target.lighting = { ...this.lighting };
|
|
5119
|
+
target.effects.set(this.effects.provider);
|
|
5120
|
+
if (w === width && h === height) for (const [key, img] of this.effectPaintPixels) target.effectPaintPixels.set(key, img);
|
|
4941
5121
|
return target;
|
|
4942
5122
|
}
|
|
4943
5123
|
/**
|
|
@@ -4989,6 +5169,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4989
5169
|
dispose() {
|
|
4990
5170
|
this.wet.destroy();
|
|
4991
5171
|
this.effectsRenderer.dispose();
|
|
5172
|
+
this.effectPaintPixels.clear();
|
|
4992
5173
|
}
|
|
4993
5174
|
/** Resolves when wet paint is between steps (edits, undo and exports can run). */
|
|
4994
5175
|
wetIdle() {
|
|
@@ -7833,7 +8014,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
7833
8014
|
add({
|
|
7834
8015
|
name: "set_color",
|
|
7835
8016
|
title: "Set the paint colour",
|
|
7836
|
-
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Editor state, not an undo step. Returns both colours.",
|
|
8017
|
+
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours.",
|
|
7837
8018
|
inputSchema: {
|
|
7838
8019
|
type: "object",
|
|
7839
8020
|
properties: { color: { ...COLOR3, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." } },
|
|
@@ -7850,7 +8031,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
7850
8031
|
add({
|
|
7851
8032
|
name: "set_paint",
|
|
7852
8033
|
title: "Set the paint (colour or gradient)",
|
|
7853
|
-
description: 'Sets what strokes paint with: one colour, or gradient paint that changes colour as the stroke goes. Gradient paint: 2..16 stops sorted by offset (#rrggbb, or "paint" / "secondary" for the two editor colours), space pigment (default: mixes like paint) / oklab / srgb, and map: distance (default: the colour changes as the stroke is dragged out, 300 px then mirrored back), pressure, tilt, velocity, direction, time, across-tip or random. Every stroking brush samples it per dab (dab, mixer, watercolour and gouache, oil and impasto, particle, bristle, sticker); erasers, blenders and smudges ignore it, and a brush with its own gradient (get_brush paint) paints that instead. set_color goes back to one colour. Editor state, not an undo step. Returns paint, color and secondaryColor.',
|
|
8034
|
+
description: 'Sets what strokes paint with: one colour, or gradient paint that changes colour as the stroke goes. Gradient paint: 2..16 stops sorted by offset (#rrggbb, or "paint" / "secondary" for the two editor colours), space pigment (default: mixes like paint) / oklab / srgb, and map: distance (default: the colour changes as the stroke is dragged out, 300 px then mirrored back), pressure, tilt, velocity, direction, time, across-tip or random. Every stroking brush samples it per dab (dab, mixer, watercolour and gouache, oil and impasto, particle, bristle, sticker); erasers, blenders and smudges ignore it, and a brush with its own gradient (get_brush paint) paints that instead. It also ends effect paint (set_effect_paint); set_color goes back to one colour. Editor state, not an undo step. Returns paint, color and secondaryColor.',
|
|
7854
8035
|
inputSchema: { type: "object", properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ["paint"], additionalProperties: false },
|
|
7855
8036
|
annotations: { title: "Set the paint (colour or gradient)", ...SET4 },
|
|
7856
8037
|
handler: ({ paint }) => {
|
|
@@ -8001,8 +8182,9 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
8001
8182
|
},
|
|
8002
8183
|
annotations: { title: "Replay strokes, varied", ...WRITE5 },
|
|
8003
8184
|
coordinates: { in: ["origin"], out: ["bounds"] },
|
|
8004
|
-
handler: (a) => {
|
|
8185
|
+
handler: async (a) => {
|
|
8005
8186
|
const replay = a;
|
|
8187
|
+
if (replay.color === void 0) await c.prepareStrokeEffects(replay.strokeIds);
|
|
8006
8188
|
return act({ kind: "replay", replay }, () => c.replayStrokes(replay));
|
|
8007
8189
|
}
|
|
8008
8190
|
});
|
|
@@ -8692,6 +8874,7 @@ function quickStencil(kind, canvas) {
|
|
|
8692
8874
|
|
|
8693
8875
|
// src/paint-editor/effect-tools.ts
|
|
8694
8876
|
var PAPER_TARGET = "paper";
|
|
8877
|
+
var PAINT_TARGET = "paint";
|
|
8695
8878
|
var ref = (s) => ({ effect: s.effect, ...s.name ? { name: s.name } : {}, ...s.provider ? { provider: s.provider } : {} });
|
|
8696
8879
|
function createPaintEffectsToolProvider(controller, options = {}) {
|
|
8697
8880
|
const readOnlyReason = () => {
|
|
@@ -8702,6 +8885,10 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
8702
8885
|
const p = controller.getPaperEffect();
|
|
8703
8886
|
return p ? [{ id: PAPER_TARGET, ...ref(p), role: "effect paper", enabled: true, opacity: p.opacity, blend: p.blend, values: p.values, ...p.time ? { time: p.time } : {} }] : [];
|
|
8704
8887
|
};
|
|
8888
|
+
const paintInfo = () => {
|
|
8889
|
+
const p = controller.getEffectPaint();
|
|
8890
|
+
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 } : {} }] : [];
|
|
8891
|
+
};
|
|
8705
8892
|
const layerInfo = (layerId) => {
|
|
8706
8893
|
const s = controller.getEffectLayer(layerId);
|
|
8707
8894
|
return s ? [{ id: "effect", ...ref(s), role: "effect layer", enabled: true, values: s.values, ...s.time ? { time: s.time } : {} }] : [];
|
|
@@ -8714,14 +8901,14 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
8714
8901
|
}
|
|
8715
8902
|
return l;
|
|
8716
8903
|
};
|
|
8717
|
-
const effectsOf = (target) => target === PAPER_TARGET ? paperInfo() : layerInfo(target);
|
|
8904
|
+
const effectsOf = (target) => target === PAPER_TARGET ? paperInfo() : target === PAINT_TARGET ? paintInfo() : layerInfo(target);
|
|
8718
8905
|
const instanceCheck = (target, instance) => {
|
|
8719
|
-
const want = target === PAPER_TARGET ?
|
|
8720
|
-
if (target !== PAPER_TARGET) requireLayer(target);
|
|
8906
|
+
const want = target === PAPER_TARGET || target === PAINT_TARGET ? target : "effect";
|
|
8907
|
+
if (target !== PAPER_TARGET && target !== PAINT_TARGET) requireLayer(target);
|
|
8721
8908
|
if (instance !== want || !effectsOf(target).length) {
|
|
8722
8909
|
const have = effectsOf(target).map((e) => `"${e.id}"`);
|
|
8723
8910
|
throw new Error(
|
|
8724
|
-
`${target === PAPER_TARGET ? "The paper" : `Layer "${target}"`} has no provider effect "${instance}"; its effects are ${have.join(", ") || "none"} (list_applied_effects). Light baked into paint is part of the pixels: undo takes it back.`
|
|
8911
|
+
`${target === PAPER_TARGET ? "The paper" : target === PAINT_TARGET ? "The paint" : `Layer "${target}"`} has no provider effect "${instance}"; its effects are ${have.join(", ") || "none"} (list_applied_effects). Light baked into paint is part of the pixels: undo takes it back.`
|
|
8725
8912
|
);
|
|
8726
8913
|
}
|
|
8727
8914
|
};
|
|
@@ -8736,24 +8923,27 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
8736
8923
|
title: "Paint effects",
|
|
8737
8924
|
description: "Provider effects on paint layers and on the effect paper.",
|
|
8738
8925
|
skill: "pixerate-paint-editor",
|
|
8739
|
-
target: { noun: "layer", id: `A pixel layer id from list_layers ("${PAPER_TARGET}" is the effect paper, for list, update and remove).` },
|
|
8926
|
+
target: { noun: "layer", id: `A pixel layer id from list_layers ("${PAPER_TARGET}" is the effect paper and "${PAINT_TARGET}" the effect paint, for list, update and remove).` },
|
|
8740
8927
|
applyUses: ["filter"],
|
|
8741
8928
|
applyDetails: "Light on dried paint: the filter (search_effects with use filter, e.g. a light, glow or grade) runs on the layer's paint at the canvas size and is baked into its pixels; transparent areas stay transparent, and thick paint keeps its relief (an effect with a height input gets it). opacity is the strength (default 1). Wet paint is refused: dry it first (dry_paint). It is one undo step and is not listed afterwards (it is part of the pixels). Generators go on the paper (set_paper_effect), on their own layer (add_effect_layer) or into a layer (fill_layer_with_effect).",
|
|
8742
8929
|
provider: () => controller.effectsProvider,
|
|
8743
8930
|
readOnlyReason,
|
|
8744
8931
|
list: (target) => {
|
|
8745
8932
|
if (target === PAPER_TARGET) return [{ target: PAPER_TARGET, name: "Paper", effects: paperInfo() }];
|
|
8933
|
+
if (target === PAINT_TARGET) return [{ target: PAINT_TARGET, name: "Paint", effects: paintInfo() }];
|
|
8746
8934
|
if (target) {
|
|
8747
8935
|
const l = requireLayer(target);
|
|
8748
8936
|
return [{ target, name: l.name, effects: layerInfo(target) }];
|
|
8749
8937
|
}
|
|
8750
8938
|
const out = [];
|
|
8939
|
+
if (paintInfo().length) out.push({ target: PAINT_TARGET, name: "Paint", effects: paintInfo() });
|
|
8751
8940
|
if (paperInfo().length) out.push({ target: PAPER_TARGET, name: "Paper", effects: paperInfo() });
|
|
8752
8941
|
for (const e of controller.listEffectLayers()) out.push({ target: e.layerId, name: e.name, effects: layerInfo(e.layerId) });
|
|
8753
8942
|
return out;
|
|
8754
8943
|
},
|
|
8755
8944
|
apply: async (target, effect, args) => {
|
|
8756
8945
|
if (target === PAPER_TARGET) throw new Error("The paper takes generator effects through set_paper_effect; apply_effect runs a filter on a layer's paint.");
|
|
8946
|
+
if (target === PAINT_TARGET) throw new Error("Brushes paint with an effect through set_effect_paint; apply_effect runs a filter on a layer's paint.");
|
|
8757
8947
|
noStackArgs(args);
|
|
8758
8948
|
requireLayer(target);
|
|
8759
8949
|
const r = await controller.applyLightEffect(target, effect, { values: values(args.values), amount: args.opacity });
|
|
@@ -8763,6 +8953,13 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
8763
8953
|
update: async (target, instance, changes) => {
|
|
8764
8954
|
instanceCheck(target, instance);
|
|
8765
8955
|
noStackArgs(changes);
|
|
8956
|
+
if (target === PAINT_TARGET) {
|
|
8957
|
+
if (changes.opacity !== void 0) throw new Error("opacity does not apply to the effect paint: change the brush opacity (set_brush tip.opacity).");
|
|
8958
|
+
const p = controller.getEffectPaint();
|
|
8959
|
+
await controller.setEffectPaint({ effect: p.effect, values: { ...p.values, ...changes.values ?? {} }, time: p.time });
|
|
8960
|
+
const effects2 = paintInfo();
|
|
8961
|
+
return { target, effect: effects2[0], effects: effects2 };
|
|
8962
|
+
}
|
|
8766
8963
|
if (target === PAPER_TARGET) {
|
|
8767
8964
|
await controller.updatePaperEffect({ values: changes.values, opacity: changes.opacity });
|
|
8768
8965
|
const effects2 = paperInfo();
|
|
@@ -8776,6 +8973,10 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
8776
8973
|
remove: async (target, instance) => {
|
|
8777
8974
|
instanceCheck(target, instance);
|
|
8778
8975
|
const was = effectsOf(target)[0];
|
|
8976
|
+
if (target === PAINT_TARGET) {
|
|
8977
|
+
controller.setColor(controller.getState().color);
|
|
8978
|
+
return { target, effect: was, effects: [] };
|
|
8979
|
+
}
|
|
8779
8980
|
if (target === PAPER_TARGET) await controller.setPaperEffect(null);
|
|
8780
8981
|
else controller.rasterizeEffectLayer(target);
|
|
8781
8982
|
return { target, effect: was, effects: [], ...target === PAPER_TARGET ? {} : { keptPixels: true } };
|
|
@@ -8811,6 +9012,26 @@ function createPaintEffectsToolProvider(controller, options = {}) {
|
|
|
8811
9012
|
return { paperEffect: await controller.setPaperEffect(args.effect, { values: values(args.values), opacity: args.opacity, blend: args.blend, time: args.time }) };
|
|
8812
9013
|
}
|
|
8813
9014
|
},
|
|
9015
|
+
{
|
|
9016
|
+
name: "set_effect_paint",
|
|
9017
|
+
title: "Paint with an effect",
|
|
9018
|
+
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. Wet, impasto and sticker brushes refuse it (paint the shape and use fill_layer_with_effect with lockTransparency instead); 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).`,
|
|
9019
|
+
inputSchema: {
|
|
9020
|
+
type: "object",
|
|
9021
|
+
properties: {
|
|
9022
|
+
effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
|
|
9023
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
9024
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to paint with, in ms (default 0)." }
|
|
9025
|
+
},
|
|
9026
|
+
required: ["effect"],
|
|
9027
|
+
additionalProperties: false
|
|
9028
|
+
},
|
|
9029
|
+
annotations: { title: "Paint with an effect", ...ctx.annotations.writeIdempotent },
|
|
9030
|
+
handler: async (args) => {
|
|
9031
|
+
ctx.requireProvider();
|
|
9032
|
+
return { effectPaint: await controller.setEffectPaint({ effect: args.effect, values: values(args.values), time: args.time }) };
|
|
9033
|
+
}
|
|
9034
|
+
},
|
|
8814
9035
|
{
|
|
8815
9036
|
name: "add_effect_layer",
|
|
8816
9037
|
title: "Add an effect layer",
|
|
@@ -8960,6 +9181,7 @@ export {
|
|
|
8960
9181
|
PAINT_GRADIENT_SPACE_LABELS,
|
|
8961
9182
|
PAINT_SCHEMA_VERSION,
|
|
8962
9183
|
PAINT_SOURCE_JSON_SCHEMA,
|
|
9184
|
+
PAINT_TARGET,
|
|
8963
9185
|
PAINT_TOOLS,
|
|
8964
9186
|
PAPER_EFFECT_BLENDS,
|
|
8965
9187
|
PAPER_EFFECT_NODE_ID,
|
|
@@ -9052,6 +9274,7 @@ export {
|
|
|
9052
9274
|
drawSticker,
|
|
9053
9275
|
drawWaterOverlay,
|
|
9054
9276
|
effectLayersToJSON,
|
|
9277
|
+
effectPaintKey,
|
|
9055
9278
|
emptiedLayers,
|
|
9056
9279
|
emptyThickChannels,
|
|
9057
9280
|
fitZoom,
|
|
@@ -9068,6 +9291,7 @@ export {
|
|
|
9068
9291
|
invertAffine,
|
|
9069
9292
|
isBlendMode,
|
|
9070
9293
|
isGradientPaint,
|
|
9294
|
+
isPaintEffectPaint,
|
|
9071
9295
|
isPainterlyBlendMode,
|
|
9072
9296
|
isPalmTouch,
|
|
9073
9297
|
isWetBrush,
|
|
@@ -9134,6 +9358,7 @@ export {
|
|
|
9134
9358
|
quickStencil,
|
|
9135
9359
|
rand01,
|
|
9136
9360
|
randSigned,
|
|
9361
|
+
readPaintEffectPaint,
|
|
9137
9362
|
readPaintEffectSource,
|
|
9138
9363
|
readPaintSource,
|
|
9139
9364
|
readPath,
|
|
@@ -9152,6 +9377,8 @@ export {
|
|
|
9152
9377
|
rgbToHexColor,
|
|
9153
9378
|
rgbToHsl,
|
|
9154
9379
|
rgbToHsv,
|
|
9380
|
+
sampleEffectAt,
|
|
9381
|
+
sampleEffectField,
|
|
9155
9382
|
samplePressure,
|
|
9156
9383
|
scalePaintGradient,
|
|
9157
9384
|
scaleStencil,
|
|
@@ -21,6 +21,7 @@ import { CoverageMap } from '../paint/brush.cjs';
|
|
|
21
21
|
import { type PaintGradientSource } from '../paint/gradient-paint.cjs';
|
|
22
22
|
import type { RasterRect } from '../raster/surface.cjs';
|
|
23
23
|
import type { PaintBrush } from './brushes.cjs';
|
|
24
|
+
import { type EffectPaintPixels } from './effect-paint.cjs';
|
|
24
25
|
import { PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine.cjs';
|
|
25
26
|
import { type PaperGrain } from './paper.cjs';
|
|
26
27
|
export interface ProceduralStrokeOptions {
|
|
@@ -30,6 +31,8 @@ export interface ProceduralStrokeOptions {
|
|
|
30
31
|
copies?: readonly StrokeCopy[];
|
|
31
32
|
/** Gradient paint (resolved): each mark takes the colour of the pen dab that made it. */
|
|
32
33
|
paint?: PaintGradientSource | null;
|
|
34
|
+
/** Effect paint: each pixel of a mark takes its colour (and alpha) from these canvas-sized pixels. */
|
|
35
|
+
effect?: EffectPaintPixels | null;
|
|
33
36
|
}
|
|
34
37
|
/** What a coverage-building stroke engine provides to the stroke session. */
|
|
35
38
|
export interface CoverageStroke {
|
|
@@ -49,6 +52,7 @@ declare abstract class MarkStroke implements CoverageStroke {
|
|
|
49
52
|
protected readonly walkers: PenDabWalker[];
|
|
50
53
|
protected readonly depth: number;
|
|
51
54
|
private buf;
|
|
55
|
+
private field;
|
|
52
56
|
private lastRaw;
|
|
53
57
|
private lastFed;
|
|
54
58
|
protected readonly seed: number;
|
|
@@ -21,6 +21,7 @@ import { CoverageMap } from '../paint/brush.js';
|
|
|
21
21
|
import { type PaintGradientSource } from '../paint/gradient-paint.js';
|
|
22
22
|
import type { RasterRect } from '../raster/surface.js';
|
|
23
23
|
import type { PaintBrush } from './brushes.js';
|
|
24
|
+
import { type EffectPaintPixels } from './effect-paint.js';
|
|
24
25
|
import { PenDabWalker, PenSmoother, type PenDab, type PenPoint, type StrokeCopy } from './engine.js';
|
|
25
26
|
import { type PaperGrain } from './paper.js';
|
|
26
27
|
export interface ProceduralStrokeOptions {
|
|
@@ -30,6 +31,8 @@ export interface ProceduralStrokeOptions {
|
|
|
30
31
|
copies?: readonly StrokeCopy[];
|
|
31
32
|
/** Gradient paint (resolved): each mark takes the colour of the pen dab that made it. */
|
|
32
33
|
paint?: PaintGradientSource | null;
|
|
34
|
+
/** Effect paint: each pixel of a mark takes its colour (and alpha) from these canvas-sized pixels. */
|
|
35
|
+
effect?: EffectPaintPixels | null;
|
|
33
36
|
}
|
|
34
37
|
/** What a coverage-building stroke engine provides to the stroke session. */
|
|
35
38
|
export interface CoverageStroke {
|
|
@@ -49,6 +52,7 @@ declare abstract class MarkStroke implements CoverageStroke {
|
|
|
49
52
|
protected readonly walkers: PenDabWalker[];
|
|
50
53
|
protected readonly depth: number;
|
|
51
54
|
private buf;
|
|
55
|
+
private field;
|
|
52
56
|
private lastRaw;
|
|
53
57
|
private lastFed;
|
|
54
58
|
protected readonly seed: number;
|