@pixerate/editor 0.123.0 → 0.124.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 (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-TVXFHE4T.js → chunk-64PYNEHJ.js} +45 -12
  20. package/dist/chunk-64PYNEHJ.js.map +1 -0
  21. package/dist/{chunk-2IW3BWRT.cjs → chunk-7LQO6OHH.cjs} +2 -2
  22. package/dist/{chunk-2IW3BWRT.cjs.map → chunk-7LQO6OHH.cjs.map} +1 -1
  23. package/dist/chunk-IOXKXQNW.cjs +178 -0
  24. package/dist/chunk-IOXKXQNW.cjs.map +1 -0
  25. package/dist/{chunk-SMJGE7HM.cjs → chunk-IWSMDQE5.cjs} +523 -39
  26. package/dist/chunk-IWSMDQE5.cjs.map +1 -0
  27. package/dist/{chunk-KMBC4HNV.js → chunk-J67OLQUQ.js} +2 -2
  28. package/dist/{chunk-6CJRDJCU.cjs → chunk-PEHYWUVV.cjs} +176 -111
  29. package/dist/chunk-PEHYWUVV.cjs.map +1 -0
  30. package/dist/chunk-TGKT5QAB.js +178 -0
  31. package/dist/chunk-TGKT5QAB.js.map +1 -0
  32. package/dist/chunk-TK53AB4Q.cjs +215 -0
  33. package/dist/chunk-TK53AB4Q.cjs.map +1 -0
  34. package/dist/chunk-VFAKN46Z.js +215 -0
  35. package/dist/chunk-VFAKN46Z.js.map +1 -0
  36. package/dist/{chunk-6ZBR4J62.js → chunk-WATAQ2Z2.js} +79 -14
  37. package/dist/chunk-WATAQ2Z2.js.map +1 -0
  38. package/dist/{chunk-IPFRWILU.js → chunk-YVVSTD3B.js} +499 -15
  39. package/dist/chunk-YVVSTD3B.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 +588 -0
  43. package/dist/effects/index.cjs.map +1 -0
  44. package/dist/effects/index.d.cts +131 -0
  45. package/dist/effects/index.d.ts +131 -0
  46. package/dist/effects/index.js +588 -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-8RnlngAv.d.ts} +534 -128
  51. package/dist/{export-Dv0-ZVrV.d.ts → export-BzSRXyH6.d.cts} +390 -5
  52. package/dist/{export-Dv0-ZVrV.d.cts → export-DM3h7QEI.d.ts} +390 -5
  53. package/dist/{export-lU5qSpHE.d.cts → export-TqXMOlDt.d.cts} +534 -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 +1307 -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 +508 -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,24 @@ 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-VFAKN46Z.js";
375
+ import {
376
+ EffectUnsupportedError,
377
+ EffectsSlot,
378
+ createEffectPixels,
379
+ linearToSrgb,
380
+ resolveEffectValues
381
+ } from "../chunk-TGKT5QAB.js";
382
+ import "../chunk-4FX5ACVG.js";
370
383
  import {
371
384
  createCaptureTool
372
385
  } from "../chunk-REOJJIY3.js";
