@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.
Files changed (97) hide show
  1. package/dist/agent/index.cjs +3 -3
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.js +3 -3
  4. package/dist/animation-editor/index.cjs +422 -85
  5. package/dist/animation-editor/index.cjs.map +1 -1
  6. package/dist/animation-editor/index.d.cts +174 -7
  7. package/dist/animation-editor/index.d.ts +174 -7
  8. package/dist/animation-editor/index.js +346 -9
  9. package/dist/animation-editor/index.js.map +1 -1
  10. package/dist/{chunk-ORHMVUR6.js → chunk-FICSDDDF.js} +5 -3
  11. package/dist/{chunk-ORHMVUR6.js.map → chunk-FICSDDDF.js.map} +1 -1
  12. package/dist/chunk-FJ3324TZ.js +59 -0
  13. package/dist/chunk-FJ3324TZ.js.map +1 -0
  14. package/dist/{chunk-5EO2IXQE.js → chunk-FPRIUR4T.js} +1 -1
  15. package/dist/chunk-FPRIUR4T.js.map +1 -0
  16. package/dist/{chunk-FHYDQOPC.cjs → chunk-JODTP4DP.cjs} +178 -7
  17. package/dist/chunk-JODTP4DP.cjs.map +1 -0
  18. package/dist/{chunk-NGYD3AMN.js → chunk-MPQAO33O.js} +2 -2
  19. package/dist/{chunk-PEHYWUVV.cjs → chunk-O5RVCDEC.cjs} +82 -82
  20. package/dist/{chunk-PEHYWUVV.cjs.map → chunk-O5RVCDEC.cjs.map} +1 -1
  21. package/dist/{chunk-JRXJTPES.js → chunk-QFSTQHZY.js} +176 -5
  22. package/dist/chunk-QFSTQHZY.js.map +1 -0
  23. package/dist/{chunk-5XXMLBH4.cjs → chunk-QQKTOA4U.cjs} +1 -1
  24. package/dist/chunk-QQKTOA4U.cjs.map +1 -0
  25. package/dist/{chunk-4QQZV5Y5.cjs → chunk-SFCPGMCT.cjs} +11 -9
  26. package/dist/chunk-SFCPGMCT.cjs.map +1 -0
  27. package/dist/{chunk-WE25VLCO.cjs → chunk-V5YGPRCZ.cjs} +36 -36
  28. package/dist/{chunk-WE25VLCO.cjs.map → chunk-V5YGPRCZ.cjs.map} +1 -1
  29. package/dist/chunk-W6QTLHZ3.cjs +59 -0
  30. package/dist/chunk-W6QTLHZ3.cjs.map +1 -0
  31. package/dist/{chunk-WATAQ2Z2.js → chunk-WUDPFWAN.js} +2 -2
  32. package/dist/effects/index.cjs +59 -33
  33. package/dist/effects/index.cjs.map +1 -1
  34. package/dist/effects/index.d.cts +4 -5
  35. package/dist/effects/index.d.ts +4 -5
  36. package/dist/effects/index.js +33 -7
  37. package/dist/effects/index.js.map +1 -1
  38. package/dist/{export-WmYoWmLM.d.cts → export-Bp7ji2r4.d.cts} +1 -2
  39. package/dist/{export-BdfY_lH0.d.ts → export-C1s4eolZ.d.ts} +1 -1
  40. package/dist/{export-BGtXgMJ3.d.ts → export-DAyPY94f.d.ts} +1 -2
  41. package/dist/{export-CbpjwxaI.d.cts → export-DVl4fWy5.d.cts} +1 -1
  42. package/dist/generation/index.cjs +3 -3
  43. package/dist/generation/index.cjs.map +1 -1
  44. package/dist/generation/index.js +3 -3
  45. package/dist/helpers-BBLlekuM.d.ts +2009 -0
  46. package/dist/helpers-i95Ob6Vi.d.cts +2009 -0
  47. package/dist/image-editor/index.cjs +627 -627
  48. package/dist/image-editor/index.cjs.map +1 -1
  49. package/dist/image-editor/index.d.cts +5 -7
  50. package/dist/image-editor/index.d.ts +5 -7
  51. package/dist/image-editor/index.js +13 -13
  52. package/dist/paint-editor/index.cjs +736 -115
  53. package/dist/paint-editor/index.cjs.map +1 -1
  54. package/dist/paint-editor/index.d.cts +248 -13
  55. package/dist/paint-editor/index.d.ts +248 -13
  56. package/dist/paint-editor/index.js +634 -13
  57. package/dist/paint-editor/index.js.map +1 -1
  58. package/dist/{svg-BDrjRnL1.d.cts → svg-BkRL-fZJ.d.cts} +1 -1
  59. package/dist/{svg-C-vZ3q50.d.ts → svg-DJKriJ4A.d.ts} +1 -1
  60. package/dist/tools/index.cjs +2 -2
  61. package/dist/tools/index.cjs.map +1 -1
  62. package/dist/tools/index.js +3 -3
  63. package/dist/{tools-C7JtLYzo.d.ts → tools-CM8Oe8F3.d.ts} +1 -1
  64. package/dist/{tools-DHKGHiSK.d.cts → tools-DZCv2kX1.d.cts} +1 -1
  65. package/dist/vector/index.cjs +1030 -883
  66. package/dist/vector/index.cjs.map +1 -1
  67. package/dist/vector/index.d.cts +61 -6
  68. package/dist/vector/index.d.ts +61 -6
  69. package/dist/vector/index.js +153 -6
  70. package/dist/vector/index.js.map +1 -1
  71. package/dist/video-editor/index.cjs +272 -273
  72. package/dist/video-editor/index.cjs.map +1 -1
  73. package/dist/video-editor/index.d.cts +4 -6
  74. package/dist/video-editor/index.d.ts +4 -6
  75. package/dist/video-editor/index.js +6 -7
  76. package/dist/video-editor/index.js.map +1 -1
  77. package/package.json +1 -1
  78. package/skills/pixerate-animation-editor/SKILL.md +24 -1
  79. package/skills/pixerate-paint-editor/SKILL.md +40 -1
  80. package/skills/pixerate-vector/SKILL.md +15 -0
  81. package/dist/chunk-4QQZV5Y5.cjs.map +0 -1
  82. package/dist/chunk-5EO2IXQE.js.map +0 -1
  83. package/dist/chunk-5XXMLBH4.cjs.map +0 -1
  84. package/dist/chunk-CN4PJHUG.cjs +0 -234
  85. package/dist/chunk-CN4PJHUG.cjs.map +0 -1
  86. package/dist/chunk-FHYDQOPC.cjs.map +0 -1
  87. package/dist/chunk-JRXJTPES.js.map +0 -1
  88. package/dist/chunk-QEG3GRN2.js +0 -234
  89. package/dist/chunk-QEG3GRN2.js.map +0 -1
  90. package/dist/flair-CxB3fOcU.d.ts +0 -739
  91. package/dist/flair-D7BxyBwL.d.cts +0 -739
  92. package/dist/helpers-777mC6dc.d.cts +0 -253
  93. package/dist/helpers-DX2_7L5t.d.ts +0 -253
  94. package/dist/types-BcX7sCsn.d.ts +0 -993
  95. package/dist/types-CznabG7m.d.cts +0 -993
  96. /package/dist/{chunk-NGYD3AMN.js.map → chunk-MPQAO33O.js.map} +0 -0
  97. /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-ORHMVUR6.js";
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-WATAQ2Z2.js";
83
+ } from "../chunk-WUDPFWAN.js";
84
84
  import "../chunk-CTTF4KYV.js";
85
- import "../chunk-QEG3GRN2.js";
86
- import "../chunk-4FX5ACVG.js";
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-5EO2IXQE.js";
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 ref = {
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, ref] });
5039
- return ref;
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 ref of r.references) {
5104
- if (!ref || typeof ref.src !== "string" || typeof ref.id !== "string") continue;
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(ref.src, `Reference "${ref.name}"`);
5107
- guides.references.push({ id: ref.id, name: String(ref.name ?? ref.id), src: ref.src, width: px.width, height: px.height, x: Number(ref.x) || 0, y: Number(ref.y) || 0, displayWidth: Number(ref.displayWidth) || 240, visible: ref.visible !== false });
5108
- const n = Number(/^ref(\d+)$/.exec(ref.id)?.[1] ?? 0);
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,