@pixerate/editor 0.124.1 → 0.126.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/agent/index.cjs +3 -3
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.js +3 -3
- package/dist/animation-editor/index.cjs +422 -85
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +174 -7
- package/dist/animation-editor/index.d.ts +174 -7
- package/dist/animation-editor/index.js +346 -9
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/{chunk-ORHMVUR6.js → chunk-FICSDDDF.js} +5 -3
- package/dist/{chunk-ORHMVUR6.js.map → chunk-FICSDDDF.js.map} +1 -1
- package/dist/chunk-FJ3324TZ.js +59 -0
- package/dist/chunk-FJ3324TZ.js.map +1 -0
- package/dist/{chunk-5EO2IXQE.js → chunk-FPRIUR4T.js} +1 -1
- package/dist/chunk-FPRIUR4T.js.map +1 -0
- package/dist/{chunk-FHYDQOPC.cjs → chunk-JODTP4DP.cjs} +178 -7
- package/dist/chunk-JODTP4DP.cjs.map +1 -0
- package/dist/{chunk-NGYD3AMN.js → chunk-MPQAO33O.js} +2 -2
- package/dist/{chunk-PEHYWUVV.cjs → chunk-O5RVCDEC.cjs} +82 -82
- package/dist/{chunk-PEHYWUVV.cjs.map → chunk-O5RVCDEC.cjs.map} +1 -1
- package/dist/{chunk-JRXJTPES.js → chunk-QFSTQHZY.js} +176 -5
- package/dist/chunk-QFSTQHZY.js.map +1 -0
- package/dist/{chunk-5XXMLBH4.cjs → chunk-QQKTOA4U.cjs} +1 -1
- package/dist/chunk-QQKTOA4U.cjs.map +1 -0
- package/dist/{chunk-4QQZV5Y5.cjs → chunk-SFCPGMCT.cjs} +11 -9
- package/dist/chunk-SFCPGMCT.cjs.map +1 -0
- package/dist/{chunk-WE25VLCO.cjs → chunk-V5YGPRCZ.cjs} +36 -36
- package/dist/{chunk-WE25VLCO.cjs.map → chunk-V5YGPRCZ.cjs.map} +1 -1
- package/dist/chunk-W6QTLHZ3.cjs +59 -0
- package/dist/chunk-W6QTLHZ3.cjs.map +1 -0
- package/dist/{chunk-WATAQ2Z2.js → chunk-WUDPFWAN.js} +2 -2
- package/dist/effects/index.cjs +59 -33
- package/dist/effects/index.cjs.map +1 -1
- package/dist/effects/index.d.cts +4 -5
- package/dist/effects/index.d.ts +4 -5
- package/dist/effects/index.js +33 -7
- package/dist/effects/index.js.map +1 -1
- package/dist/{export-WmYoWmLM.d.cts → export-Bp7ji2r4.d.cts} +1 -2
- package/dist/{export-BdfY_lH0.d.ts → export-C1s4eolZ.d.ts} +1 -1
- package/dist/{export-BGtXgMJ3.d.ts → export-DAyPY94f.d.ts} +1 -2
- package/dist/{export-CbpjwxaI.d.cts → export-DVl4fWy5.d.cts} +1 -1
- package/dist/generation/index.cjs +3 -3
- package/dist/generation/index.cjs.map +1 -1
- package/dist/generation/index.js +3 -3
- package/dist/helpers-BBLlekuM.d.ts +2009 -0
- package/dist/helpers-i95Ob6Vi.d.cts +2009 -0
- package/dist/image-editor/index.cjs +627 -627
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +5 -7
- package/dist/image-editor/index.d.ts +5 -7
- package/dist/image-editor/index.js +13 -13
- package/dist/paint-editor/index.cjs +736 -115
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +248 -13
- package/dist/paint-editor/index.d.ts +248 -13
- package/dist/paint-editor/index.js +634 -13
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/{svg-BDrjRnL1.d.cts → svg-BkRL-fZJ.d.cts} +1 -1
- package/dist/{svg-C-vZ3q50.d.ts → svg-DJKriJ4A.d.ts} +1 -1
- package/dist/tools/index.cjs +2 -2
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/{tools-C7JtLYzo.d.ts → tools-CM8Oe8F3.d.ts} +1 -1
- package/dist/{tools-DHKGHiSK.d.cts → tools-DZCv2kX1.d.cts} +1 -1
- package/dist/vector/index.cjs +1030 -883
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +61 -6
- package/dist/vector/index.d.ts +61 -6
- package/dist/vector/index.js +153 -6
- package/dist/vector/index.js.map +1 -1
- package/dist/video-editor/index.cjs +272 -273
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +4 -6
- package/dist/video-editor/index.d.ts +4 -6
- package/dist/video-editor/index.js +6 -7
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-animation-editor/SKILL.md +24 -1
- package/skills/pixerate-paint-editor/SKILL.md +40 -1
- package/skills/pixerate-vector/SKILL.md +15 -0
- package/dist/chunk-4QQZV5Y5.cjs.map +0 -1
- package/dist/chunk-5EO2IXQE.js.map +0 -1
- package/dist/chunk-5XXMLBH4.cjs.map +0 -1
- package/dist/chunk-CN4PJHUG.cjs +0 -234
- package/dist/chunk-CN4PJHUG.cjs.map +0 -1
- package/dist/chunk-FHYDQOPC.cjs.map +0 -1
- package/dist/chunk-JRXJTPES.js.map +0 -1
- package/dist/chunk-QEG3GRN2.js +0 -234
- package/dist/chunk-QEG3GRN2.js.map +0 -1
- package/dist/flair-CxB3fOcU.d.ts +0 -739
- package/dist/flair-D7BxyBwL.d.cts +0 -739
- package/dist/helpers-777mC6dc.d.cts +0 -253
- package/dist/helpers-DX2_7L5t.d.ts +0 -253
- package/dist/types-BcX7sCsn.d.ts +0 -993
- package/dist/types-CznabG7m.d.cts +0 -993
- /package/dist/{chunk-NGYD3AMN.js.map → chunk-MPQAO33O.js.map} +0 -0
- /package/dist/{chunk-WATAQ2Z2.js.map → chunk-WUDPFWAN.js.map} +0 -0
|
@@ -73,17 +73,18 @@ import {
|
|
|
73
73
|
} from "../chunk-UYQUPUR6.js";
|
|
74
74
|
import {
|
|
75
75
|
toVideoFfmpeg
|
|
76
|
-
} from "../chunk-
|
|
76
|
+
} from "../chunk-FICSDDDF.js";
|
|
77
77
|
import {
|
|
78
78
|
deserializeLayers,
|
|
79
79
|
exportPsd,
|
|
80
80
|
resolveExternalAssets,
|
|
81
81
|
serializeLayers,
|
|
82
82
|
serializeLayersExternal
|
|
83
|
-
} from "../chunk-
|
|
83
|
+
} from "../chunk-WUDPFWAN.js";
|
|
84
84
|
import "../chunk-CTTF4KYV.js";
|
|
85
|
-
import
|
|
86
|
-
|
|
85
|
+
import {
|
|
86
|
+
EffectRendererPool
|
|
87
|
+
} from "../chunk-FJ3324TZ.js";
|
|
87
88
|
import {
|
|
88
89
|
RasterCompositor,
|
|
89
90
|
findLayer,
|
|
@@ -210,9 +211,16 @@ import {
|
|
|
210
211
|
realPresenceClock,
|
|
211
212
|
tweenPresenceCursor
|
|
212
213
|
} from "../chunk-B6DSIEXA.js";
|
|
213
|
-
import "../chunk-
|
|
214
|
+
import "../chunk-FPRIUR4T.js";
|
|
215
|
+
import {
|
|
216
|
+
EffectUnsupportedError,
|
|
217
|
+
EffectsSlot,
|
|
218
|
+
createEffectToolProvider,
|
|
219
|
+
resolveEffectValues
|
|
220
|
+
} from "../chunk-QFSTQHZY.js";
|
|
214
221
|
import "../chunk-FG274FYT.js";
|
|
215
222
|
import "../chunk-GLWZXQ4Q.js";
|
|
223
|
+
import "../chunk-4FX5ACVG.js";
|
|
216
224
|
import "../chunk-33AHNSEB.js";
|
|
217
225
|
import "../chunk-ZRJMJIQU.js";
|
|
218
226
|
import "../chunk-TGD52FEZ.js";
|
|
@@ -1482,6 +1490,7 @@ function varyStroke(stroke, m, v) {
|
|
|
1482
1490
|
// src/paint-editor/render.ts
|
|
1483
1491
|
var PAPER_NODE_ID = "__paper";
|
|
1484
1492
|
var TRACING_NODE_ID = "__tracing";
|
|
1493
|
+
var PAPER_EFFECT_NODE_ID = "__paper_effect";
|
|
1485
1494
|
function paperKey(p, light) {
|
|
1486
1495
|
const metal = p.metallic ?? 0;
|
|
1487
1496
|
return JSON.stringify([p.pattern, p.seed, p.color, p.scale, p.tiling, p.shading, p.dry, metal, metal > 0 && light ? [light.azimuth, light.elevation] : null]);
|
|
@@ -3215,6 +3224,130 @@ ${tl.ffmpeg.command}
|
|
|
3215
3224
|
return writeZip(entries, { compression: "store" });
|
|
3216
3225
|
}
|
|
3217
3226
|
|
|
3227
|
+
// src/paint-editor/effects.ts
|
|
3228
|
+
var PAINT_EFFECT_PLACES = {
|
|
3229
|
+
/** Effect paper: drawn over the whole sheet. */
|
|
3230
|
+
paper: ["generator"],
|
|
3231
|
+
/** An effect layer: drawn at the canvas size. */
|
|
3232
|
+
layer: ["generator", "overlay", "title"],
|
|
3233
|
+
/** An effect fill: composited onto a layer's pixels. */
|
|
3234
|
+
fill: ["generator", "overlay", "title"],
|
|
3235
|
+
/** Light (or any filter) on a layer's dried paint. */
|
|
3236
|
+
light: ["filter"]
|
|
3237
|
+
};
|
|
3238
|
+
var PLACE_WORDS = {
|
|
3239
|
+
paper: "effect paper takes generator effects",
|
|
3240
|
+
layer: "an effect layer takes generator, overlay or title effects",
|
|
3241
|
+
fill: "an effect fill takes generator, overlay or title effects",
|
|
3242
|
+
light: "light on dried paint takes filter effects"
|
|
3243
|
+
};
|
|
3244
|
+
function checkPaintEffectPlace(d, place) {
|
|
3245
|
+
const allowed = PAINT_EFFECT_PLACES[place];
|
|
3246
|
+
if (d.kind !== "motion" && d.kind !== "vector" && d.uses.some((u) => allowed.includes(u))) return;
|
|
3247
|
+
let instead = "";
|
|
3248
|
+
if (d.uses.includes("filter") && place !== "light") instead = "; run it on a layer's dried paint with apply_effect instead";
|
|
3249
|
+
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";
|
|
3250
|
+
else if (place === "paper" && d.uses.some((u) => u === "overlay" || u === "title")) instead = "; add it as an effect layer with add_effect_layer instead";
|
|
3251
|
+
throw new EffectUnsupportedError(`"${d.id}" (${d.name}) is a ${d.uses.join(" / ") || d.kind} effect, and ${PLACE_WORDS[place]}${instead}.`, d.id);
|
|
3252
|
+
}
|
|
3253
|
+
var PAPER_EFFECT_BLENDS = ["multiply", "normal", "overlay", "soft-light", "screen", "color"];
|
|
3254
|
+
var paperVersion = 1;
|
|
3255
|
+
function nextPaperEffectVersion() {
|
|
3256
|
+
return paperVersion++;
|
|
3257
|
+
}
|
|
3258
|
+
function overPixels(base, top, opacity = 1) {
|
|
3259
|
+
const out = new Uint8ClampedArray(base.length);
|
|
3260
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
3261
|
+
const ta = top[i + 3] / 255 * opacity;
|
|
3262
|
+
const ba = base[i + 3] / 255;
|
|
3263
|
+
const a = ta + ba * (1 - ta);
|
|
3264
|
+
out[i + 3] = Math.round(a * 255);
|
|
3265
|
+
if (a > 0) for (let c = 0; c < 3; c++) out[i + c] = (top[i + c] * ta + base[i + c] * ba * (1 - ta)) / a;
|
|
3266
|
+
}
|
|
3267
|
+
return out;
|
|
3268
|
+
}
|
|
3269
|
+
function mixPixels(before, after, amount) {
|
|
3270
|
+
const out = new Uint8ClampedArray(before.length);
|
|
3271
|
+
for (let i = 0; i < out.length; i += 4) {
|
|
3272
|
+
for (let c = 0; c < 3; c++) out[i + c] = before[i + c] + (after[i + c] - before[i + c]) * amount;
|
|
3273
|
+
out[i + 3] = before[i + 3];
|
|
3274
|
+
}
|
|
3275
|
+
return out;
|
|
3276
|
+
}
|
|
3277
|
+
var PaintEffectsRenderer = class {
|
|
3278
|
+
pool = new EffectRendererPool({ maxRenderers: 3 });
|
|
3279
|
+
descriptions = /* @__PURE__ */ new Map();
|
|
3280
|
+
provider = null;
|
|
3281
|
+
/** Forgets cached descriptions and renderers when the provider changes. */
|
|
3282
|
+
follow(provider) {
|
|
3283
|
+
if (provider === this.provider) return;
|
|
3284
|
+
this.provider = provider;
|
|
3285
|
+
this.descriptions.clear();
|
|
3286
|
+
this.pool.clear();
|
|
3287
|
+
}
|
|
3288
|
+
describe(provider, id) {
|
|
3289
|
+
this.follow(provider);
|
|
3290
|
+
let d = this.descriptions.get(id);
|
|
3291
|
+
if (!d) {
|
|
3292
|
+
d = Promise.resolve().then(() => provider.describe(id));
|
|
3293
|
+
d.catch(() => this.descriptions.delete(id));
|
|
3294
|
+
this.descriptions.set(id, d);
|
|
3295
|
+
}
|
|
3296
|
+
return d;
|
|
3297
|
+
}
|
|
3298
|
+
async render(provider, id, size, request) {
|
|
3299
|
+
this.follow(provider);
|
|
3300
|
+
const out = await this.pool.render(provider, id, { ...size, depth: 8 }, request);
|
|
3301
|
+
if (out.width !== size.width || out.height !== size.height || out.data.length !== size.width * size.height * 4) {
|
|
3302
|
+
throw new Error(`The effect "${id}" returned ${out.width}\xD7${out.height} pixels for a ${size.width}\xD7${size.height} canvas.`);
|
|
3303
|
+
}
|
|
3304
|
+
return out;
|
|
3305
|
+
}
|
|
3306
|
+
dispose() {
|
|
3307
|
+
this.pool.clear();
|
|
3308
|
+
this.descriptions.clear();
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
var sourceOut = (s) => ({
|
|
3312
|
+
effect: s.effect,
|
|
3313
|
+
...s.provider ? { provider: s.provider } : {},
|
|
3314
|
+
...s.name ? { name: s.name } : {},
|
|
3315
|
+
values: JSON.parse(JSON.stringify(s.values)),
|
|
3316
|
+
...s.time ? { time: s.time } : {}
|
|
3317
|
+
});
|
|
3318
|
+
function readPaintEffectSource(raw) {
|
|
3319
|
+
if (!raw || typeof raw !== "object") return null;
|
|
3320
|
+
const r = raw;
|
|
3321
|
+
if (typeof r.effect !== "string" || !r.effect || !r.values || typeof r.values !== "object" || Array.isArray(r.values)) return null;
|
|
3322
|
+
return {
|
|
3323
|
+
effect: r.effect,
|
|
3324
|
+
...typeof r.provider === "string" ? { provider: r.provider } : {},
|
|
3325
|
+
...typeof r.name === "string" ? { name: r.name } : {},
|
|
3326
|
+
values: r.values,
|
|
3327
|
+
...typeof r.time === "number" && r.time > 0 ? { time: r.time } : {}
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
function paperEffectToJSON(p) {
|
|
3331
|
+
return { ...sourceOut(p), opacity: p.opacity, blend: p.blend, png: pngDataUrl(p.image.width, p.image.height, p.image.data) };
|
|
3332
|
+
}
|
|
3333
|
+
function paperEffectFromJSON(raw, width, height) {
|
|
3334
|
+
const source = readPaintEffectSource(raw);
|
|
3335
|
+
if (!source) throw new Error("it is not an effect (effect id and values)");
|
|
3336
|
+
const r = raw;
|
|
3337
|
+
const parsed = typeof r.png === "string" ? parseDataUrl(r.png) : null;
|
|
3338
|
+
if (!parsed) throw new Error("its pixels are missing");
|
|
3339
|
+
const image = decodePng(parsed.bytes);
|
|
3340
|
+
if (image.width !== width || image.height !== height) throw new Error(`its pixels are ${image.width}\xD7${image.height}, not the canvas size ${width}\xD7${height}`);
|
|
3341
|
+
const opacity = typeof r.opacity === "number" && r.opacity >= 0 && r.opacity <= 1 ? r.opacity : 1;
|
|
3342
|
+
const blend = PAPER_EFFECT_BLENDS.includes(r.blend) ? r.blend : "multiply";
|
|
3343
|
+
return { ...source, opacity, blend, image: { width, height, data: new Uint8ClampedArray(image.data) }, version: nextPaperEffectVersion() };
|
|
3344
|
+
}
|
|
3345
|
+
function effectLayersToJSON(map, keep) {
|
|
3346
|
+
const out = {};
|
|
3347
|
+
for (const [id, s] of Object.entries(map)) if (keep(id)) out[id] = sourceOut(s);
|
|
3348
|
+
return Object.keys(out).length ? out : void 0;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3218
3351
|
// src/paint-editor/controller.ts
|
|
3219
3352
|
var PaintEditorError = class extends Error {
|
|
3220
3353
|
constructor(message) {
|
|
@@ -3290,6 +3423,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3290
3423
|
liveThick = null;
|
|
3291
3424
|
/** Set while an operation that changes wet paint runs: the simulation before it (for undo). */
|
|
3292
3425
|
wetOp = null;
|
|
3426
|
+
/** The effects provider slot (see `setEffectsProvider`). */
|
|
3427
|
+
effects = new EffectsSlot();
|
|
3428
|
+
effectsRenderer = new PaintEffectsRenderer();
|
|
3429
|
+
paperEffectNodeCache = null;
|
|
3293
3430
|
constructor(options = {}) {
|
|
3294
3431
|
this.historyLimit = Math.max(1, options.historyLimit ?? 200);
|
|
3295
3432
|
this.doc = createPaintDocument({ width: options.width, height: options.height, dpi: options.dpi, paper: this.resolvePaper(options.paper) });
|
|
@@ -3403,6 +3540,21 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3403
3540
|
this.trimWetHistory();
|
|
3404
3541
|
this.emit();
|
|
3405
3542
|
}
|
|
3543
|
+
/**
|
|
3544
|
+
* Commits `next` as a new undo step, or with `commitToHistory` false folds it into the last step
|
|
3545
|
+
* when that step has the same label (the later values of one slider drag).
|
|
3546
|
+
*/
|
|
3547
|
+
commitOrAmend(label, next, commitToHistory) {
|
|
3548
|
+
const last = this.undoStack[this.undoStack.length - 1];
|
|
3549
|
+
if (commitToHistory || !last || last.label !== label || last.wet || this.wetOp) {
|
|
3550
|
+
this.commit(label, next);
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3553
|
+
last.after = next;
|
|
3554
|
+
this.redoStack = [];
|
|
3555
|
+
this.doc = next;
|
|
3556
|
+
this.emit();
|
|
3557
|
+
}
|
|
3406
3558
|
/** Keeps the wet snapshots in history within the wet undo memory, dropping the oldest. */
|
|
3407
3559
|
trimWetHistory() {
|
|
3408
3560
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3811,6 +3963,257 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
3811
3963
|
}
|
|
3812
3964
|
return new TiledSurface(4, tiles, 0);
|
|
3813
3965
|
}
|
|
3966
|
+
// --- Provider effects ------------------------------------------------------------
|
|
3967
|
+
/** The registered effects provider, or `null`. */
|
|
3968
|
+
get effectsProvider() {
|
|
3969
|
+
return this.effects.provider;
|
|
3970
|
+
}
|
|
3971
|
+
/** Registers (or with `null` removes) the effects provider. Not an edit: no undo step. */
|
|
3972
|
+
setEffectsProvider(provider) {
|
|
3973
|
+
this.effects.set(provider);
|
|
3974
|
+
}
|
|
3975
|
+
/** Registers `provider` and returns a function that removes it again (if it is still the one set). */
|
|
3976
|
+
useEffects(provider) {
|
|
3977
|
+
return this.effects.use(provider);
|
|
3978
|
+
}
|
|
3979
|
+
/** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
|
|
3980
|
+
async describeEffect(effect, place) {
|
|
3981
|
+
const d = await this.effectsRenderer.describe(this.effects.require(), effect);
|
|
3982
|
+
if (place) checkPaintEffectPlace(d, place);
|
|
3983
|
+
return d;
|
|
3984
|
+
}
|
|
3985
|
+
/** Every effect layer (top first) with its effect. */
|
|
3986
|
+
listEffectLayers() {
|
|
3987
|
+
const map = this.doc.effectLayers ?? {};
|
|
3988
|
+
return this.listLayers().filter((l) => map[l.id]).map((l) => ({ layerId: l.id, name: l.name, source: map[l.id] }));
|
|
3989
|
+
}
|
|
3990
|
+
/** The effect a layer is drawn from, or `undefined` for other layers. */
|
|
3991
|
+
getEffectLayer(layerId) {
|
|
3992
|
+
return findLayer(this.doc.layers, layerId) ? this.doc.effectLayers?.[layerId] : void 0;
|
|
3993
|
+
}
|
|
3994
|
+
/** The effect paper (without its pixels), or `null`. */
|
|
3995
|
+
getPaperEffect() {
|
|
3996
|
+
const p = this.doc.paperEffect;
|
|
3997
|
+
if (!p) return null;
|
|
3998
|
+
const { image: _i, version: _v, ...rest } = p;
|
|
3999
|
+
return { ...rest, values: { ...rest.values } };
|
|
4000
|
+
}
|
|
4001
|
+
/** Values over `base` (`null` resets a control to its default), checked against the effect's controls. */
|
|
4002
|
+
effectValues(d, base, changes) {
|
|
4003
|
+
const merged = { ...base };
|
|
4004
|
+
const given = {};
|
|
4005
|
+
for (const [k, v] of Object.entries(changes ?? {})) {
|
|
4006
|
+
if (!(k in d.controls.properties)) throw new PaintEditorError(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ") || "none"} (describe_effect).`);
|
|
4007
|
+
if (v === null) merged[k] = d.defaults[k];
|
|
4008
|
+
else given[k] = v;
|
|
4009
|
+
}
|
|
4010
|
+
return { ...merged, ...resolveEffectValues({ ...d, defaults: merged }, given) };
|
|
4011
|
+
}
|
|
4012
|
+
/** Renders a generator, overlay or title effect at the canvas size. */
|
|
4013
|
+
renderEffectCanvas(d, values, time = 0) {
|
|
4014
|
+
const { width, height } = this.doc.canvas;
|
|
4015
|
+
return this.effectsRenderer.render(this.effects.require(), d.id, { width, height }, { time, values });
|
|
4016
|
+
}
|
|
4017
|
+
sourceOf(d, values, time) {
|
|
4018
|
+
return { effect: d.id, provider: this.effects.require().id, name: d.name, values, ...time ? { time } : {} };
|
|
4019
|
+
}
|
|
4020
|
+
checkTime(time) {
|
|
4021
|
+
if (time !== void 0 && !(Number.isFinite(time) && time >= 0)) throw new PaintEditorError(`time ${time} is not a moment of the effect; use 0 or more milliseconds.`);
|
|
4022
|
+
}
|
|
4023
|
+
checkOpacity(opacity) {
|
|
4024
|
+
if (opacity !== void 0 && !(opacity >= 0 && opacity <= 1)) throw new PaintEditorError(`opacity ${opacity} is out of range; use 0..1.`);
|
|
4025
|
+
}
|
|
4026
|
+
/** A pixel layer that takes effect pixels: unlocked, visible, no sticker layer, no wet paint. */
|
|
4027
|
+
effectTarget(layerId, what) {
|
|
4028
|
+
const l = this.paintTarget(layerId, what);
|
|
4029
|
+
if (this.doc.stickers?.[l.id]?.length) throw new PaintEditorError(`Layer "${l.name}" is a sticker layer; ${what} another layer (add_layer).`);
|
|
4030
|
+
if (this.wet.wetLayerIds().includes(l.id)) throw new PaintEditorError(`Layer "${l.name}" holds wet paint; effects go on dried paint, so dry it first (dry_paint) and try again.`);
|
|
4031
|
+
return l;
|
|
4032
|
+
}
|
|
4033
|
+
/** After an effect rendered: the layer must be as it was when the render started. */
|
|
4034
|
+
sameLayerAfterRender(before) {
|
|
4035
|
+
this.requireIdle("applying an effect");
|
|
4036
|
+
const now = this.getLayer(before.id);
|
|
4037
|
+
if (!isPixelLayer(now)) throw new PaintEditorError(`Layer "${before.name}" was removed while the effect rendered.`);
|
|
4038
|
+
if (now.surface !== before.surface || now.offset.x !== before.offset.x || now.offset.y !== before.offset.y) throw new PaintEditorError(`Layer "${before.name}" changed while the effect rendered; apply the effect again.`);
|
|
4039
|
+
return now;
|
|
4040
|
+
}
|
|
4041
|
+
/** The canvas rectangle in a layer's own coordinates. */
|
|
4042
|
+
canvasRectOf(l) {
|
|
4043
|
+
return { x: -l.offset.x, y: -l.offset.y, width: this.doc.canvas.width, height: this.doc.canvas.height };
|
|
4044
|
+
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Sets the effect paper: a provider generator drawn over the paper and under the paint, at the
|
|
4047
|
+
* canvas size (`multiply` by default, so the paper's colour and grain still show). `null` removes
|
|
4048
|
+
* it. One undo step. Returns the effect paper (without pixels), or `null`.
|
|
4049
|
+
*/
|
|
4050
|
+
async setPaperEffect(effect, options = {}) {
|
|
4051
|
+
this.requireIdle("changing the paper");
|
|
4052
|
+
if (effect === null) {
|
|
4053
|
+
if (!this.doc.paperEffect) return null;
|
|
4054
|
+
const { paperEffect: _p, ...rest } = this.doc;
|
|
4055
|
+
this.commit("Remove effect paper", rest);
|
|
4056
|
+
return null;
|
|
4057
|
+
}
|
|
4058
|
+
this.checkOpacity(options.opacity);
|
|
4059
|
+
this.checkTime(options.time);
|
|
4060
|
+
if (options.blend !== void 0 && !PAPER_EFFECT_BLENDS.includes(options.blend)) throw new PaintEditorError(`Unknown effect paper blend "${options.blend}"; use ${PAPER_EFFECT_BLENDS.join(", ")}.`);
|
|
4061
|
+
const d = await this.describeEffect(effect, "paper");
|
|
4062
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4063
|
+
const pixels = await this.renderEffectCanvas(d, values, options.time);
|
|
4064
|
+
this.requireIdle("changing the paper");
|
|
4065
|
+
const was = this.doc.paperEffect;
|
|
4066
|
+
const paperEffect = {
|
|
4067
|
+
...this.sourceOf(d, values, options.time),
|
|
4068
|
+
opacity: options.opacity ?? was?.opacity ?? 1,
|
|
4069
|
+
blend: options.blend ?? was?.blend ?? "multiply",
|
|
4070
|
+
image: { width: pixels.width, height: pixels.height, data: new Uint8ClampedArray(pixels.data) },
|
|
4071
|
+
version: nextPaperEffectVersion()
|
|
4072
|
+
};
|
|
4073
|
+
this.commit("Effect paper", { ...this.doc, paperEffect });
|
|
4074
|
+
return this.getPaperEffect();
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Changes the effect paper: new control `values` (merged over its own; `null` resets one) or
|
|
4078
|
+
* `time` draw it again at the canvas size; `opacity` and `blend` only change how it is drawn.
|
|
4079
|
+
* One undo step; with `commitToHistory` false it joins the previous effect paper step (a slider
|
|
4080
|
+
* drag). Returns the effect paper (without pixels).
|
|
4081
|
+
*/
|
|
4082
|
+
async updatePaperEffect(changes, commitToHistory = true) {
|
|
4083
|
+
this.requireIdle("changing the paper");
|
|
4084
|
+
const was = this.doc.paperEffect;
|
|
4085
|
+
if (!was) throw new PaintEditorError("There is no effect paper; set one with set_paper_effect.");
|
|
4086
|
+
this.checkOpacity(changes.opacity);
|
|
4087
|
+
this.checkTime(changes.time);
|
|
4088
|
+
if (changes.blend !== void 0 && !PAPER_EFFECT_BLENDS.includes(changes.blend)) throw new PaintEditorError(`Unknown effect paper blend "${changes.blend}"; use ${PAPER_EFFECT_BLENDS.join(", ")}.`);
|
|
4089
|
+
let next = { ...was, ...changes.opacity !== void 0 ? { opacity: changes.opacity } : {}, ...changes.blend !== void 0 ? { blend: changes.blend } : {} };
|
|
4090
|
+
const { width, height } = this.doc.canvas;
|
|
4091
|
+
if (changes.values !== void 0 || changes.time !== void 0 || was.image.width !== width || was.image.height !== height) {
|
|
4092
|
+
const d = await this.describeEffect(was.effect, "paper");
|
|
4093
|
+
const values = this.effectValues(d, was.values, changes.values);
|
|
4094
|
+
const time = changes.time ?? was.time ?? 0;
|
|
4095
|
+
const pixels = await this.renderEffectCanvas(d, values, time);
|
|
4096
|
+
this.requireIdle("changing the paper");
|
|
4097
|
+
if (this.doc.paperEffect !== was) throw new PaintEditorError("The effect paper changed while it rendered; change it again.");
|
|
4098
|
+
next = { ...next, ...this.sourceOf(d, values, time), image: { width: pixels.width, height: pixels.height, data: new Uint8ClampedArray(pixels.data) }, version: nextPaperEffectVersion() };
|
|
4099
|
+
if (!time) delete next.time;
|
|
4100
|
+
}
|
|
4101
|
+
this.commitOrAmend("Effect paper", { ...this.doc, paperEffect: next }, commitToHistory);
|
|
4102
|
+
return this.getPaperEffect();
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Adds an effect layer above the active layer (or `aboveId`): a pixel layer drawn by a provider
|
|
4106
|
+
* generator, overlay or title effect at the canvas size. It keeps its effect, so
|
|
4107
|
+
* `updateEffectLayer` draws it again. One undo step. Returns the layer and its effect.
|
|
4108
|
+
*/
|
|
4109
|
+
async addEffectLayer(effect, options = {}) {
|
|
4110
|
+
this.requireIdle("adding a layer");
|
|
4111
|
+
this.checkOpacity(options.opacity);
|
|
4112
|
+
this.checkTime(options.time);
|
|
4113
|
+
if (options.blend !== void 0 && !isBlendMode(options.blend)) throw new PaintEditorError(`Unknown blend mode "${options.blend}". Call get_canvas_info for the blend modes.`);
|
|
4114
|
+
if (options.aboveId !== void 0) this.requireLayer(options.aboveId);
|
|
4115
|
+
const d = await this.describeEffect(effect, "layer");
|
|
4116
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4117
|
+
const pixels = await this.renderEffectCanvas(d, values, options.time);
|
|
4118
|
+
this.requireIdle("adding a layer");
|
|
4119
|
+
const above = options.aboveId ?? this.doc.activeLayerId;
|
|
4120
|
+
const loc = above ? findLayer(this.doc.layers, above) : null;
|
|
4121
|
+
if (options.aboveId !== void 0 && !loc) throw new PaintEditorError(`Layer "${options.aboveId}" was removed while the effect rendered.`);
|
|
4122
|
+
const layer = { ...createPaintLayer(options.name?.trim() || d.name), surface: surfaceFromPixels(pixels, 4), opacity: options.opacity ?? 1, blend: options.blend ?? "normal" };
|
|
4123
|
+
const layers = insertLayer(this.doc.layers, layer, loc?.parent?.id ?? null, loc ? loc.index + 1 : void 0);
|
|
4124
|
+
const source = this.sourceOf(d, values, options.time);
|
|
4125
|
+
this.commit("Add effect layer", { ...this.doc, layers, activeLayerId: layer.id, effectLayers: { ...this.doc.effectLayers ?? {}, [layer.id]: source } });
|
|
4126
|
+
return { layer: this.describeLayer(layer), source };
|
|
4127
|
+
}
|
|
4128
|
+
/**
|
|
4129
|
+
* Draws an effect layer again with new `values` (merged over its own; `null` resets one) or
|
|
4130
|
+
* `time`, at the canvas size, replacing its pixels (paint on it is lost). One undo step; with
|
|
4131
|
+
* `commitToHistory` false it joins the previous effect layer step (a slider drag).
|
|
4132
|
+
*/
|
|
4133
|
+
async updateEffectLayer(layerId, changes, commitToHistory = true) {
|
|
4134
|
+
this.requireIdle("changing an effect layer");
|
|
4135
|
+
const was = this.getEffectLayer(layerId);
|
|
4136
|
+
if (!was) throw new PaintEditorError(`"${layerId}" is not an effect layer; add one with add_effect_layer.`);
|
|
4137
|
+
this.checkTime(changes.time);
|
|
4138
|
+
const l = this.effectTarget(layerId, "draw the effect on");
|
|
4139
|
+
const d = await this.describeEffect(was.effect, "layer");
|
|
4140
|
+
const values = this.effectValues(d, was.values, changes.values);
|
|
4141
|
+
const time = changes.time ?? was.time ?? 0;
|
|
4142
|
+
const pixels = await this.renderEffectCanvas(d, values, time);
|
|
4143
|
+
const now = this.getLayer(l.id);
|
|
4144
|
+
this.requireIdle("changing an effect layer");
|
|
4145
|
+
if (!isPixelLayer(now)) throw new PaintEditorError(`Layer "${l.name}" was removed while the effect rendered.`);
|
|
4146
|
+
const source = this.sourceOf(d, values, time);
|
|
4147
|
+
this.commitOrAmend("Effect layer", { ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...now, surface: surfaceFromPixels(pixels, 4), offset: { x: 0, y: 0 } })), effectLayers: { ...this.doc.effectLayers ?? {}, [l.id]: source } }, commitToHistory);
|
|
4148
|
+
return source;
|
|
4149
|
+
}
|
|
4150
|
+
/** Turns an effect layer into a plain pixel layer, keeping its pixels (one undo step). */
|
|
4151
|
+
rasterizeEffectLayer(layerId) {
|
|
4152
|
+
this.requireIdle("changing an effect layer");
|
|
4153
|
+
if (!this.getEffectLayer(layerId)) throw new PaintEditorError(`"${layerId}" is not an effect layer; list_applied_effects lists them.`);
|
|
4154
|
+
const { [layerId]: _gone, ...rest } = this.doc.effectLayers ?? {};
|
|
4155
|
+
this.commit("Rasterize effect layer", { ...this.doc, effectLayers: rest });
|
|
4156
|
+
return this.describeLayer(this.requireLayer(layerId));
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Fills a layer with a provider generator, overlay or title effect: its pixels, rendered at the
|
|
4160
|
+
* canvas size, are composited onto the layer's paint at `opacity` (only where it is already
|
|
4161
|
+
* painted with `lockTransparency`, or when the layer's transparency is locked). One undo step.
|
|
4162
|
+
*/
|
|
4163
|
+
async fillLayerWithEffect(effect, options = {}) {
|
|
4164
|
+
this.requireIdle("filling");
|
|
4165
|
+
this.checkOpacity(options.opacity);
|
|
4166
|
+
this.checkTime(options.time);
|
|
4167
|
+
const l = this.effectTarget(options.layerId, "fill");
|
|
4168
|
+
const d = await this.describeEffect(effect, "fill");
|
|
4169
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4170
|
+
const pixels = await this.renderEffectCanvas(d, values, options.time);
|
|
4171
|
+
const now = this.sameLayerAfterRender(l);
|
|
4172
|
+
const rect = this.canvasRectOf(now);
|
|
4173
|
+
const before = readRegion(now.surface, rect);
|
|
4174
|
+
let after = overPixels(before, pixels.data, options.opacity ?? 1);
|
|
4175
|
+
if (options.lockTransparency || isLayerLocked(now, "transparency")) after = mixPixels(before, after, 1);
|
|
4176
|
+
const surface = writeRegion(now.surface, rect, after);
|
|
4177
|
+
this.commit("Effect fill", { ...this.doc, layers: mapLayer(this.doc.layers, now.id, () => ({ ...now, surface })) });
|
|
4178
|
+
return { layerId: now.id, source: this.sourceOf(d, values, options.time) };
|
|
4179
|
+
}
|
|
4180
|
+
/**
|
|
4181
|
+
* Runs a filter effect (light, glow, colour grade …) on a layer's dried paint and bakes it in, at
|
|
4182
|
+
* `amount` strength (0..1, default 1). Transparent areas stay transparent; thick paint keeps its
|
|
4183
|
+
* relief, and an effect with a `height` input gets the relief as a height map. Wet paint is
|
|
4184
|
+
* refused (dry it first). One undo step.
|
|
4185
|
+
*/
|
|
4186
|
+
async applyLightEffect(layerId, effect, options = {}) {
|
|
4187
|
+
this.requireIdle("applying an effect");
|
|
4188
|
+
const amount = options.amount ?? 1;
|
|
4189
|
+
if (!(amount >= 0 && amount <= 1)) throw new PaintEditorError(`amount ${options.amount} is out of range; use 0..1.`);
|
|
4190
|
+
this.checkTime(options.time);
|
|
4191
|
+
const l = this.effectTarget(layerId, "light");
|
|
4192
|
+
if (!contentBounds(l.surface)) throw new PaintEditorError(`Layer "${l.name}" has no paint to light; paint on it first, or add an effect layer (add_effect_layer).`);
|
|
4193
|
+
const d = await this.describeEffect(effect, "light");
|
|
4194
|
+
const values = this.effectValues(d, { ...d.defaults }, options.values);
|
|
4195
|
+
const rect = this.canvasRectOf(l);
|
|
4196
|
+
const before = readRegion(l.surface, rect);
|
|
4197
|
+
const inputs = { source: { width: rect.width, height: rect.height, data: before } };
|
|
4198
|
+
const heightInput = d.inputs.find((i) => i.kind === "image" && /^(height|depth|relief)$/i.test(i.name));
|
|
4199
|
+
const relief = this.doc.thick?.[l.id];
|
|
4200
|
+
if (heightInput && relief) {
|
|
4201
|
+
const h = readRegion(relief.height, rect);
|
|
4202
|
+
const rgba = new Uint8ClampedArray(rect.width * rect.height * 4);
|
|
4203
|
+
for (let i = 0; i < h.length; i++) rgba.set([h[i], h[i], h[i], 255], i * 4);
|
|
4204
|
+
inputs[heightInput.name] = { width: rect.width, height: rect.height, data: rgba };
|
|
4205
|
+
}
|
|
4206
|
+
const sourceInput = d.inputs.find((i) => i.kind === "image" && i.name !== heightInput?.name)?.name ?? "source";
|
|
4207
|
+
if (sourceInput !== "source") {
|
|
4208
|
+
inputs[sourceInput] = inputs.source;
|
|
4209
|
+
delete inputs.source;
|
|
4210
|
+
}
|
|
4211
|
+
const out = await this.effectsRenderer.render(this.effects.require(), d.id, { width: rect.width, height: rect.height }, { time: options.time ?? 0, values, inputs });
|
|
4212
|
+
const now = this.sameLayerAfterRender(l);
|
|
4213
|
+
const surface = writeRegion(now.surface, rect, mixPixels(before, out.data, amount));
|
|
4214
|
+
this.commit("Light effect", { ...this.doc, layers: mapLayer(this.doc.layers, now.id, () => ({ ...now, surface })) });
|
|
4215
|
+
return { layerId: now.id, source: this.sourceOf(d, values, options.time), amount };
|
|
4216
|
+
}
|
|
3814
4217
|
// --- Brushes -------------------------------------------------------------------
|
|
3815
4218
|
findBrush(id) {
|
|
3816
4219
|
return BUILTIN_BRUSHES.find((b) => b.id === id) ?? this.userBrushes.find((b) => b.id === id);
|
|
@@ -4463,6 +4866,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4463
4866
|
/** Stops the wet simulation's worker threads (call when the editor goes away). */
|
|
4464
4867
|
dispose() {
|
|
4465
4868
|
this.wet.destroy();
|
|
4869
|
+
this.effectsRenderer.dispose();
|
|
4466
4870
|
}
|
|
4467
4871
|
/** Resolves when wet paint is between steps (edits, undo and exports can run). */
|
|
4468
4872
|
wetIdle() {
|
|
@@ -4650,6 +5054,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4650
5054
|
if (!t || !t.visible) return [];
|
|
4651
5055
|
return [this.tracingNode(t)];
|
|
4652
5056
|
}
|
|
5057
|
+
/** The effect paper as a compositor layer (one object per effect paper, so caches hold). */
|
|
5058
|
+
paperEffectNode(p) {
|
|
5059
|
+
const c = this.paperEffectNodeCache;
|
|
5060
|
+
if (c && c.image === p.image && c.opacity === p.opacity && c.blend === p.blend) return c;
|
|
5061
|
+
const node = { type: "image", id: PAPER_EFFECT_NODE_ID, name: "Effect paper", visible: true, opacity: p.opacity, blend: p.blend, image: p.image, version: p.version };
|
|
5062
|
+
this.paperEffectNodeCache = node;
|
|
5063
|
+
return node;
|
|
5064
|
+
}
|
|
4653
5065
|
/** The tracing image resampled onto the canvas (cached until it or the canvas changes). */
|
|
4654
5066
|
tracingNode(t) {
|
|
4655
5067
|
const { width, height } = this.doc.canvas;
|
|
@@ -4699,6 +5111,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4699
5111
|
layers = picked.map((l) => ({ ...l, visible: true }));
|
|
4700
5112
|
}
|
|
4701
5113
|
const under = options.paper === false ? [] : [paperNode(this.doc.paper, this.lighting)];
|
|
5114
|
+
if (options.paper !== false && this.doc.paperEffect) under.push(this.paperEffectNode(this.doc.paperEffect));
|
|
4702
5115
|
if (view) under.push(...this.underlayLayers());
|
|
4703
5116
|
return [...under, ...layers];
|
|
4704
5117
|
}
|
|
@@ -4773,6 +5186,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4773
5186
|
for (const [id, list] of Object.entries(this.doc.stickers ?? {})) if (list.length && findLayer(this.doc.layers, id)) stickers[id] = list.map((x) => ({ ...x }));
|
|
4774
5187
|
if (Object.keys(stickers).length) ext.stickers = stickers;
|
|
4775
5188
|
else delete ext.stickers;
|
|
5189
|
+
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
5190
|
+
if (effectLayers) ext.effectLayers = effectLayers;
|
|
5191
|
+
else delete ext.effectLayers;
|
|
5192
|
+
if (this.doc.paperEffect) ext.paperEffect = paperEffectToJSON(this.doc.paperEffect);
|
|
5193
|
+
else delete ext.paperEffect;
|
|
4776
5194
|
if (this.lighting.azimuth !== DEFAULT_LIGHTING.azimuth || this.lighting.elevation !== DEFAULT_LIGHTING.elevation || this.lighting.relief !== DEFAULT_LIGHTING.relief) ext.lighting = { ...this.lighting };
|
|
4777
5195
|
else delete ext.lighting;
|
|
4778
5196
|
return ext;
|
|
@@ -4791,6 +5209,23 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4791
5209
|
}
|
|
4792
5210
|
if (Object.keys(stickers).length) state = { ...state, stickers };
|
|
4793
5211
|
}
|
|
5212
|
+
if (ext.effectLayers && typeof ext.effectLayers === "object") {
|
|
5213
|
+
const effectLayers = {};
|
|
5214
|
+
for (const [id, v] of Object.entries(ext.effectLayers)) {
|
|
5215
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
5216
|
+
const source = readPaintEffectSource(v);
|
|
5217
|
+
if (source) effectLayers[id] = source;
|
|
5218
|
+
else problems.push(`The effect of layer "${id}" could not be read; it loads as a plain layer.`);
|
|
5219
|
+
}
|
|
5220
|
+
if (Object.keys(effectLayers).length) state = { ...state, effectLayers };
|
|
5221
|
+
}
|
|
5222
|
+
if (ext.paperEffect !== void 0) {
|
|
5223
|
+
try {
|
|
5224
|
+
state = { ...state, paperEffect: paperEffectFromJSON(ext.paperEffect, state.canvas.width, state.canvas.height) };
|
|
5225
|
+
} catch (e) {
|
|
5226
|
+
problems.push(`The effect paper could not be read (${e.message}); the plain paper shows.`);
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
4794
5229
|
const raw = ext.layerChannels;
|
|
4795
5230
|
if (!raw || typeof raw !== "object") return state;
|
|
4796
5231
|
const thick = {};
|
|
@@ -4827,6 +5262,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
4827
5262
|
delete this.extensions.layerChannels;
|
|
4828
5263
|
delete this.extensions.lighting;
|
|
4829
5264
|
delete this.extensions.stickers;
|
|
5265
|
+
delete this.extensions.effectLayers;
|
|
5266
|
+
delete this.extensions.paperEffect;
|
|
4830
5267
|
this.viewCompositor.clear();
|
|
4831
5268
|
this.emit();
|
|
4832
5269
|
return loaded.problems;
|
|
@@ -5024,7 +5461,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5024
5461
|
/** Adds a floating reference image to look at and pick colours from. */
|
|
5025
5462
|
addReferenceImage(options) {
|
|
5026
5463
|
const { src, pixels } = this.importImage(options, "The reference image");
|
|
5027
|
-
const
|
|
5464
|
+
const ref2 = {
|
|
5028
5465
|
id: `ref${this.nextReferenceId++}`,
|
|
5029
5466
|
name: options.name?.trim() || `Reference ${this.guides.references.length + 1}`,
|
|
5030
5467
|
src,
|
|
@@ -5035,8 +5472,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5035
5472
|
displayWidth: options.displayWidth ?? Math.min(240, pixels.width),
|
|
5036
5473
|
visible: true
|
|
5037
5474
|
};
|
|
5038
|
-
this.setGuides({ ...this.guides, references: [...this.guides.references,
|
|
5039
|
-
return
|
|
5475
|
+
this.setGuides({ ...this.guides, references: [...this.guides.references, ref2] });
|
|
5476
|
+
return ref2;
|
|
5040
5477
|
}
|
|
5041
5478
|
requireReference(id) {
|
|
5042
5479
|
const r = this.guides.references.find((x) => x.id === id);
|
|
@@ -5100,12 +5537,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5100
5537
|
}
|
|
5101
5538
|
}
|
|
5102
5539
|
if (Array.isArray(r.references)) {
|
|
5103
|
-
for (const
|
|
5104
|
-
if (!
|
|
5540
|
+
for (const ref2 of r.references) {
|
|
5541
|
+
if (!ref2 || typeof ref2.src !== "string" || typeof ref2.id !== "string") continue;
|
|
5105
5542
|
try {
|
|
5106
|
-
const px = this.decodeImage(
|
|
5107
|
-
guides.references.push({ id:
|
|
5108
|
-
const n = Number(/^ref(\d+)$/.exec(
|
|
5543
|
+
const px = this.decodeImage(ref2.src, `Reference "${ref2.name}"`);
|
|
5544
|
+
guides.references.push({ id: ref2.id, name: String(ref2.name ?? ref2.id), src: ref2.src, width: px.width, height: px.height, x: Number(ref2.x) || 0, y: Number(ref2.y) || 0, displayWidth: Number(ref2.displayWidth) || 240, visible: ref2.visible !== false });
|
|
5545
|
+
const n = Number(/^ref(\d+)$/.exec(ref2.id)?.[1] ?? 0);
|
|
5109
5546
|
if (n >= this.nextReferenceId) this.nextReferenceId = n + 1;
|
|
5110
5547
|
} catch (e) {
|
|
5111
5548
|
problems.push(e.message);
|
|
@@ -7937,6 +8374,176 @@ function quickStencil(kind, canvas) {
|
|
|
7937
8374
|
return { kind: "line", from: { x: w / 2, y: 0 }, to: { x: w / 2, y: h } };
|
|
7938
8375
|
}
|
|
7939
8376
|
}
|
|
8377
|
+
|
|
8378
|
+
// src/paint-editor/effect-tools.ts
|
|
8379
|
+
var PAPER_TARGET = "paper";
|
|
8380
|
+
var ref = (s) => ({ effect: s.effect, ...s.name ? { name: s.name } : {}, ...s.provider ? { provider: s.provider } : {} });
|
|
8381
|
+
function createPaintEffectsToolProvider(controller, options = {}) {
|
|
8382
|
+
const readOnlyReason = () => {
|
|
8383
|
+
const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
|
|
8384
|
+
return ro ? typeof ro === "string" ? ro : "The painting is read-only." : null;
|
|
8385
|
+
};
|
|
8386
|
+
const paperInfo = () => {
|
|
8387
|
+
const p = controller.getPaperEffect();
|
|
8388
|
+
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 } : {} }] : [];
|
|
8389
|
+
};
|
|
8390
|
+
const layerInfo = (layerId) => {
|
|
8391
|
+
const s = controller.getEffectLayer(layerId);
|
|
8392
|
+
return s ? [{ id: "effect", ...ref(s), role: "effect layer", enabled: true, values: s.values, ...s.time ? { time: s.time } : {} }] : [];
|
|
8393
|
+
};
|
|
8394
|
+
const requireLayer = (id) => {
|
|
8395
|
+
const l = controller.getLayer(id);
|
|
8396
|
+
if (!l) {
|
|
8397
|
+
const ids = controller.listLayers().map((x) => `"${x.id}"`);
|
|
8398
|
+
throw new Error(`There is no layer "${id}"; the targets are "${PAPER_TARGET}" and the layers ${ids.join(", ") || "(none)"} (list_layers lists them).`);
|
|
8399
|
+
}
|
|
8400
|
+
return l;
|
|
8401
|
+
};
|
|
8402
|
+
const effectsOf = (target) => target === PAPER_TARGET ? paperInfo() : layerInfo(target);
|
|
8403
|
+
const instanceCheck = (target, instance) => {
|
|
8404
|
+
const want = target === PAPER_TARGET ? PAPER_TARGET : "effect";
|
|
8405
|
+
if (target !== PAPER_TARGET) requireLayer(target);
|
|
8406
|
+
if (instance !== want || !effectsOf(target).length) {
|
|
8407
|
+
const have = effectsOf(target).map((e) => `"${e.id}"`);
|
|
8408
|
+
throw new Error(
|
|
8409
|
+
`${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.`
|
|
8410
|
+
);
|
|
8411
|
+
}
|
|
8412
|
+
};
|
|
8413
|
+
const noStackArgs = (changes) => {
|
|
8414
|
+
if (changes.index !== void 0) throw new Error("index does not apply: the paper and each effect layer have one effect.");
|
|
8415
|
+
if (changes.enabled !== void 0) throw new Error("enabled does not apply here: hide an effect layer with update_layer visible false, or remove the effect paper.");
|
|
8416
|
+
};
|
|
8417
|
+
const values = (v) => v;
|
|
8418
|
+
return createEffectToolProvider({
|
|
8419
|
+
id: options.id ?? "paint-effects",
|
|
8420
|
+
namespace: options.namespace,
|
|
8421
|
+
target: { noun: "layer", id: `A pixel layer id from list_layers ("${PAPER_TARGET}" is the effect paper, for list, update and remove).` },
|
|
8422
|
+
applyUses: ["filter"],
|
|
8423
|
+
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).",
|
|
8424
|
+
provider: () => controller.effectsProvider,
|
|
8425
|
+
readOnlyReason,
|
|
8426
|
+
list: (target) => {
|
|
8427
|
+
if (target === PAPER_TARGET) return [{ target: PAPER_TARGET, name: "Paper", effects: paperInfo() }];
|
|
8428
|
+
if (target) {
|
|
8429
|
+
const l = requireLayer(target);
|
|
8430
|
+
return [{ target, name: l.name, effects: layerInfo(target) }];
|
|
8431
|
+
}
|
|
8432
|
+
const out = [];
|
|
8433
|
+
if (paperInfo().length) out.push({ target: PAPER_TARGET, name: "Paper", effects: paperInfo() });
|
|
8434
|
+
for (const e of controller.listEffectLayers()) out.push({ target: e.layerId, name: e.name, effects: layerInfo(e.layerId) });
|
|
8435
|
+
return out;
|
|
8436
|
+
},
|
|
8437
|
+
apply: async (target, effect, args) => {
|
|
8438
|
+
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.");
|
|
8439
|
+
noStackArgs(args);
|
|
8440
|
+
requireLayer(target);
|
|
8441
|
+
const r = await controller.applyLightEffect(target, effect, { values: values(args.values), amount: args.opacity });
|
|
8442
|
+
const applied = { id: "light", ...ref(r.source), role: "baked light", enabled: true, opacity: r.amount, values: r.source.values };
|
|
8443
|
+
return { target, effect: applied, effects: layerInfo(target), baked: true };
|
|
8444
|
+
},
|
|
8445
|
+
update: async (target, instance, changes) => {
|
|
8446
|
+
instanceCheck(target, instance);
|
|
8447
|
+
noStackArgs(changes);
|
|
8448
|
+
if (target === PAPER_TARGET) {
|
|
8449
|
+
await controller.updatePaperEffect({ values: changes.values, opacity: changes.opacity });
|
|
8450
|
+
const effects2 = paperInfo();
|
|
8451
|
+
return { target, effect: effects2[0], effects: effects2 };
|
|
8452
|
+
}
|
|
8453
|
+
if (changes.opacity !== void 0) throw new Error("An effect layer's opacity is the layer's: change it with update_layer.");
|
|
8454
|
+
await controller.updateEffectLayer(target, { values: changes.values });
|
|
8455
|
+
const effects = layerInfo(target);
|
|
8456
|
+
return { target, effect: effects[0], effects };
|
|
8457
|
+
},
|
|
8458
|
+
remove: async (target, instance) => {
|
|
8459
|
+
instanceCheck(target, instance);
|
|
8460
|
+
const was = effectsOf(target)[0];
|
|
8461
|
+
if (target === PAPER_TARGET) await controller.setPaperEffect(null);
|
|
8462
|
+
else controller.rasterizeEffectLayer(target);
|
|
8463
|
+
return { target, effect: was, effects: [], ...target === PAPER_TARGET ? {} : { keptPixels: true } };
|
|
8464
|
+
},
|
|
8465
|
+
subscribe: (notify) => {
|
|
8466
|
+
const a = controller.subscribe(() => notify());
|
|
8467
|
+
const b = controller.effects.subscribe(() => notify());
|
|
8468
|
+
return () => {
|
|
8469
|
+
a();
|
|
8470
|
+
b();
|
|
8471
|
+
};
|
|
8472
|
+
},
|
|
8473
|
+
extraTools: (ctx) => [
|
|
8474
|
+
{
|
|
8475
|
+
name: "set_paper_effect",
|
|
8476
|
+
title: "Set the effect paper",
|
|
8477
|
+
description: `Draws a provider generator (search_effects with use generator) over the paper and under the paint, at the canvas size: multiplied by default, so the paper's colour and grain still show. Replaces any effect paper; it is saved and exported with the painting. effect null removes it. One undo step. Returns the effect paper (update_effect / remove_effect with target and effect "paper" change it later).`,
|
|
8478
|
+
inputSchema: {
|
|
8479
|
+
type: "object",
|
|
8480
|
+
properties: {
|
|
8481
|
+
effect: { anyOf: [ctx.effectIdSchema(["generator"]), { type: "null" }], description: "A generator effect id from search_effects, or null to remove the effect paper." },
|
|
8482
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
8483
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Strength 0\u20131 (default 1, or the current effect paper's)." },
|
|
8484
|
+
blend: { type: "string", enum: [...PAPER_EFFECT_BLENDS], description: "How it draws over the paper (default multiply, which keeps the paper's grain; normal covers it)." },
|
|
8485
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." }
|
|
8486
|
+
},
|
|
8487
|
+
required: ["effect"],
|
|
8488
|
+
additionalProperties: false
|
|
8489
|
+
},
|
|
8490
|
+
annotations: { title: "Set the effect paper", ...ctx.annotations.writeIdempotent },
|
|
8491
|
+
handler: async (args) => {
|
|
8492
|
+
if (args.effect !== null) ctx.requireProvider();
|
|
8493
|
+
return { paperEffect: await controller.setPaperEffect(args.effect, { values: values(args.values), opacity: args.opacity, blend: args.blend, time: args.time }) };
|
|
8494
|
+
}
|
|
8495
|
+
},
|
|
8496
|
+
{
|
|
8497
|
+
name: "add_effect_layer",
|
|
8498
|
+
title: "Add an effect layer",
|
|
8499
|
+
description: 'Adds a pixel layer drawn by a provider generator, overlay or title effect (search_effects with use generator, overlay or title) at the canvas size, above the active layer unless aboveId is given, and makes it active. Paint on it like any layer; update_effect (effect "effect") draws it again with new values, replacing its pixels; remove_effect keeps its pixels as a plain layer. One undo step. Returns the new layer and its effect.',
|
|
8500
|
+
inputSchema: {
|
|
8501
|
+
type: "object",
|
|
8502
|
+
properties: {
|
|
8503
|
+
effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
|
|
8504
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
8505
|
+
name: { type: "string", minLength: 1, description: "The layer name (default: the effect's name)." },
|
|
8506
|
+
aboveId: { type: "string", minLength: 1, description: "Put it directly above this layer (default: above the active layer, else on top)." },
|
|
8507
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131 (default 1)." },
|
|
8508
|
+
blend: { type: "string", enum: BLEND_MODES.map((m) => m.id), description: "Layer blend mode (default normal); screen or linear-dodge for light, multiply for textures." },
|
|
8509
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." }
|
|
8510
|
+
},
|
|
8511
|
+
required: ["effect"],
|
|
8512
|
+
additionalProperties: false
|
|
8513
|
+
},
|
|
8514
|
+
annotations: { title: "Add an effect layer", ...ctx.annotations.write },
|
|
8515
|
+
handler: async (args) => {
|
|
8516
|
+
ctx.requireProvider();
|
|
8517
|
+
const r = await controller.addEffectLayer(args.effect, { values: values(args.values), name: args.name, aboveId: args.aboveId, opacity: args.opacity, blend: args.blend, time: args.time });
|
|
8518
|
+
return { layer: r.layer, effects: layerInfo(r.layer.id) };
|
|
8519
|
+
}
|
|
8520
|
+
},
|
|
8521
|
+
{
|
|
8522
|
+
name: "fill_layer_with_effect",
|
|
8523
|
+
title: "Fill a layer with an effect",
|
|
8524
|
+
description: "Composites a provider generator, overlay or title effect (rendered at the canvas size) onto a layer's pixels at opacity, as a fill: the layer keeps no link to the effect. lockTransparency (or a transparency-locked layer) only colours what is already painted. Wet paint is refused: dry it first (dry_paint). One undo step. Returns the layer id and the effect used.",
|
|
8525
|
+
inputSchema: {
|
|
8526
|
+
type: "object",
|
|
8527
|
+
properties: {
|
|
8528
|
+
effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
|
|
8529
|
+
layerId: { type: "string", minLength: 1, description: "The pixel layer to fill (default: the active layer)." },
|
|
8530
|
+
values: ctx.valuesSchema("Control values over the effect's defaults"),
|
|
8531
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Fill strength 0\u20131 (default 1)." },
|
|
8532
|
+
lockTransparency: { type: "boolean", description: "Only colour pixels that are already painted (default false)." },
|
|
8533
|
+
time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." }
|
|
8534
|
+
},
|
|
8535
|
+
required: ["effect"],
|
|
8536
|
+
additionalProperties: false
|
|
8537
|
+
},
|
|
8538
|
+
annotations: { title: "Fill a layer with an effect", ...ctx.annotations.write },
|
|
8539
|
+
handler: async (args) => {
|
|
8540
|
+
ctx.requireProvider();
|
|
8541
|
+
return controller.fillLayerWithEffect(args.effect, { layerId: args.layerId, values: values(args.values), opacity: args.opacity, lockTransparency: args.lockTransparency, time: args.time });
|
|
8542
|
+
}
|
|
8543
|
+
}
|
|
8544
|
+
]
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
7940
8547
|
export {
|
|
7941
8548
|
BLEND_MENU_GROUPS,
|
|
7942
8549
|
BLEND_MODES,
|
|
@@ -7990,6 +8597,7 @@ export {
|
|
|
7990
8597
|
PAINT_BRUSH_CATEGORIES,
|
|
7991
8598
|
PAINT_BRUSH_ENGINES,
|
|
7992
8599
|
PAINT_COLOR_JSON_SCHEMA,
|
|
8600
|
+
PAINT_EFFECT_PLACES,
|
|
7993
8601
|
PAINT_FORMAT,
|
|
7994
8602
|
PAINT_GRADIENT_FULL_SPEED,
|
|
7995
8603
|
PAINT_GRADIENT_JSON_SCHEMA,
|
|
@@ -8004,10 +8612,13 @@ export {
|
|
|
8004
8612
|
PAINT_SCHEMA_VERSION,
|
|
8005
8613
|
PAINT_SOURCE_JSON_SCHEMA,
|
|
8006
8614
|
PAINT_TOOLS,
|
|
8615
|
+
PAPER_EFFECT_BLENDS,
|
|
8616
|
+
PAPER_EFFECT_NODE_ID,
|
|
8007
8617
|
PAPER_FAMILIES,
|
|
8008
8618
|
PAPER_LIBRARY,
|
|
8009
8619
|
PAPER_NODE_ID,
|
|
8010
8620
|
PAPER_SLIDERS,
|
|
8621
|
+
PAPER_TARGET,
|
|
8011
8622
|
PARTICLE_KINDS,
|
|
8012
8623
|
PARTICLE_KIND_LABELS,
|
|
8013
8624
|
PARTICLE_RENDERS,
|
|
@@ -8018,6 +8629,7 @@ export {
|
|
|
8018
8629
|
PaintDocumentError,
|
|
8019
8630
|
PaintEditorController,
|
|
8020
8631
|
PaintEditorError,
|
|
8632
|
+
PaintEffectsRenderer,
|
|
8021
8633
|
PaintGesture,
|
|
8022
8634
|
PaintGradientSampler,
|
|
8023
8635
|
PaintPresence,
|
|
@@ -8064,6 +8676,7 @@ export {
|
|
|
8064
8676
|
brushTakesPaint,
|
|
8065
8677
|
brushToJSON,
|
|
8066
8678
|
buildTimeLapse,
|
|
8679
|
+
checkPaintEffectPlace,
|
|
8067
8680
|
clonePaper,
|
|
8068
8681
|
colorHarmony,
|
|
8069
8682
|
composeAffine,
|
|
@@ -8071,6 +8684,7 @@ export {
|
|
|
8071
8684
|
createPaintCompositor,
|
|
8072
8685
|
createPaintDocument,
|
|
8073
8686
|
createPaintEditorToolProvider,
|
|
8687
|
+
createPaintEffectsToolProvider,
|
|
8074
8688
|
createPaintLayer,
|
|
8075
8689
|
createPaintReplayTools,
|
|
8076
8690
|
createPaintStickerTools,
|
|
@@ -8087,6 +8701,7 @@ export {
|
|
|
8087
8701
|
downsampleRgba,
|
|
8088
8702
|
drawSticker,
|
|
8089
8703
|
drawWaterOverlay,
|
|
8704
|
+
effectLayersToJSON,
|
|
8090
8705
|
emptiedLayers,
|
|
8091
8706
|
emptyThickChannels,
|
|
8092
8707
|
fitZoom,
|
|
@@ -8111,6 +8726,7 @@ export {
|
|
|
8111
8726
|
lightingKey,
|
|
8112
8727
|
mapPenPoint,
|
|
8113
8728
|
mix32,
|
|
8729
|
+
mixPixels,
|
|
8114
8730
|
mixerLoadForStroke,
|
|
8115
8731
|
mixerTip,
|
|
8116
8732
|
multiColorForDab,
|
|
@@ -8119,6 +8735,7 @@ export {
|
|
|
8119
8735
|
multiColorSliders,
|
|
8120
8736
|
multiColorToPaint,
|
|
8121
8737
|
newPaintLayerId,
|
|
8738
|
+
nextPaperEffectVersion,
|
|
8122
8739
|
normalizeBehavior,
|
|
8123
8740
|
normalizeBristle,
|
|
8124
8741
|
normalizeBrushStroke,
|
|
@@ -8141,11 +8758,14 @@ export {
|
|
|
8141
8758
|
normalizeView,
|
|
8142
8759
|
normalizeWetBrush,
|
|
8143
8760
|
normalizeWetParams,
|
|
8761
|
+
overPixels,
|
|
8144
8762
|
padSampleRadius,
|
|
8145
8763
|
paintGradientMappingFor,
|
|
8146
8764
|
paintSourceColor,
|
|
8147
8765
|
paintSourceCss,
|
|
8148
8766
|
paintToMultiColor,
|
|
8767
|
+
paperEffectFromJSON,
|
|
8768
|
+
paperEffectToJSON,
|
|
8149
8769
|
paperGrain,
|
|
8150
8770
|
paperKey,
|
|
8151
8771
|
paperNode,
|
|
@@ -8162,6 +8782,7 @@ export {
|
|
|
8162
8782
|
quickStencil,
|
|
8163
8783
|
rand01,
|
|
8164
8784
|
randSigned,
|
|
8785
|
+
readPaintEffectSource,
|
|
8165
8786
|
readPaintSource,
|
|
8166
8787
|
readPath,
|
|
8167
8788
|
readSwatch,
|