@@ -475,7 +488,7 @@ import {
475
488
  pathStrokeCoverage,
476
489
  pathStrokeDabs,
477
490
  renderBrushstroke
478
- } from "../chunk-TVXFHE4T.js";
491
+ } from "../chunk-64PYNEHJ.js";
479
492
  import {
480
493
  BODY_SCATTER,
481
494
  DEFAULT_FLOW,
@@ -2772,6 +2785,182 @@ function refineSelection(pixels, mask, params = {}) {
2772
2785
  return createSelectionMask(mask.width, mask.height, out);
2773
2786
  }
2774
2787
 
2788
+ // src/image-editor/effects.ts
2789
+ var IMAGE_EFFECT_PLACES = {
2790
+ /** A smart filter: the effect changes the pixels below it. */
2791
+ filter: ["filter"],
2792
+ /** An effect layer: drawn at the document's size. */
2793
+ layer: ["generator", "overlay", "title"]
2794
+ };
2795
+ function checkImageEffectPlace(d, place) {
2796
+ const allowed = IMAGE_EFFECT_PLACES[place];
2797
+ if (d.kind === "motion" || !d.uses.some((u) => allowed.includes(u))) {
2798
+ 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" : "";
2799
+ throw new EffectUnsupportedError(
2800
+ `"${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}.`,
2801
+ d.id
2802
+ );
2803
+ }
2804
+ }
2805
+ function over(base, top) {
2806
+ const out = new Uint8ClampedArray(base.length);
2807
+ for (let i = 0; i < out.length; i += 4) {
2808
+ const ta = top[i + 3] / 255;
2809
+ const ba = base[i + 3] / 255;
2810
+ const a = ta + ba * (1 - ta);
2811
+ out[i + 3] = Math.round(a * 255);
2812
+ if (a > 0) for (let c = 0; c < 3; c++) out[i + c] = (top[i + c] * ta + base[i + c] * ba * (1 - ta)) / a;
2813
+ }
2814
+ return out;
2815
+ }
2816
+ function effectPixelsToSurface(p, depth) {
2817
+ if (depth === 8 || !p.float) return surfaceFromPixels({ width: p.width, height: p.height, data: p.data }, 4);
2818
+ if (depth === 32) return surfaceFromDeepPixels({ width: p.width, height: p.height, data: p.float }, 4);
2819
+ const f = p.float;
2820
+ const d = new Uint16Array(f.length);
2821
+ const code = (v) => v > 0 ? v < 1 ? Math.round(v * 65535) : 65535 : 0;
2822
+ for (let i = 0; i < f.length; i += 4) {
2823
+ d[i] = code(linearToSrgb(f[i]));
2824
+ d[i + 1] = code(linearToSrgb(f[i + 1]));
2825
+ d[i + 2] = code(linearToSrgb(f[i + 2]));
2826
+ d[i + 3] = code(f[i + 3]);
2827
+ }
2828
+ return surfaceFromDeepPixels({ width: p.width, height: p.height, data: d }, 4);
2829
+ }
2830
+ var ImageEffectsEngine = class {
2831
+ slot;
2832
+ cache = /* @__PURE__ */ new Map();
2833
+ pending = /* @__PURE__ */ new Map();
2834
+ failures = /* @__PURE__ */ new Map();
2835
+ renderers = /* @__PURE__ */ new Map();
2836
+ descriptions = /* @__PURE__ */ new Map();
2837
+ listeners = /* @__PURE__ */ new Set();
2838
+ maxEntries;
2839
+ generation = 0;
2840
+ detach;
2841
+ constructor(options = {}) {
2842
+ this.slot = options.slot ?? new EffectsSlot();
2843
+ this.maxEntries = options.maxEntries ?? 24;
2844
+ this.detach = this.slot.subscribe(() => {
2845
+ this.reset();
2846
+ this.notify();
2847
+ });
2848
+ }
2849
+ get provider() {
2850
+ return this.slot.provider;
2851
+ }
2852
+ /** See `SmartEffectResolver.token`: bumped when the provider changes. */
2853
+ get token() {
2854
+ return this.generation;
2855
+ }
2856
+ /** True while a render is under way. */
2857
+ get busy() {
2858
+ return this.pending.size > 0;
2859
+ }
2860
+ /** See `SmartEffectResolver`. */
2861
+ resolve(filter, input, width, height, key) {
2862
+ const hit = this.cache.get(key);
2863
+ if (hit) {
2864
+ this.cache.delete(key);
2865
+ this.cache.set(key, hit);
2866
+ return { result: hit, pending: false };
2867
+ }
2868
+ if (this.pending.has(key)) return { result: null, pending: true };
2869
+ const provider = this.slot.provider;
2870
+ if (!provider || this.failures.has(key)) return { result: null, pending: false };
2871
+ const generation = this.generation;
2872
+ const source = { width, height, data: new Uint8ClampedArray(input) };
2873
+ const run = this.filter(provider, filter.effect, filter.values, source).then((out) => {
2874
+ if (generation !== this.generation) return;
2875
+ this.cache.set(key, out);
2876
+ while (this.cache.size > this.maxEntries) this.cache.delete(this.cache.keys().next().value);
2877
+ }).catch((error) => {
2878
+ if (generation === this.generation) this.failures.set(key, error instanceof Error ? error : new Error(String(error)));
2879
+ }).finally(() => {
2880
+ if (this.pending.get(key) === run) this.pending.delete(key);
2881
+ if (generation === this.generation) this.notify();
2882
+ });
2883
+ this.pending.set(key, run);
2884
+ return { result: null, pending: true };
2885
+ }
2886
+ /** A filter effect over `source`: its result, or (for an overlay) the effect drawn over it. */
2887
+ async filter(provider, effect, values, source) {
2888
+ const d = await this.describe(provider, effect);
2889
+ const takesSource = d.inputs.some((i) => i.name === "source");
2890
+ const out = await this.render(provider, effect, source.width, source.height, 8, { time: 0, values, ...takesSource ? { inputs: { source } } : {} });
2891
+ return takesSource ? out.data : over(source.data, out.data);
2892
+ }
2893
+ /**
2894
+ * Renders a generator or overlay effect at a size and depth (an effect layer's pixels). Rejects
2895
+ * with the provider's error, or without a provider.
2896
+ */
2897
+ async renderLayer(effect, values, size) {
2898
+ const provider = this.slot.require();
2899
+ const depth = size.depth ?? 8;
2900
+ const out = await this.render(provider, effect, size.width, size.height, depth, { time: size.time ?? 0, values });
2901
+ return out.width === size.width && out.height === size.height ? out : createEffectPixels(size.width, size.height);
2902
+ }
2903
+ /** Why the result for `key` failed, if it did. */
2904
+ failure(key) {
2905
+ return this.failures.get(key);
2906
+ }
2907
+ /** Every failure so far. */
2908
+ failuresList() {
2909
+ return [...this.failures].map(([key, error]) => ({ key, error }));
2910
+ }
2911
+ /** Resolves when nothing is rendering. */
2912
+ async idle() {
2913
+ while (this.pending.size) await Promise.all([...this.pending.values()]);
2914
+ }
2915
+ /** The provider's description of an effect, fetched once per provider. */
2916
+ describe(provider, id) {
2917
+ let d = this.descriptions.get(id);
2918
+ if (!d) {
2919
+ d = Promise.resolve().then(() => provider.describe(id));
2920
+ d.catch(() => this.descriptions.delete(id));
2921
+ this.descriptions.set(id, d);
2922
+ }
2923
+ return d;
2924
+ }
2925
+ render(provider, id, width, height, depth, request) {
2926
+ const k = `${id}@${width}x${height}x${depth}`;
2927
+ let r = this.renderers.get(k);
2928
+ if (!r) {
2929
+ const renderer = Promise.resolve().then(() => provider.createRenderer(id, { width, height, depth }));
2930
+ renderer.catch(() => this.renderers.delete(k));
2931
+ r = { renderer, queue: Promise.resolve() };
2932
+ this.renderers.set(k, r);
2933
+ }
2934
+ const entry = r;
2935
+ const next = entry.queue.then(() => entry.renderer).then((renderer) => renderer.render(request));
2936
+ entry.queue = next.catch(() => void 0);
2937
+ return next;
2938
+ }
2939
+ /** Forgets every result and failure and disposes the renderers. */
2940
+ reset() {
2941
+ this.generation++;
2942
+ this.cache.clear();
2943
+ this.pending.clear();
2944
+ this.failures.clear();
2945
+ this.descriptions.clear();
2946
+ for (const r of this.renderers.values()) r.renderer.then((x) => x.dispose(), () => {
2947
+ });
2948
+ this.renderers.clear();
2949
+ }
2950
+ subscribe(listener) {
2951
+ this.listeners.add(listener);
2952
+ return () => this.listeners.delete(listener);
2953
+ }
2954
+ notify() {
2955
+ for (const l of [...this.listeners]) l();
2956
+ }
2957
+ destroy() {
2958
+ this.reset();
2959
+ this.detach();
2960
+ this.listeners.clear();
2961
+ }
2962
+ };
2963
+
2775
2964
  // src/image-editor/free-transform.ts
2776
2965
  var FREE_TRANSFORM_CONTENTS = ["auto", "layer", "selection", "selection-copy", "selection-outline"];
2777
2966
  var fail = (message) => {
@@ -3885,7 +4074,15 @@ var ImageEditorController = class {
3885
4074
  viewKey = [];
3886
4075
  /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */
3887
4076
  compositor = new LayerCompositor();
4077
+ /** The effects provider slot (see `setEffectsProvider`). */
4078
+ effects = new EffectsSlot();
4079
+ /** Renders provider effects: effect smart filter results and effect layers. */
4080
+ effectsEngine = new ImageEffectsEngine({ slot: this.effects });
4081
+ effectRefresh = "none";
3888
4082
  constructor(options = {}) {
4083
+ this.effects.subscribe(() => this.queueEffectRefresh("all"));
4084
+ this.effectsEngine.subscribe(() => this.queueEffectRefresh("incomplete"));
4085
+ if (options.effects) this.effects.set(options.effects);
3889
4086
  this.maxHistoryDepth = options.maxHistoryDepth ?? 50;
3890
4087
  if (options.textShaper !== void 0) this.textShaperValue = options.textShaper;
3891
4088
  this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;
@@ -4019,6 +4216,206 @@ var ImageEditorController = class {
4019
4216
  const currentState = { ...this.state };
4020
4217
  this.onStateChange?.(currentState);
4021
4218
  this.listeners.forEach((l) => l(currentState));
4219
+ if (this.effects.provider) this.queueEffectRefresh("incomplete");
4220
+ }
4221
+ // --- Provider effects ---
4222
+ queueEffectRefresh(kind) {
4223
+ const queued = this.effectRefresh !== "none";
4224
+ if (kind === "all" || this.effectRefresh === "none") this.effectRefresh = kind;
4225
+ if (queued) return;
4226
+ queueMicrotask(() => {
4227
+ const all = this.effectRefresh === "all";
4228
+ this.effectRefresh = "none";
4229
+ this.refreshEffectSurfaces(all);
4230
+ });
4231
+ }
4232
+ /**
4233
+ * Renders smart objects with effect filters again: every one (`all`, after the provider changed)
4234
+ * or those drawn while a result was rendering, once it is in. A cache refresh: no undo step.
4235
+ */
4236
+ refreshEffectSurfaces(all) {
4237
+ let changed = false;
4238
+ const canvas = this.canvasSize;
4239
+ const walk = (layers) => {
4240
+ let out = null;
4241
+ layers.forEach((l, i) => {
4242
+ let next2 = l;
4243
+ if (l.type === "group") {
4244
+ const children = walk(l.children);
4245
+ if (children !== l.children) next2 = { ...l, children };
4246
+ } else if (l.type === "smart" && l.smartFilters?.filters.some((f) => f.kind === "effect") && (all || isSmartSurfaceIncomplete(l.surface))) {
4247
+ try {
4248
+ const r = rerender(l, canvas);
4249
+ if (all || !isSmartSurfaceIncomplete(r.surface)) next2 = r;
4250
+ } catch {
4251
+ }
4252
+ }
4253
+ if (next2 !== l) {
4254
+ out ??= [...layers];
4255
+ out[i] = next2;
4256
+ changed = true;
4257
+ }
4258
+ });
4259
+ return out ?? layers;
4260
+ };
4261
+ const next = walk(this.state.layers);
4262
+ if (!changed) return;
4263
+ this.state.layers = next;
4264
+ this.notify();
4265
+ }
4266
+ /** The registered effects provider, or `null`. */
4267
+ get effectsProvider() {
4268
+ return this.effects.provider;
4269
+ }
4270
+ /** Registers (or with `null` removes) the effects provider. Not an edit: no undo step. */
4271
+ setEffectsProvider(provider) {
4272
+ this.effects.set(provider);
4273
+ }
4274
+ /** Registers `provider` and returns a function that removes it again (if it is still the one set). */
4275
+ useEffects(provider) {
4276
+ return this.effects.use(provider);
4277
+ }
4278
+ /** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
4279
+ async describeEffect(effect, place) {
4280
+ const provider = this.effects.require();
4281
+ const d = await this.effectsEngine.describe(provider, effect);
4282
+ if (place) checkImageEffectPlace(d, place);
4283
+ return d;
4284
+ }
4285
+ /** Resolves when every effect render has landed and the surfaces waiting for them are drawn. */
4286
+ async effectsIdle() {
4287
+ const tick = () => new Promise((r) => queueMicrotask(r));
4288
+ for (let round6 = 0; round6 < 32; round6++) {
4289
+ await this.effectsEngine.idle();
4290
+ await tick();
4291
+ await tick();
4292
+ if (!this.effectsEngine.busy && this.effectRefresh === "none") return;
4293
+ }
4294
+ }
4295
+ /**
4296
+ * Applies a provider filter effect as a smart filter (at `index`, default the top), one undo
4297
+ * step. A pixel, shape or type layer (or a group) is first converted to a smart object in the
4298
+ * same step (Filter › Convert for Smart Filters), keeping its id when it is one layer. `values`
4299
+ * are checked against the effect's controls and stored over its defaults.
4300
+ */
4301
+ async applyEffectFilter(layerId, effect, options = {}) {
4302
+ const d = await this.describeEffect(effect, "filter");
4303
+ const values = resolveEffectValues(d, options.values);
4304
+ const found = findLayer(this.state.layers, layerId);
4305
+ if (!found) throw new LayerOperationError(`There is no layer "${layerId}"; call list_layers for the layer ids.`);
4306
+ let layers = this.state.layers;
4307
+ let id = layerId;
4308
+ let converted = false;
4309
+ if (found.layer.type !== "smart") {
4310
+ 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.`);
4311
+ const res2 = convertToSmartObject(layers, [layerId], this.canvasSize, { surfaceOf: (l) => this.surfaceOf(l) });
4312
+ layers = res2.layers;
4313
+ id = res2.layer.id;
4314
+ converted = true;
4315
+ }
4316
+ 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 });
4317
+ const res = addSmartFilter(layers, id, filter, this.canvasSize, options.index);
4318
+ this.commitLayers(res.layers);
4319
+ return { layerId: id, filter: res.filter, converted };
4320
+ }
4321
+ /**
4322
+ * Changes an effect smart filter: `values` merged over its own (`null` resets a control to the
4323
+ * effect's default), `enabled`, `blend`, `opacity` or its place in the stack (`index`). Values
4324
+ * are checked against the provider's controls. One undo step (none with `commitToHistory`
4325
+ * false, during a drag).
4326
+ */
4327
+ async updateEffectFilter(layerId, filterId, changes, commitToHistory = true) {
4328
+ const layer = requireSmartObject(this.state.layers, layerId);
4329
+ const was = layer.smartFilters?.filters.find((f) => f.id === filterId);
4330
+ 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"}.`);
4331
+ const update = {};
4332
+ if (changes.values) {
4333
+ const d = await this.describeEffect(was.effect);
4334
+ const given = {};
4335
+ const base = { ...was.values };
4336
+ for (const [k, v] of Object.entries(changes.values)) {
4337
+ if (v !== null) given[k] = v;
4338
+ else if (k in d.controls.properties) base[k] = d.defaults[k];
4339
+ else throw new LayerOperationError(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
4340
+ }
4341
+ update.values = { ...base, ...resolveEffectValues({ ...d, defaults: base }, given) };
4342
+ }
4343
+ for (const k of ["enabled", "blend", "opacity"]) if (changes[k] !== void 0) update[k] = changes[k];
4344
+ let res = updateSmartFilter(this.state.layers, layerId, filterId, update, this.canvasSize);
4345
+ if (changes.index !== void 0) res = { ...res, layers: moveSmartFilter(res.layers, layerId, filterId, changes.index, this.canvasSize).layers };
4346
+ this.commitLayers(res.layers, void 0, commitToHistory);
4347
+ return requireSmartObject(this.state.layers, layerId).smartFilters.filters.find((f) => f.id === filterId);
4348
+ }
4349
+ /**
4350
+ * Adds an effect layer: a pixel layer drawn by a provider generator, overlay or title effect at
4351
+ * the document's size and bit depth (at `time` ms, default 0), placed like `addPixelLayer`. One
4352
+ * undo step. Masks, blend and opacity apply as to any layer. Returns its id.
4353
+ */
4354
+ async addEffectLayer(effect, options = {}) {
4355
+ const d = await this.describeEffect(effect, "layer");
4356
+ const values = resolveEffectValues(d, options.values);
4357
+ const { width, height } = this.naturalSize;
4358
+ const pixels = await this.effectsEngine.renderLayer(d.id, values, { width, height, depth: this.bitDepth, time: options.time });
4359
+ const blend = parseBlend(options.blend);
4360
+ if (options.opacity !== void 0 && !(options.opacity >= 0 && options.opacity <= 1)) throw new LayerOperationError("opacity must be from 0 to 1.");
4361
+ const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));
4362
+ const layer = {
4363
+ type: "pixel",
4364
+ id: options.id && !ids.has(options.id) ? options.id : newLayerId(),
4365
+ name: options.name?.trim() || d.name,
4366
+ visible: true,
4367
+ opacity: options.opacity ?? 1,
4368
+ blend: blend ?? "normal",
4369
+ surface: effectPixelsToSurface(pixels, this.bitDepth),
4370
+ offset: { x: 0, y: 0 },
4371
+ generator: { effect: d.id, provider: this.effects.require().id, name: d.name, values, ...options.time ? { time: options.time } : {} }
4372
+ };
4373
+ const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(options));
4374
+ this.commitLayers(next, () => {
4375
+ this.state.activeLayerId = layer.id;
4376
+ this.state.activeTarget = "pixels";
4377
+ });
4378
+ return layer.id;
4379
+ }
4380
+ /**
4381
+ * Draws an effect layer again with new `values` (merged over its own; `null` resets a control)
4382
+ * or `time`, at the document's size and depth, replacing its pixels (painting on it is lost).
4383
+ * One undo step (none with `commitToHistory` false). Returns its effect settings.
4384
+ */
4385
+ async updateEffectLayer(layerId, changes, commitToHistory = true) {
4386
+ const found = findLayer(this.state.layers, layerId);
4387
+ const layer = found?.layer;
4388
+ if (!layer || layer.type !== "pixel" || !layer.generator) throw new LayerOperationError(`"${layerId}" is not an effect layer; add one with add_effect_layer.`);
4389
+ requireUnlocked(layer, "pixels", "draw its effect again");
4390
+ const g = layer.generator;
4391
+ const d = await this.describeEffect(g.effect);
4392
+ const base = { ...g.values };
4393
+ const given = {};
4394
+ for (const [k, v] of Object.entries(changes.values ?? {})) {
4395
+ if (v !== null) given[k] = v;
4396
+ else if (k in d.controls.properties) base[k] = d.defaults[k];
4397
+ else throw new LayerOperationError(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
4398
+ }
4399
+ const values = { ...base, ...resolveEffectValues({ ...d, defaults: base }, given) };
4400
+ const time = changes.time ?? g.time ?? 0;
4401
+ const { width, height } = this.naturalSize;
4402
+ const pixels = await this.effectsEngine.renderLayer(d.id, values, { width, height, depth: this.bitDepth, time });
4403
+ const generator = { ...g, values, provider: this.effects.require().id, ...time ? { time } : {} };
4404
+ if (!time) delete generator.time;
4405
+ const next = mapLayer(this.state.layers, layerId, (l) => ({ ...l, surface: effectPixelsToSurface(pixels, this.bitDepth), offset: { x: 0, y: 0 }, generator }));
4406
+ this.commitLayers(next, void 0, commitToHistory);
4407
+ return generator;
4408
+ }
4409
+ /** Turns an effect layer into a plain pixel layer keeping its pixels (one undo step). */
4410
+ rasterizeEffectLayer(layerId) {
4411
+ const found = findLayer(this.state.layers, layerId);
4412
+ if (!found || found.layer.type !== "pixel" || !found.layer.generator) throw new LayerOperationError(`"${layerId}" is not an effect layer.`);
4413
+ this.commitLayers(
4414
+ mapLayer(this.state.layers, layerId, (l) => {
4415
+ const { generator: _g, ...rest } = l;
4416
+ return rest;
4417
+ })
4418
+ );
4022
4419
  }
4023
4420
  displayFor = null;
4024
4421
  /** Keeps the renderer's display transform in step with the document profile, mode and Proof Setup. */
@@ -4918,7 +5315,7 @@ var ImageEditorController = class {
4918
5315
  }
4919
5316
  // --- Smart objects ---
4920
5317
  get canvasSize() {
4921
- return this.naturalSize;
5318
+ return { ...this.naturalSize, effects: this.effectsEngine };
4922
5319
  }
4923
5320
  /**
4924
5321
  * Layer › Smart Objects › Convert to Smart Object (one undo step): sibling layers (default the
@@ -13440,7 +13837,7 @@ function smartFilterRows(layer) {
13440
13837
  filter: f,
13441
13838
  index,
13442
13839
  drawn: f.kind !== "psd",
13443
- label: f.kind === "psd" ? f.name.replace(/&/g, "").replace(/\.\.\.$/, "") : f.kind === "blur" ? `Blur (${f.radius} px)` : "Develop"
13840
+ 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
13841
  }));
13445
13842
  return rows.reverse();
13446
13843
  }
@@ -14295,6 +14692,103 @@ var FreeTransformTool = class {
14295
14692
  return false;
14296
14693
  }
14297
14694
  };
14695
+
14696
+ // src/image-editor/provider-effect-tools.ts
14697
+ function imageLayerAppliedEffects(layer) {
14698
+ const out = [];
14699
+ const ref = (r) => ({ effect: r.effect, ...r.name ? { name: r.name } : {}, ...r.provider ? { provider: r.provider } : {} });
14700
+ 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 } : {} });
14701
+ if (layer.type === "smart")
14702
+ (layer.smartFilters?.filters ?? []).forEach((f, index) => {
14703
+ 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 });
14704
+ });
14705
+ return out;
14706
+ }
14707
+ function createImageEffectsToolProvider(controller, options = {}) {
14708
+ const readOnlyReason = () => {
14709
+ const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
14710
+ return ro ? typeof ro === "string" ? ro : "The image is read-only." : null;
14711
+ };
14712
+ const layerOf = (id) => {
14713
+ const found = findLayer(controller.getState().layers, id);
14714
+ if (!found) {
14715
+ const ids = flattenLayers(controller.getState().layers).map((l) => `"${l.id}"`);
14716
+ throw new Error(`There is no layer "${id}"; the layers are ${ids.join(", ") || "none"} (list_layers lists them).`);
14717
+ }
14718
+ return found.layer;
14719
+ };
14720
+ const result = (target, instance, extra = {}) => {
14721
+ const effects = imageLayerAppliedEffects(layerOf(target));
14722
+ return { target, effect: effects.find((e) => e.id === instance) ?? effects[effects.length - 1], effects, ...extra };
14723
+ };
14724
+ const plainValues = (v) => v;
14725
+ return createEffectToolProvider({
14726
+ id: options.id ?? "image-effects",
14727
+ namespace: options.namespace,
14728
+ target: { noun: "layer", id: "A layer id from list_layers." },
14729
+ applyUses: ["filter"],
14730
+ 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.",
14731
+ provider: () => controller.effectsProvider,
14732
+ readOnlyReason,
14733
+ list: (target) => {
14734
+ const layers = target ? [layerOf(target)] : flattenLayers(controller.getState().layers);
14735
+ return layers.map((l) => ({ target: l.id, ...l.name ? { name: l.name } : {}, effects: imageLayerAppliedEffects(l) })).filter((t) => target || t.effects.length);
14736
+ },
14737
+ apply: async (target, effect, args) => {
14738
+ const r = await controller.applyEffectFilter(target, effect, { values: plainValues(args.values), index: args.index, enabled: args.enabled, opacity: args.opacity });
14739
+ return result(r.layerId, r.filter.id, { converted: r.converted });
14740
+ },
14741
+ update: async (target, instance, changes) => {
14742
+ const layer = layerOf(target);
14743
+ if (instance === "generator") {
14744
+ const extra = ["enabled", "opacity", "index"].filter((k) => changes[k] !== void 0);
14745
+ if (extra.length) throw new Error(`An effect layer's own effect only takes values; change the layer's ${extra.join(", ")} with update_layer.`);
14746
+ if (layer.type !== "pixel" || !layer.generator) throw new Error(`"${target}" is not an effect layer.`);
14747
+ await controller.updateEffectLayer(target, { values: changes.values });
14748
+ return result(target, "generator");
14749
+ }
14750
+ await controller.updateEffectFilter(target, instance, { values: changes.values, enabled: changes.enabled, opacity: changes.opacity, index: changes.index });
14751
+ return result(target, instance);
14752
+ },
14753
+ remove: async (target, instance) => {
14754
+ const layer = layerOf(target);
14755
+ const was = imageLayerAppliedEffects(layer).find((e) => e.id === instance);
14756
+ 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).`);
14757
+ if (instance === "generator") controller.rasterizeEffectLayer(target);
14758
+ else controller.removeSmartFilter(target, instance);
14759
+ return { target, effect: was, effects: imageLayerAppliedEffects(layerOf(target)) };
14760
+ },
14761
+ subscribe: (notify) => controller.subscribe(() => notify()),
14762
+ extraTools: (ctx) => [
14763
+ {
14764
+ name: "add_effect_layer",
14765
+ title: "Add an effect layer",
14766
+ 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.`,
14767
+ inputSchema: {
14768
+ type: "object",
14769
+ properties: {
14770
+ effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
14771
+ values: ctx.valuesSchema("Control values over the effect's defaults"),
14772
+ time: { type: "number", minimum: 0, description: "Which moment of an animated effect to draw, in ms (default 0)." },
14773
+ name: { type: "string", minLength: 1, description: "The layer name (default: the effect's name)." },
14774
+ aboveId: { type: "string", minLength: 1, description: "Put it directly above this layer (default: above the active layer, else on top)." },
14775
+ belowId: { type: "string", minLength: 1, description: "Put it directly below this layer." },
14776
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131 (default 1)." },
14777
+ 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." }
14778
+ },
14779
+ required: ["effect"],
14780
+ additionalProperties: false
14781
+ },
14782
+ annotations: { title: "Add an effect layer", ...ctx.annotations.write },
14783
+ handler: async (args) => {
14784
+ ctx.requireProvider();
14785
+ 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 });
14786
+ return { layerId: id, effects: imageLayerAppliedEffects(layerOf(id)) };
14787
+ }
14788
+ }
14789
+ ]
14790
+ });
14791
+ }
14298
14792
  export {
14299
14793
  ADJUSTMENT_KEYS,
14300
14794
  ADJUSTMENT_KINDS,
@@ -14372,12 +14866,14 @@ export {
14372
14866
  IMAGE_CROP_PRESETS,
14373
14867
  IMAGE_EDITOR_SCHEMA_VERSION,
14374
14868
  IMAGE_EDITOR_STATE_VERSION,
14869
+ IMAGE_EFFECT_PLACES,
14375
14870
  IMAGE_FILTER_PRESETS,
14376
14871
  INTENTS,
14377
14872
  INTENT_OPTIONS,
14378
14873
  INTERPOLATIONS,
14379
14874
  ImageEditorController,
14380
14875
  ImageEditorRenderer,
14876
+ ImageEffectsEngine,
14381
14877
  LATEST_PROCESS_VERSION,
14382
14878
  LAYERED_SCHEMA_VERSION,
14383
14879
  LAYER_EFFECT_KINDS,
@@ -14522,6 +15018,7 @@ export {
14522
15018
  XYZ_D50_TO_SRGB,
14523
15019
  ZlibError,
14524
15020
  activeAdjustmentLayers,
15021
+ activeEffectSmartFilters,
14525
15022
  activeSmartFilters,
14526
15023
  addLayerEffect,
14527
15024
  addLayerFilter,
@@ -14599,6 +15096,7 @@ export {
14599
15096
  canvasToImagePoint,
14600
15097
  caretAt,
14601
15098
  changedTiles,
15099
+ checkImageEffectPlace,
14602
15100
  clampControlValue,
14603
15101
  clampCropToImage,
14604
15102
  cleanMixer,
@@ -14638,6 +15136,7 @@ export {
14638
15136
  createImageEditTools,
14639
15137
  createImageEditorToolProvider,
14640
15138
  createImageEffectTools,
15139
+ createImageEffectsToolProvider,
14641
15140
  createImageLayerTools,
14642
15141
  createImagePaintTools,
14643
15142
  createImagePsdTools,
@@ -14716,6 +15215,7 @@ export {
14716
15215
  edgeWidth,
14717
15216
  effectColorHex,
14718
15217
  effectControlChange,
15218
+ effectPixelsToSurface,
14719
15219
  effectReach,
14720
15220
  effectiveProof,
14721
15221
  effectsFingerprint,
@@ -14797,6 +15297,7 @@ export {
14797
15297
  identityTransform,
14798
15298
  identityWarpMesh,
14799
15299
  imageEditorStateMigrations,
15300
+ imageLayerAppliedEffects,
14800
15301
  imageToCanvasPoint,
14801
15302
  importPsd,
14802
15303
  inflate,
@@ -14831,6 +15332,7 @@ export {
14831
15332
  isSelectionEmpty,
14832
15333
  isSelectionMode,
14833
15334
  isSeparableBlendMode,
15335
+ isSmartSurfaceIncomplete,
14834
15336
  isSpatialFilter,
14835
15337
  isSrgb,
14836
15338
  isTransformAnchor,
@@ -14866,7 +15368,7 @@ export {
14866
15368
  lerpPigmentInto,
14867
15369
  levelsValue,
14868
15370
  linearSrgbToXy,
14869
- linearToSrgb,
15371
+ linearToSrgb2 as linearToSrgb,
14870
15372
  linearToSrgb8,
14871
15373
  liveComponent,
14872
15374
  liveShapeForDrag,