@pixerate/editor 0.123.0 → 0.124.1

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 (116) hide show
  1. package/dist/agent/index.cjs +2 -2
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.js +3 -3
  4. package/dist/animation/index.d.cts +2 -1
  5. package/dist/animation/index.d.ts +2 -1
  6. package/dist/animation-editor/index.cjs.map +1 -1
  7. package/dist/animation-editor/index.d.cts +11 -733
  8. package/dist/animation-editor/index.d.ts +11 -733
  9. package/dist/animation-editor/index.js.map +1 -1
  10. package/dist/{bezier-C-W9LFyD.d.cts → bezier-CBzKKDjl.d.cts} +2 -66
  11. package/dist/{bezier-DQZE5dxA.d.ts → bezier-CWv6LQal.d.ts} +2 -66
  12. package/dist/{brushstrokeRender-M4S65DHT.cjs → brushstrokeRender-EELTV7U4.cjs} +8 -8
  13. package/dist/{brushstrokeRender-M4S65DHT.cjs.map → brushstrokeRender-EELTV7U4.cjs.map} +1 -1
  14. package/dist/{brushstrokeRender-CDDRSV2W.js → brushstrokeRender-LRV7GXGD.js} +3 -3
  15. package/dist/canvas/index.cjs +6 -6
  16. package/dist/canvas/index.d.cts +10 -6
  17. package/dist/canvas/index.d.ts +10 -6
  18. package/dist/canvas/index.js +1 -1
  19. package/dist/{chunk-SMJGE7HM.cjs → chunk-4QQZV5Y5.cjs} +512 -39
  20. package/dist/chunk-4QQZV5Y5.cjs.map +1 -0
  21. package/dist/{chunk-TVXFHE4T.js → chunk-64PYNEHJ.js} +45 -12
  22. package/dist/chunk-64PYNEHJ.js.map +1 -0
  23. package/dist/{chunk-2IW3BWRT.cjs → chunk-7LQO6OHH.cjs} +2 -2
  24. package/dist/{chunk-2IW3BWRT.cjs.map → chunk-7LQO6OHH.cjs.map} +1 -1
  25. package/dist/chunk-CN4PJHUG.cjs +234 -0
  26. package/dist/chunk-CN4PJHUG.cjs.map +1 -0
  27. package/dist/chunk-FHYDQOPC.cjs +215 -0
  28. package/dist/chunk-FHYDQOPC.cjs.map +1 -0
  29. package/dist/{chunk-KMBC4HNV.js → chunk-J67OLQUQ.js} +2 -2
  30. package/dist/chunk-JRXJTPES.js +215 -0
  31. package/dist/chunk-JRXJTPES.js.map +1 -0
  32. package/dist/{chunk-IPFRWILU.js → chunk-ORHMVUR6.js} +488 -15
  33. package/dist/chunk-ORHMVUR6.js.map +1 -0
  34. package/dist/{chunk-6CJRDJCU.cjs → chunk-PEHYWUVV.cjs} +176 -111
  35. package/dist/chunk-PEHYWUVV.cjs.map +1 -0
  36. package/dist/chunk-QEG3GRN2.js +234 -0
  37. package/dist/chunk-QEG3GRN2.js.map +1 -0
  38. package/dist/{chunk-6ZBR4J62.js → chunk-WATAQ2Z2.js} +79 -14
  39. package/dist/chunk-WATAQ2Z2.js.map +1 -0
  40. package/dist/{chunk-RLRUAPBJ.cjs → chunk-Z6OUCS2K.cjs} +47 -14
  41. package/dist/chunk-Z6OUCS2K.cjs.map +1 -0
  42. package/dist/effects/index.cjs +605 -0
  43. package/dist/effects/index.cjs.map +1 -0
  44. package/dist/effects/index.d.cts +163 -0
  45. package/dist/effects/index.d.ts +163 -0
  46. package/dist/effects/index.js +605 -0
  47. package/dist/effects/index.js.map +1 -0
  48. package/dist/evaluate-2S3vSPLy.d.cts +55 -0
  49. package/dist/evaluate-CfmT6xA3.d.ts +55 -0
  50. package/dist/{export-DfjzHAJI.d.ts → export-BGtXgMJ3.d.ts} +536 -128
  51. package/dist/{export-Dv0-ZVrV.d.cts → export-BdfY_lH0.d.ts} +392 -5
  52. package/dist/{export-Dv0-ZVrV.d.ts → export-CbpjwxaI.d.cts} +392 -5
  53. package/dist/{export-lU5qSpHE.d.cts → export-WmYoWmLM.d.cts} +536 -128
  54. package/dist/flair-CxB3fOcU.d.ts +739 -0
  55. package/dist/flair-D7BxyBwL.d.cts +739 -0
  56. package/dist/gradient-KDfCvhuj.d.cts +66 -0
  57. package/dist/gradient-KDfCvhuj.d.ts +66 -0
  58. package/dist/helpers-777mC6dc.d.cts +253 -0
  59. package/dist/helpers-DX2_7L5t.d.ts +253 -0
  60. package/dist/image-editor/index.cjs +1296 -805
  61. package/dist/image-editor/index.cjs.map +1 -1
  62. package/dist/image-editor/index.d.cts +39 -182
  63. package/dist/image-editor/index.d.ts +39 -182
  64. package/dist/image-editor/index.js +497 -6
  65. package/dist/image-editor/index.js.map +1 -1
  66. package/dist/{opentype-p8Xxf-Wh.d.ts → opentype-14PDI5D2.d.ts} +1 -1
  67. package/dist/{opentype-BV7wWatY.d.cts → opentype-D80AVmMu.d.cts} +1 -1
  68. package/dist/paint/index.cjs +2 -2
  69. package/dist/paint/index.d.cts +32 -10
  70. package/dist/paint/index.d.ts +32 -10
  71. package/dist/paint/index.js +1 -1
  72. package/dist/paint-editor/index.cjs +10 -8
  73. package/dist/paint-editor/index.cjs.map +1 -1
  74. package/dist/paint-editor/index.d.cts +11 -5
  75. package/dist/paint-editor/index.d.ts +11 -5
  76. package/dist/paint-editor/index.js +4 -2
  77. package/dist/paint-editor/index.js.map +1 -1
  78. package/dist/parameters/index.d.cts +4 -184
  79. package/dist/parameters/index.d.ts +4 -184
  80. package/dist/{pathfinder-IxcyhO00.d.cts → pathfinder-C0UoFypx.d.cts} +1 -1
  81. package/dist/{pathfinder-StVChJOT.d.ts → pathfinder-DjISxkUv.d.ts} +1 -1
  82. package/dist/{player-DrcP2xqj.d.cts → player-CBtEoU2x.d.cts} +2 -54
  83. package/dist/{player-DV4Jujdq.d.ts → player-cGLrngL_.d.ts} +2 -54
  84. package/dist/{svg-BtND4TJu.d.cts → svg-BDrjRnL1.d.cts} +3 -2
  85. package/dist/{svg-BfVXG7hL.d.ts → svg-C-vZ3q50.d.ts} +3 -2
  86. package/dist/tools/index.cjs +2 -2
  87. package/dist/tools/index.cjs.map +1 -1
  88. package/dist/tools/index.js +3 -3
  89. package/dist/tools-C7JtLYzo.d.ts +126 -0
  90. package/dist/tools-DHKGHiSK.d.cts +126 -0
  91. package/dist/{types-fuXEme9j.d.ts → types-BcX7sCsn.d.ts} +2 -2
  92. package/dist/{types-DJF3Ghn5.d.cts → types-CznabG7m.d.cts} +2 -2
  93. package/dist/types-D3-J3TFf.d.ts +185 -0
  94. package/dist/types-DMcf_z4J.d.cts +185 -0
  95. package/dist/vector/index.d.cts +10 -10
  96. package/dist/vector/index.d.ts +10 -10
  97. package/dist/video-editor/index.cjs +749 -277
  98. package/dist/video-editor/index.cjs.map +1 -1
  99. package/dist/video-editor/index.d.cts +265 -138
  100. package/dist/video-editor/index.d.ts +265 -138
  101. package/dist/video-editor/index.js +490 -18
  102. package/dist/video-editor/index.js.map +1 -1
  103. package/dist/{warp-BjmAgQwq.d.cts → warp-CH9MM435.d.cts} +1 -1
  104. package/dist/{warp-Ca5fedzS.d.ts → warp-k_PdtXOG.d.ts} +1 -1
  105. package/package.json +6 -1
  106. package/skills/pixerate-image-editor/SKILL.md +7 -0
  107. package/skills/pixerate-image-editor/references/provider-effects.md +35 -0
  108. package/skills/pixerate-video-editor/SKILL.md +39 -1
  109. package/dist/chunk-6CJRDJCU.cjs.map +0 -1
  110. package/dist/chunk-6ZBR4J62.js.map +0 -1
  111. package/dist/chunk-IPFRWILU.js.map +0 -1
  112. package/dist/chunk-RLRUAPBJ.cjs.map +0 -1
  113. package/dist/chunk-SMJGE7HM.cjs.map +0 -1
  114. package/dist/chunk-TVXFHE4T.js.map +0 -1
  115. /package/dist/{brushstrokeRender-CDDRSV2W.js.map → brushstrokeRender-LRV7GXGD.js.map} +0 -0
  116. /package/dist/{chunk-KMBC4HNV.js.map → chunk-J67OLQUQ.js.map} +0 -0
@@ -51,6 +51,7 @@ import {
51
51
  VIBRANCE_GREY,
52
52
  XYZ_D50_TO_SRGB,
53
53
  activeAdjustmentLayers,
54
+ activeEffectSmartFilters,
54
55
  activeSmartFilters,
55
56
  addLayerEffect,
56
57
  addLayerFilter,
@@ -180,6 +181,7 @@ import {
180
181
  isRenderedLayer,
181
182
  isSelectionEmpty,
182
183
  isSelectionMode,
184
+ isSmartSurfaceIncomplete,
183
185
  isSpatialFilter,
184
186
  isTransformAnchor,
185
187
  isValidWarp,
@@ -197,7 +199,7 @@ import {
197
199
  legacyView,
198
200
  levelsValue,
199
201
  linearSrgbToXy,
200
- linearToSrgb,
202
+ linearToSrgb as linearToSrgb2,
201
203
  liveComponent,
202
204
  liveShapeSubpaths,
203
205
  mapCropHandle,
@@ -360,13 +362,25 @@ import {
360
362
  writePsd,
361
363
  xyToTempTint,
362
364
  xyToUv
363
- } from "../chunk-6ZBR4J62.js";
365
+ } from "../chunk-WATAQ2Z2.js";
364
366
  import {
365
367
  parseCssColor
366
368
  } from "../chunk-CTTF4KYV.js";
367
369
  import {
368
370
  loadDocument
369
371
  } from "../chunk-U5HZSFCS.js";
372
+ import {
373
+ createEffectToolProvider
374
+ } from "../chunk-JRXJTPES.js";
375
+ import {
376
+ EffectRendererPool,
377
+ EffectUnsupportedError,
378
+ EffectsSlot,
379
+ createEffectPixels,
380
+ linearToSrgb,
381
+ resolveEffectValues
382
+ } from "../chunk-QEG3GRN2.js";
383
+ import "../chunk-4FX5ACVG.js";
370
384
  import {
371
385
  createCaptureTool
372
386
  } from "../chunk-REOJJIY3.js";
@@ -475,7 +489,7 @@ import {
475
489
  pathStrokeCoverage,
476
490
  pathStrokeDabs,
477
491
  renderBrushstroke
478
- } from "../chunk-TVXFHE4T.js";
492
+ } from "../chunk-64PYNEHJ.js";
479
493
  import {
480
494
  BODY_SCATTER,
481
495
  DEFAULT_FLOW,
@@ -2772,6 +2786,170 @@ function refineSelection(pixels, mask, params = {}) {
2772
2786
  return createSelectionMask(mask.width, mask.height, out);
2773
2787
  }
2774
2788
 
2789
+ // src/image-editor/effects.ts
2790
+ var IMAGE_EFFECT_PLACES = {
2791
+ /** A smart filter: the effect changes the pixels below it. */
2792
+ filter: ["filter"],
2793
+ /** An effect layer: drawn at the document's size. */
2794
+ layer: ["generator", "overlay", "title"]
2795
+ };
2796
+ function checkImageEffectPlace(d, place) {
2797
+ const allowed = IMAGE_EFFECT_PLACES[place];
2798
+ if (d.kind === "motion" || !d.uses.some((u) => allowed.includes(u))) {
2799
+ const instead = place === "filter" && d.uses.some((u) => IMAGE_EFFECT_PLACES.layer.includes(u)) ? "; add it as an effect layer with add_effect_layer instead" : place === "layer" && d.uses.includes("filter") ? "; apply it to a layer as a smart filter with apply_effect instead" : "";
2800
+ throw new EffectUnsupportedError(
2801
+ `"${d.id}" (${d.name}) is a ${d.uses.join(" / ")} effect, and ${place === "filter" ? "a smart filter takes filter effects" : "an effect layer takes generator, overlay or title effects"}${instead}.`,
2802
+ d.id
2803
+ );
2804
+ }
2805
+ }
2806
+ function over(base, top) {
2807
+ const out = new Uint8ClampedArray(base.length);
2808
+ for (let i = 0; i < out.length; i += 4) {
2809
+ const ta = top[i + 3] / 255;
2810
+ const ba = base[i + 3] / 255;
2811
+ const a = ta + ba * (1 - ta);
2812
+ out[i + 3] = Math.round(a * 255);
2813
+ if (a > 0) for (let c = 0; c < 3; c++) out[i + c] = (top[i + c] * ta + base[i + c] * ba * (1 - ta)) / a;
2814
+ }
2815
+ return out;
2816
+ }
2817
+ function effectPixelsToSurface(p, depth) {
2818
+ if (depth === 8 || !p.float) return surfaceFromPixels({ width: p.width, height: p.height, data: p.data }, 4);
2819
+ if (depth === 32) return surfaceFromDeepPixels({ width: p.width, height: p.height, data: p.float }, 4);
2820
+ const f = p.float;
2821
+ const d = new Uint16Array(f.length);
2822
+ const code = (v) => v > 0 ? v < 1 ? Math.round(v * 65535) : 65535 : 0;
2823
+ for (let i = 0; i < f.length; i += 4) {
2824
+ d[i] = code(linearToSrgb(f[i]));
2825
+ d[i + 1] = code(linearToSrgb(f[i + 1]));
2826
+ d[i + 2] = code(linearToSrgb(f[i + 2]));
2827
+ d[i + 3] = code(f[i + 3]);
2828
+ }
2829
+ return surfaceFromDeepPixels({ width: p.width, height: p.height, data: d }, 4);
2830
+ }
2831
+ var ImageEffectsEngine = class {
2832
+ slot;
2833
+ cache = /* @__PURE__ */ new Map();
2834
+ pending = /* @__PURE__ */ new Map();
2835
+ failures = /* @__PURE__ */ new Map();
2836
+ pool;
2837
+ descriptions = /* @__PURE__ */ new Map();
2838
+ listeners = /* @__PURE__ */ new Set();
2839
+ maxEntries;
2840
+ generation = 0;
2841
+ detach;
2842
+ constructor(options = {}) {
2843
+ this.slot = options.slot ?? new EffectsSlot();
2844
+ this.maxEntries = options.maxEntries ?? 24;
2845
+ this.pool = new EffectRendererPool({ maxRenderers: options.maxRenderers });
2846
+ this.detach = this.slot.subscribe(() => {
2847
+ this.reset();
2848
+ this.notify();
2849
+ });
2850
+ }
2851
+ get provider() {
2852
+ return this.slot.provider;
2853
+ }
2854
+ /** See `SmartEffectResolver.token`: bumped when the provider changes. */
2855
+ get token() {
2856
+ return this.generation;
2857
+ }
2858
+ /** True while a render is under way. */
2859
+ get busy() {
2860
+ return this.pending.size > 0;
2861
+ }
2862
+ /** See `SmartEffectResolver`. */
2863
+ resolve(filter, input, width, height, key) {
2864
+ const hit = this.cache.get(key);
2865
+ if (hit) {
2866
+ this.cache.delete(key);
2867
+ this.cache.set(key, hit);
2868
+ return { result: hit, pending: false };
2869
+ }
2870
+ if (this.pending.has(key)) return { result: null, pending: true };
2871
+ const provider = this.slot.provider;
2872
+ if (!provider || this.failures.has(key)) return { result: null, pending: false };
2873
+ const generation = this.generation;
2874
+ const source = { width, height, data: new Uint8ClampedArray(input) };
2875
+ const run = this.filter(provider, filter.effect, filter.values, source).then((out) => {
2876
+ if (generation !== this.generation) return;
2877
+ this.cache.set(key, out);
2878
+ while (this.cache.size > this.maxEntries) this.cache.delete(this.cache.keys().next().value);
2879
+ }).catch((error) => {
2880
+ if (generation === this.generation) this.failures.set(key, error instanceof Error ? error : new Error(String(error)));
2881
+ }).finally(() => {
2882
+ if (this.pending.get(key) === run) this.pending.delete(key);
2883
+ if (generation === this.generation) this.notify();
2884
+ });
2885
+ this.pending.set(key, run);
2886
+ return { result: null, pending: true };
2887
+ }
2888
+ /** A filter effect over `source`: its result, or (for an overlay) the effect drawn over it. */
2889
+ async filter(provider, effect, values, source) {
2890
+ const d = await this.describe(provider, effect);
2891
+ const takesSource = d.inputs.some((i) => i.name === "source");
2892
+ const out = await this.render(provider, effect, source.width, source.height, 8, { time: 0, values, ...takesSource ? { inputs: { source } } : {} });
2893
+ return takesSource ? out.data : over(source.data, out.data);
2894
+ }
2895
+ /**
2896
+ * Renders a generator or overlay effect at a size and depth (an effect layer's pixels). Rejects
2897
+ * with the provider's error, or without a provider.
2898
+ */
2899
+ async renderLayer(effect, values, size) {
2900
+ const provider = this.slot.require();
2901
+ const depth = size.depth ?? 8;
2902
+ const out = await this.render(provider, effect, size.width, size.height, depth, { time: size.time ?? 0, values });
2903
+ return out.width === size.width && out.height === size.height ? out : createEffectPixels(size.width, size.height);
2904
+ }
2905
+ /** Why the result for `key` failed, if it did. */
2906
+ failure(key) {
2907
+ return this.failures.get(key);
2908
+ }
2909
+ /** Every failure so far. */
2910
+ failuresList() {
2911
+ return [...this.failures].map(([key, error]) => ({ key, error }));
2912
+ }
2913
+ /** Resolves when nothing is rendering. */
2914
+ async idle() {
2915
+ while (this.pending.size) await Promise.all([...this.pending.values()]);
2916
+ }
2917
+ /** The provider's description of an effect, fetched once per provider. */
2918
+ describe(provider, id) {
2919
+ let d = this.descriptions.get(id);
2920
+ if (!d) {
2921
+ d = Promise.resolve().then(() => provider.describe(id));
2922
+ d.catch(() => this.descriptions.delete(id));
2923
+ this.descriptions.set(id, d);
2924
+ }
2925
+ return d;
2926
+ }
2927
+ render(provider, id, width, height, depth, request) {
2928
+ return this.pool.render(provider, id, { width, height, depth }, request);
2929
+ }
2930
+ /** Forgets every result and failure and disposes the renderers. */
2931
+ reset() {
2932
+ this.generation++;
2933
+ this.cache.clear();
2934
+ this.pending.clear();
2935
+ this.failures.clear();
2936
+ this.descriptions.clear();
2937
+ this.pool.clear();
2938
+ }
2939
+ subscribe(listener) {
2940
+ this.listeners.add(listener);
2941
+ return () => this.listeners.delete(listener);
2942
+ }
2943
+ notify() {
2944
+ for (const l of [...this.listeners]) l();
2945
+ }
2946
+ destroy() {
2947
+ this.reset();
2948
+ this.detach();
2949
+ this.listeners.clear();
2950
+ }
2951
+ };
2952
+
2775
2953
  // src/image-editor/free-transform.ts
2776
2954
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
2777
2955
  var fail = (message) => {
@@ -3885,7 +4063,15 @@ var ImageEditorController = class {
3885
4063
  viewKey = [];
3886
4064
  /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */
3887
4065
  compositor = new LayerCompositor();
4066
+ /** The effects provider slot (see `setEffectsProvider`). */
4067
+ effects = new EffectsSlot();
4068
+ /** Renders provider effects: effect smart filter results and effect layers. */
4069
+ effectsEngine = new ImageEffectsEngine({ slot: this.effects });
4070
+ effectRefresh = "none";
3888
4071
  constructor(options = {}) {
4072
+ this.effects.subscribe(() => this.queueEffectRefresh("all"));
4073
+ this.effectsEngine.subscribe(() => this.queueEffectRefresh("incomplete"));
4074
+ if (options.effects) this.effects.set(options.effects);
3889
4075
  this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
3890
4076
  if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
3891
4077
  this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
@@ -4019,6 +4205,206 @@ var ImageEditorController = class {
4019
4205
  const currentState = { ...this.state };
4020
4206
  this.onStateChange?.(currentState);
4021
4207
  this.listeners.forEach((l) => l(currentState));
4208
+ if (this.effects.provider) this.queueEffectRefresh("incomplete");
4209
+ }
4210
+ // --- Provider effects ---
4211
+ queueEffectRefresh(kind) {
4212
+ const queued = this.effectRefresh !== "none";
4213
+ if (kind === "all" || this.effectRefresh === "none") this.effectRefresh = kind;
4214
+ if (queued) return;
4215
+ queueMicrotask(() => {
4216
+ const all = this.effectRefresh === "all";
4217
+ this.effectRefresh = "none";
4218
+ this.refreshEffectSurfaces(all);
4219
+ });
4220
+ }
4221
+ /**
4222
+ * Renders smart objects with effect filters again: every one (`all`, after the provider changed)
4223
+ * or those drawn while a result was rendering, once it is in. A cache refresh: no undo step.
4224
+ */
4225
+ refreshEffectSurfaces(all) {
4226
+ let changed = false;
4227
+ const canvas = this.canvasSize;
4228
+ const walk = (layers) => {
4229
+ let out = null;
4230
+ layers.forEach((l, i) => {
4231
+ let next2 = l;
4232
+ if (l.type === "group") {
4233
+ const children = walk(l.children);
4234
+ if (children !== l.children) next2 = { ...l, children };
4235
+ } else if (l.type === "smart" && l.smartFilters?.filters.some((f) => f.kind === "effect") && (all || isSmartSurfaceIncomplete(l.surface))) {
4236
+ try {
4237
+ const r = rerender(l, canvas);
4238
+ if (all || !isSmartSurfaceIncomplete(r.surface)) next2 = r;
4239
+ } catch {
4240
+ }
4241
+ }
4242
+ if (next2 !== l) {
4243
+ out ??= [...layers];
4244
+ out[i] = next2;
4245
+ changed = true;
4246
+ }
4247
+ });
4248
+ return out ?? layers;
4249
+ };
4250
+ const next = walk(this.state.layers);
4251
+ if (!changed) return;
4252
+ this.state.layers = next;
4253
+ this.notify();
4254
+ }
4255
+ /** The registered effects provider, or `null`. */
4256
+ get effectsProvider() {
4257
+ return this.effects.provider;
4258
+ }
4259
+ /** Registers (or with `null` removes) the effects provider. Not an edit: no undo step. */
4260
+ setEffectsProvider(provider) {
4261
+ this.effects.set(provider);
4262
+ }
4263
+ /** Registers `provider` and returns a function that removes it again (if it is still the one set). */
4264
+ useEffects(provider) {
4265
+ return this.effects.use(provider);
4266
+ }
4267
+ /** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
4268
+ async describeEffect(effect, place) {
4269
+ const provider = this.effects.require();
4270
+ const d = await this.effectsEngine.describe(provider, effect);
4271
+ if (place) checkImageEffectPlace(d, place);
4272
+ return d;
4273
+ }
4274
+ /** Resolves when every effect render has landed and the surfaces waiting for them are drawn. */
4275
+ async effectsIdle() {
4276
+ const tick = () => new Promise((r) => queueMicrotask(r));
4277
+ for (let round6 = 0; round6 < 32; round6++) {
4278
+ await this.effectsEngine.idle();
4279
+ await tick();
4280
+ await tick();
4281
+ if (!this.effectsEngine.busy && this.effectRefresh === "none") return;
4282
+ }
4283
+ }
4284
+ /**
4285
+ * Applies a provider filter effect as a smart filter (at `index`, default the top), one undo
4286
+ * step. A pixel, shape or type layer (or a group) is first converted to a smart object in the
4287
+ * same step (Filter › Convert for Smart Filters), keeping its id when it is one layer. `values`
4288
+ * are checked against the effect's controls and stored over its defaults.
4289
+ */
4290
+ async applyEffectFilter(layerId, effect, options = {}) {
4291
+ const d = await this.describeEffect(effect, "filter");
4292
+ const values = resolveEffectValues(d, options.values);
4293
+ const found = findLayer(this.state.layers, layerId);
4294
+ if (!found) throw new LayerOperationError(`There is no layer "${layerId}"; call list_layers for the layer ids.`);
4295
+ let layers = this.state.layers;
4296
+ let id = layerId;
4297
+ let converted = false;
4298
+ if (found.layer.type !== "smart") {
4299
+ if (found.layer.type === "adjustment" || found.layer.type === "annotation") throw new LayerOperationError(`Layer "${found.layer.name || layerId}" is ${found.layer.type === "adjustment" ? "an adjustment layer" : "an annotation"}, which has no pixels to filter; apply the effect to a pixel layer or smart object.`);
4300
+ const res2 = convertToSmartObject(layers, [layerId], this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l) });
4301
+ layers = res2.layers;
4302
+ id = res2.layer.id;
4303
+ converted = true;
4304
+ }
4305
+ const filter = normalizeSmartFilter({ kind: "effect", effect: d.id, provider: this.effects.require().id, name: d.name, values, enabled: options.enabled !== false, blend: options.blend, opacity: options.opacity });
4306
+ const res = addSmartFilter(layers, id, filter, this.canvasSize, options.index);
4307
+ this.commitLayers(res.layers);
4308
+ return { layerId: id, filter: res.filter, converted };
4309
+ }
4310
+ /**
4311
+ * Changes an effect smart filter: `values` merged over its own (`null` resets a control to the
4312
+ * effect's default), `enabled`, `blend`, `opacity` or its place in the stack (`index`). Values
4313
+ * are checked against the provider's controls. One undo step (none with `commitToHistory`
4314
+ * false, during a drag).
4315
+ */
4316
+ async updateEffectFilter(layerId, filterId, changes, commitToHistory = true) {
4317
+ const layer = requireSmartObject(this.state.layers, layerId);
4318
+ const was = layer.smartFilters?.filters.find((f) => f.id === filterId);
4319
+ if (!was || was.kind !== "effect") throw new LayerOperationError(`Smart object "${layer.name || layerId}" has no effect filter "${filterId}"; its filters are ${(layer.smartFilters?.filters ?? []).map((f) => `${f.id} (${f.kind === "effect" ? f.name ?? f.effect : f.kind})`).join(", ") || "none"}.`);
4320
+ const update = {};
4321
+ if (changes.values) {
4322
+ const d = await this.describeEffect(was.effect);
4323
+ const given = {};
4324
+ const base = { ...was.values };
4325
+ for (const [k, v] of Object.entries(changes.values)) {
4326
+ if (v !== null) given[k] = v;
4327
+ else if (k in d.controls.properties) base[k] = d.defaults[k];
4328
+ else throw new LayerOperationError(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
4329
+ }
4330
+ update.values = { ...base, ...resolveEffectValues({ ...d, defaults: base }, given) };
4331
+ }
4332
+ for (const k of ["enabled", "blend", "opacity"]) if (changes[k] !== void 0) update[k] = changes[k];
4333
+ let res = updateSmartFilter(this.state.layers, layerId, filterId, update, this.canvasSize);
4334
+ if (changes.index !== void 0) res = { ...res, layers: moveSmartFilter(res.layers, layerId, filterId, changes.index, this.canvasSize).layers };
4335
+ this.commitLayers(res.layers, void 0, commitToHistory);
4336
+ return requireSmartObject(this.state.layers, layerId).smartFilters.filters.find((f) => f.id === filterId);
4337
+ }
4338
+ /**
4339
+ * Adds an effect layer: a pixel layer drawn by a provider generator, overlay or title effect at
4340
+ * the document's size and bit depth (at `time` ms, default 0), placed like `addPixelLayer`. One
4341
+ * undo step. Masks, blend and opacity apply as to any layer. Returns its id.
4342
+ */
4343
+ async addEffectLayer(effect, options = {}) {
4344
+ const d = await this.describeEffect(effect, "layer");
4345
+ const values = resolveEffectValues(d, options.values);
4346
+ const { width, height } = this.naturalSize;
4347
+ const pixels = await this.effectsEngine.renderLayer(d.id, values, { width, height, depth: this.bitDepth, time: options.time });
4348
+ const blend = parseBlend(options.blend);
4349
+ if (options.opacity !== void 0 && !(options.opacity >= 0 && options.opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
4350
+ const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));
4351
+ const layer = {
4352
+ type: "pixel",
4353
+ id: options.id && !ids.has(options.id) ? options.id : newLayerId(),
4354
+ name: options.name?.trim() || d.name,
4355
+ visible: true,
4356
+ opacity: options.opacity ?? 1,
4357
+ blend: blend ?? "normal",
4358
+ surface: effectPixelsToSurface(pixels, this.bitDepth),
4359
+ offset: { x: 0, y: 0 },
4360
+ generator: { effect: d.id, provider: this.effects.require().id, name: d.name, values, ...options.time ? { time: options.time } : {} }
4361
+ };
4362
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(options));
4363
+ this.commitLayers(next, () => {
4364
+ this.state.activeLayerId = layer.id;
4365
+ this.state.activeTarget = "pixels";
4366
+ });
4367
+ return layer.id;
4368
+ }
4369
+ /**
4370
+ * Draws an effect layer again with new `values` (merged over its own; `null` resets a control)
4371
+ * or `time`, at the document's size and depth, replacing its pixels (painting on it is lost).
4372
+ * One undo step (none with `commitToHistory` false). Returns its effect settings.
4373
+ */
4374
+ async updateEffectLayer(layerId, changes, commitToHistory = true) {
4375
+ const found = findLayer(this.state.layers, layerId);
4376
+ const layer = found?.layer;
4377
+ if (!layer || layer.type !== "pixel" || !layer.generator) throw new LayerOperationError(`"${layerId}" is not an effect layer; add one with add_effect_layer.`);
4378
+ requireUnlocked(layer, "pixels", "draw its effect again");
4379
+ const g = layer.generator;
4380
+ const d = await this.describeEffect(g.effect);
4381
+ const base = { ...g.values };
4382
+ const given = {};
4383
+ for (const [k, v] of Object.entries(changes.values ?? {})) {
4384
+ if (v !== null) given[k] = v;
4385
+ else if (k in d.controls.properties) base[k] = d.defaults[k];
4386
+ else throw new LayerOperationError(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
4387
+ }
4388
+ const values = { ...base, ...resolveEffectValues({ ...d, defaults: base }, given) };
4389
+ const time = changes.time ?? g.time ?? 0;
4390
+ const { width, height } = this.naturalSize;
4391
+ const pixels = await this.effectsEngine.renderLayer(d.id, values, { width, height, depth: this.bitDepth, time });
4392
+ const generator = { ...g, values, provider: this.effects.require().id, ...time ? { time } : {} };
4393
+ if (!time) delete generator.time;
4394
+ const next = mapLayer(this.state.layers, layerId, (l) => ({ ...l, surface: effectPixelsToSurface(pixels, this.bitDepth), offset: { x: 0, y: 0 }, generator }));
4395
+ this.commitLayers(next, void 0, commitToHistory);
4396
+ return generator;
4397
+ }
4398
+ /** Turns an effect layer into a plain pixel layer keeping its pixels (one undo step). */
4399
+ rasterizeEffectLayer(layerId) {
4400
+ const found = findLayer(this.state.layers, layerId);
4401
+ if (!found || found.layer.type !== "pixel" || !found.layer.generator) throw new LayerOperationError(`"${layerId}" is not an effect layer.`);
4402
+ this.commitLayers(
4403
+ mapLayer(this.state.layers, layerId, (l) => {
4404
+ const { generator: _g, ...rest } = l;
4405
+ return rest;
4406
+ })
4407
+ );
4022
4408
  }
4023
4409
  displayFor = null;
4024
4410
  /** Keeps the renderer's display transform in step with the document profile, mode and Proof Setup. */
@@ -4918,7 +5304,7 @@ var ImageEditorController = class {
4918
5304
  }
4919
5305
  // --- Smart objects ---
4920
5306
  get canvasSize() {
4921
- return this.naturalSize;
5307
+ return { ...this.naturalSize, effects: this.effectsEngine };
4922
5308
  }
4923
5309
  /**
4924
5310
  * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the
@@ -13440,7 +13826,7 @@ function smartFilterRows(layer) {
13440
13826
  filter: f,
13441
13827
  index,
13442
13828
  drawn: f.kind !== "psd",
13443
- label: f.kind === "psd" ? f.name.replace(/&/g, "").replace(/\.\.\.$/, "") : f.kind === "blur" ? `Blur (${f.radius} px)` : "Develop"
13829
+ label: f.kind === "psd" ? f.name.replace(/&/g, "").replace(/\.\.\.$/, "") : f.kind === "effect" ? f.name ?? f.effect : f.kind === "blur" ? `Blur (${f.radius} px)` : "Develop"
13444
13830
  }));
13445
13831
  return rows.reverse();
13446
13832
  }
@@ -14295,6 +14681,103 @@ var FreeTransformTool = class {
14295
14681
  return false;
14296
14682
  }
14297
14683
  };
14684
+
14685
+ // src/image-editor/provider-effect-tools.ts
14686
+ function imageLayerAppliedEffects(layer) {
14687
+ const out = [];
14688
+ const ref = (r) => ({ effect: r.effect, ...r.name ? { name: r.name } : {}, ...r.provider ? { provider: r.provider } : {} });
14689
+ if (layer.type === "pixel" && layer.generator) out.push({ id: "generator", ...ref(layer.generator), role: "effect layer", enabled: true, values: layer.generator.values, ...layer.generator.time ? { time: layer.generator.time } : {} });
14690
+ if (layer.type === "smart")
14691
+ (layer.smartFilters?.filters ?? []).forEach((f, index) => {
14692
+ if (f.kind === "effect") out.push({ id: f.id, ...ref(f), role: "smart filter", enabled: f.enabled && layer.smartFilters.enabled, opacity: f.opacity, blend: f.blend, values: f.values, index });
14693
+ });
14694
+ return out;
14695
+ }
14696
+ function createImageEffectsToolProvider(controller, options = {}) {
14697
+ const readOnlyReason = () => {
14698
+ const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
14699
+ return ro ? typeof ro === "string" ? ro : "The image is read-only." : null;
14700
+ };
14701
+ const layerOf = (id) => {
14702
+ const found = findLayer(controller.getState().layers, id);
14703
+ if (!found) {
14704
+ const ids = flattenLayers(controller.getState().layers).map((l) => `"${l.id}"`);
14705
+ throw new Error(`There is no layer "${id}"; the layers are ${ids.join(", ") || "none"} (list_layers lists them).`);
14706
+ }
14707
+ return found.layer;
14708
+ };
14709
+ const result = (target, instance, extra = {}) => {
14710
+ const effects = imageLayerAppliedEffects(layerOf(target));
14711
+ return { target, effect: effects.find((e) => e.id === instance) ?? effects[effects.length - 1], effects, ...extra };
14712
+ };
14713
+ const plainValues = (v) => v;
14714
+ return createEffectToolProvider({
14715
+ id: options.id ?? "image-effects",
14716
+ namespace: options.namespace,
14717
+ target: { noun: "layer", id: "A layer id from list_layers." },
14718
+ applyUses: ["filter"],
14719
+ applyDetails: "The effect becomes a smart filter: it runs on the layer's pixels after the smart filters below it, with its own blend and opacity, under the smart filter mask, and can be changed or removed later. A pixel, shape or type layer (or a group) is converted to a smart object first, in the same undo step (one layer keeps its id; the result says converted). Generators and overlays go on their own layer through add_effect_layer.",
14720
+ provider: () => controller.effectsProvider,
14721
+ readOnlyReason,
14722
+ list: (target) => {
14723
+ const layers = target ? [layerOf(target)] : flattenLayers(controller.getState().layers);
14724
+ return layers.map((l) => ({ target: l.id, ...l.name ? { name: l.name } : {}, effects: imageLayerAppliedEffects(l) })).filter((t) => target || t.effects.length);
14725
+ },
14726
+ apply: async (target, effect, args) => {
14727
+ const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity });
14728
+ return result(r.layerId, r.filter.id, { converted: r.converted });
14729
+ },
14730
+ update: async (target, instance, changes) => {
14731
+ const layer = layerOf(target);
14732
+ if (instance === "generator") {
14733
+ const extra = ["enabled", "opacity", "index"].filter((k) => changes[k] !== void 0);
14734
+ if (extra.length) throw new Error(`An effect layer's own effect only takes values; change the layer's ${extra.join(", ")} with update_layer.`);
14735
+ if (layer.type !== "pixel" || !layer.generator) throw new Error(`"${target}" is not an effect layer.`);
14736
+ await controller.updateEffectLayer(target, { values: changes.values });
14737
+ return result(target, "generator");
14738
+ }
14739
+ await controller.updateEffectFilter(target, instance, { values: changes.values, enabled: changes.enabled, opacity: changes.opacity, index: changes.index });
14740
+ return result(target, instance);
14741
+ },
14742
+ remove: async (target, instance) => {
14743
+ const layer = layerOf(target);
14744
+ const was = imageLayerAppliedEffects(layer).find((e) => e.id === instance);
14745
+ if (!was) throw new Error(`Layer "${layer.name || target}" has no provider effect "${instance}"; its effects are ${imageLayerAppliedEffects(layer).map((e) => `"${e.id}"`).join(", ") || "none"} (list_applied_effects).`);
14746
+ if (instance === "generator") controller.rasterizeEffectLayer(target);
14747
+ else controller.removeSmartFilter(target, instance);
14748
+ return { target, effect: was, effects: imageLayerAppliedEffects(layerOf(target)) };
14749
+ },
14750
+ subscribe: (notify) => controller.subscribe(() => notify()),
14751
+ extraTools: (ctx) => [
14752
+ {
14753
+ name: "add_effect_layer",
14754
+ title: "Add an effect layer",
14755
+ description: `Adds a layer drawn by a provider generator, overlay or title effect (search_effects with use generator, overlay or title) at the document's size and bit depth, above the active layer unless placed. Masks, blend mode, opacity and layer styles apply as to any layer; transparent effects show the layers beneath. update_effect with effect "generator" draws it again with new values; remove_effect "generator" keeps its pixels as a plain layer. One undo step. Returns the new layer id and its effect.`,
14756
+ inputSchema: {
14757
+ type: "object",
14758
+ properties: {
14759
+ effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
14760
+ values: ctx.valuesSchema("Control values over the effect's defaults"),
14761
+ time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." },
14762
+ name: { type: "string", minLength: 1, description: "The layer name (default: the effect's name)." },
14763
+ aboveId: { type: "string", minLength: 1, description: "Put it directly above this layer (default: above the active layer, else on top)." },
14764
+ belowId: { type: "string", minLength: 1, description: "Put it directly below this layer." },
14765
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131 (default 1)." },
14766
+ blend: { type: "string", enum: BLEND_MODES.map((m) => m.id).filter((b) => b !== "pigment"), description: "Layer blend mode (default normal); screen or add for light effects, multiply for textures." }
14767
+ },
14768
+ required: ["effect"],
14769
+ additionalProperties: false
14770
+ },
14771
+ annotations: { title: "Add an effect layer", ...ctx.annotations.write },
14772
+ handler: async (args) => {
14773
+ ctx.requireProvider();
14774
+ const id = await controller.addEffectLayer(args.effect, { values: plainValues(args.values), time: args.time, name: args.name, aboveId: args.aboveId, belowId: args.belowId, opacity: args.opacity, blend: args.blend });
14775
+ return { layerId: id, effects: imageLayerAppliedEffects(layerOf(id)) };
14776
+ }
14777
+ }
14778
+ ]
14779
+ });
14780
+ }
14298
14781
  export {
14299
14782
  ADJUSTMENT_KEYS,
14300
14783
  ADJUSTMENT_KINDS,
@@ -14372,12 +14855,14 @@ export {
14372
14855
  IMAGE_CROP_PRESETS,
14373
14856
  IMAGE_EDITOR_SCHEMA_VERSION,
14374
14857
  IMAGE_EDITOR_STATE_VERSION,
14858
+ IMAGE_EFFECT_PLACES,
14375
14859
  IMAGE_FILTER_PRESETS,
14376
14860
  INTENTS,
14377
14861
  INTENT_OPTIONS,
14378
14862
  INTERPOLATIONS,
14379
14863
  ImageEditorController,
14380
14864
  ImageEditorRenderer,
14865
+ ImageEffectsEngine,
14381
14866
  LATEST_PROCESS_VERSION,
14382
14867
  LAYERED_SCHEMA_VERSION,
14383
14868
  LAYER_EFFECT_KINDS,
@@ -14522,6 +15007,7 @@ export {
14522
15007
  XYZ_D50_TO_SRGB,
14523
15008
  ZlibError,
14524
15009
  activeAdjustmentLayers,
15010
+ activeEffectSmartFilters,
14525
15011
  activeSmartFilters,
14526
15012
  addLayerEffect,
14527
15013
  addLayerFilter,
@@ -14599,6 +15085,7 @@ export {
14599
15085
  canvasToImagePoint,
14600
15086
  caretAt,
14601
15087
  changedTiles,
15088
+ checkImageEffectPlace,
14602
15089
  clampControlValue,
14603
15090
  clampCropToImage,
14604
15091
  cleanMixer,
@@ -14638,6 +15125,7 @@ export {
14638
15125
  createImageEditTools,
14639
15126
  createImageEditorToolProvider,
14640
15127
  createImageEffectTools,
15128
+ createImageEffectsToolProvider,
14641
15129
  createImageLayerTools,
14642
15130
  createImagePaintTools,
14643
15131
  createImagePsdTools,
@@ -14716,6 +15204,7 @@ export {
14716
15204
  edgeWidth,
14717
15205
  effectColorHex,
14718
15206
  effectControlChange,
15207
+ effectPixelsToSurface,
14719
15208
  effectReach,
14720
15209
  effectiveProof,
14721
15210
  effectsFingerprint,
@@ -14797,6 +15286,7 @@ export {
14797
15286
  identityTransform,
14798
15287
  identityWarpMesh,
14799
15288
  imageEditorStateMigrations,
15289
+ imageLayerAppliedEffects,
14800
15290
  imageToCanvasPoint,
14801
15291
  importPsd,
14802
15292
  inflate,
@@ -14831,6 +15321,7 @@ export {
14831
15321
  isSelectionEmpty,
14832
15322
  isSelectionMode,
14833
15323
  isSeparableBlendMode,
15324
+ isSmartSurfaceIncomplete,
14834
15325
  isSpatialFilter,
14835
15326
  isSrgb,
14836
15327
  isTransformAnchor,
@@ -14866,7 +15357,7 @@ export {
14866
15357
  lerpPigmentInto,
14867
15358
  levelsValue,
14868
15359
  linearSrgbToXy,
14869
- linearToSrgb,
15360
+ linearToSrgb2 as linearToSrgb,
14870
15361
  linearToSrgb8,
14871
15362
  liveComponent,
14872
15363
  liveShapeForDrag,